84 Commits

Author SHA1 Message Date
gsemaj
02fd17c0e7 Implement leech 2023-08-13 18:27:18 -07:00
gsemaj
da33c5c30f Make clearBuffs memory-safe
improperly erasing during iteration oops
2023-08-13 18:26:30 -07:00
gsemaj
478ae3d996 Break if player dies from buff combat tick 2023-08-13 14:26:05 -07:00
gsemaj
9affe76587 Fix segv: Halt buff iteration if mob is dead 2023-08-13 13:59:28 -07:00
gsemaj
33fde8af69 Tighten parameter for removeBuff 2023-08-13 10:50:50 -07:00
gsemaj
1a7bb826d8 Move drain to new system
TODO
- There is a seg fault that happens when drain kills the mob
- leech unimplemented
- mob skills unimplemented
2023-07-25 20:30:54 +00:00
gsemaj
da9fb499de reorder some stuff 2023-07-25 19:49:38 +00:00
gsemaj
2bc2235179 Fix icon not disappearing for debuffs 2023-07-25 19:11:28 +00:00
gsemaj
618126f963 Tick buffs for mobs 2023-07-25 19:00:35 +00:00
gsemaj
b16ffe4f19 Fix infinite slowdown with snare 2023-07-25 19:00:01 +00:00
gsemaj
6edc01c1f1 Implement buff handling for CombatNPC 2023-07-25 13:49:40 -04:00
gsemaj
c0e566050b Move buffs from Mob to CombatNPC 2023-07-25 13:42:40 -04:00
gsemaj
7a59248ace Implement buffs for mobs 2023-07-25 13:19:49 -04:00
gsemaj
6ee55d7406 Damage n debuff handler 2023-07-25 13:09:28 -04:00
gsemaj
41622ad8aa Fix trailing structs
The change to allow flexible trailing struct sizes broke
`attachSkillResults` oops
2023-07-25 17:05:44 +00:00
gsemaj
0f09808bc4 Get rid of cbf 2023-07-25 10:43:48 -04:00
gsemaj
b6171ebdc1 Add clearBuffs 2023-07-25 10:41:45 -04:00
gsemaj
5671adbd74 Make skill result size check an assertion 2023-07-23 10:46:53 -04:00
gsemaj
550b26db39 Oops 2023-07-23 10:42:07 -04:00
gsemaj
68be9cc381 Change SkillResult size validation
Since leech uses trailing structs of two different sizes, just use the max SkillResult size in validation/zeroing and then check for overflow in a couple extra places
2023-07-23 10:31:40 -04:00
gsemaj
ef33a182d1 Reorder abilities to match client handling 2023-07-23 10:15:00 -04:00
gsemaj
9bd8eabed7 Don't add buff if player dead 2023-07-23 10:13:22 -04:00
gsemaj
2249b5381b Fix seg fault with egg powers 2023-07-22 15:10:00 -04:00
gsemaj
9b7656117d Reuse useNanoSkill codepath for passive powers 2023-07-20 17:53:13 -04:00
gsemaj
9394825d41 Fix recall 2023-07-12 17:43:05 -04:00
gsemaj
6e0d55c4b7 Make SkillType an enum class 2023-07-12 17:37:05 -04:00
gsemaj
33396f8d28 Fix self recall 2023-07-11 20:21:37 -04:00
gsemaj
cd908666af [WIP] Fix targeting for groups 2023-07-11 17:42:08 -04:00
gsemaj
74588f2c77 [WIP] Active power handling
TODO:
- recall (self and group) is broken
- revive (only group) is broken
- damage + debuff is unimplemented
2023-07-11 13:52:59 -04:00
gsemaj
829f75112c Sync with master 2023-07-11 13:52:59 -04:00
gsemaj
343668fbcd Add overload to remove specific class of buff
I initially added this because, despite the higher tickrate for
composite condition calculations thanks to the last commit, there is
still a slight status icon delay when rapidly switching nanos. I
attempted to use this to make that problem go away and for whatever
reason it wasn't effective, but I figure it would be useful to have
anyway so I'm keeping it.
2023-07-11 13:52:59 -04:00
gsemaj
2e572169c0 Move some stuff from playerTick to player combat step 2023-07-11 13:52:59 -04:00
gsemaj
b3e28ddea3 Refactor group handling 2023-07-11 13:52:58 -04:00
gsemaj
1670dfd830 Port egg buffs over to new system 2023-07-11 13:52:58 -04:00
gsemaj
e768ebcabe More skill handlers
Note: need to revisit these when active powers are implemented to make
sure they are correct. DamageNDebuff isn't even implemented yet.
2023-07-11 13:52:58 -04:00
gsemaj
9bc7e8de62 Passive nano powers 2023-07-11 13:52:58 -04:00
gsemaj
f249599ab5 YET ANOTHER ITERATION of the new ability system
I am very tired
2023-07-11 13:52:57 -04:00
gsemaj
2901f5f285 Passive nano powers pt 1 2023-07-11 13:52:57 -04:00
gsemaj
36f329c302 Passive nano powers boilerplate 2023-07-11 13:52:57 -04:00
gsemaj
eb7daf8eaa Fix timed out buffs not calling onExpire 2023-07-11 13:52:57 -04:00
gsemaj
0c5a9400ce Buff framework tweaks + polish 2023-07-11 13:52:56 -04:00
gsemaj
f150595f70 Rework buff callbacks
The first implementation was way too complicated and prone to bugs.
This is much more simple flexible; first off, std::function is now used
instead of a raw function pointer, so lambdas and binds are fair game
which is great for scripting. Second, callbacks for all stacks are
executed. It is up to the callback target to ensure correct behavior.
2023-07-11 13:52:56 -04:00
gsemaj
c6528eb2ac oops 2023-07-11 13:52:56 -04:00
gsemaj
d631ca1aa1 CRLF purge in Buffs.cpp 2023-07-11 13:52:56 -04:00
gsemaj
a94fb0ed6d egg prep 2023-07-11 13:52:55 -04:00
gsemaj
d48aa21135 Move Buff implementation to Buffs.cpp 2023-07-11 13:52:55 -04:00
gsemaj
c60c4dac38 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
- ???
2023-07-11 13:52:55 -04:00
gsemaj
215da0130d The great re-#include
Was getting frustrated by the inconsistency in our include statements,
which were causing me problems. As a result, I went through and manually
re-organized every include statement in non-core files.

I'm just gonna copy my rant from Discord:
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE

FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.

the point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues
2023-07-11 13:52:54 -04:00
gsemaj
0e8a4742eb Get rid of player fire rate suspicion
This was super primitive & jank, and caused false positives.
Will replace with a polished system later on.
2023-07-11 13:52:54 -04:00
gsemaj
85bb4d163e Handle case where cmake is invoked outside root 2023-07-11 13:52:54 -04:00
gsemaj
09b74a5711 Start moving passive power processing to playerTick 2023-07-11 13:52:53 -04:00
gsemaj
90819bea8e Groundwork for new buff system 2023-07-11 13:52:53 -04:00
gsemaj
3cc5c09a91 Active power classification 2023-07-11 13:52:53 -04:00
gsemaj
536d5fbcfa some struct reorg 2023-07-11 13:52:53 -04:00
gsemaj
4ec3a3acb7 Replace group filter operator with function 2023-07-11 13:52:52 -04:00
gsemaj
89ed0b99a3 Ignore .bak files
for my local backups lol
2023-07-11 13:52:52 -04:00
gsemaj
db73b85bc8 Refactor player groups
Group structures are used now. Adds more checks in some places but simplifies things overall.
We can expand this system to entities as well now pretty trivially.
2023-07-11 13:52:52 -04:00
gsemaj
6cab203401 (WIP) EXPERIMENTAL GROUP CHANGES 2023-07-11 13:52:52 -04:00
gsemaj
92846e0eac (WIP) TODO ABILITIES 2023-07-11 13:52:51 -04:00
gsemaj
5963ea06be Move mob aggro logic into takeDamage override
God that feels good
2023-07-11 13:52:51 -04:00
gsemaj
c28970f2e1 (WIP) Move away from rigid states/transitions to allow custom behavior 2023-07-11 13:52:51 -04:00
gsemaj
8be853c2dc EntityType -> EntityKind 2023-07-11 13:52:51 -04:00
gsemaj
a811e73fed (WIP) onRoamStart hook implementation 2023-07-11 13:52:50 -04:00
gsemaj
a58971c270 (WIP) Remove BaseNPC::barkerType to save space 2023-07-11 13:52:50 -04:00
gsemaj
07429a0e51 ope 2023-07-11 13:52:50 -04:00
gsemaj
7c9038cf10 (WIP) onCombatStart hook implementation 2023-07-11 13:52:50 -04:00
gsemaj
c1e391d86a (WIP) onDeath hook implementation 2023-07-11 13:52:49 -04:00
gsemaj
e23af08838 (WIP) Add src param to transition + certain hooks
Should all hooks have src? I think not
2023-07-11 13:52:49 -04:00
gsemaj
8a26ae2f01 (WIP) Transitions + hook definitions + onRetreat hook implementation 2023-07-11 13:52:49 -04:00
gsemaj
d17694e12e (WIP) Point 2: Generalization 2023-07-11 13:52:48 -04:00
gsemaj
4b612f35d2 (WIP) Point 1: step functions 2023-07-11 13:52:48 -04:00
gsemaj
d9e0a4a281 (WIP) Start implementing ICombatant
Start by replacing `hitMob` with `takeDamage` interface function.
Simplify `pcAttackChars` a little by utilizing the new interface, then add more interface functions as needed.

A lot of the combat logic is tied to the `Mob` class. Need to start moving stuff over to CombatNPC.
2023-07-11 13:52:48 -04:00
gsemaj
dd9891f668 (WIP) Move ICombatant functions around a bit 2023-07-11 13:52:48 -04:00
gsemaj
962141e54f (WIP) Initial ICombatant draft 2023-07-11 13:52:47 -04:00
bea41132b4 [refactor] Get rid of NPC.hpp
This file was already obsoleted at the start of the refactor, but seems
to have escaped notice until now.
2023-07-11 13:52:47 -04:00
f305d7252d [refactor] Replace a few uses of magic numbers with enums 2023-07-11 13:52:47 -04:00
ff62129eec [refactor] Cosmetic cleanup in Fuse fight functions 2023-07-11 13:52:47 -04:00
55f3ab8bad [refactor] Remove redundant coord args from most entity constructors
Mobs and CombatNPCs still need theirs in order to properly set their
roaming and spawn coords. Assignment of the latter has been moved to the
CombatNPC constructor, where it should have already been.
2023-07-11 13:52:46 -04:00
gsemaj
a732cde117 [WIP] Stub power handler 2023-07-11 13:52:46 -04:00
gsemaj
703eaff2b4 [WIP] Use EntityRef instead of CNSocket in ability handler 2023-07-11 13:52:46 -04:00
gsemaj
f4f5f2e0bd [WIP] Replace appearance data with individual fields
Storing certain things in appearance data and others in their own fields
was gross. Now everything is stored on the same level and functions have
been added to generate appearance data when it's needed by the client.
2023-07-11 13:52:45 -04:00
gsemaj
1858938280 [WIP] Rename Entity.type -> Entity.kind 2023-07-11 13:52:45 -04:00
gsemaj
0af8f7e91d [WIP] Fix Nanos -> Abilities namespace calls 2023-07-11 13:52:45 -04:00
gsemaj
168a85e8ff [WIP] Initial merge of ability namespaces & features 2023-07-11 13:52:45 -04:00
14 changed files with 122 additions and 221 deletions

View File

@@ -1 +0,0 @@
version.h

View File

@@ -26,7 +26,7 @@ Both are pretty short reads and following them will get you up to speed with bra
I will now cover a few examples of the complications people have encountered contributing to this project, how to understand them, overcome them and henceforth avoid them.
### Dirty pull requests
## Dirty pull requests
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
These are generally either:
@@ -44,7 +44,7 @@ If you read the above links, you'll note that this isn't exactly a perfect solut
The obvious issue, then, is that the people submitting dirty PRs are the exact people who don't *know* how to rebase their fork to continue submitting their work cleanly.
So they end up creating countless merge commits when pulling upstream on top of their own incompatible histories, and then submitting those merge commits in their PRs and the cycle continues.
### The details
## The details
A git commit is uniquely identified by its SHA1 hash.
Its hash is generated from its contents, as well as the hash(es) of its parent commit(s).
@@ -52,7 +52,7 @@ Its hash is generated from its contents, as well as the hash(es) of its parent c
That means that even if two commits are exactly the same in terms of content, they're not the same commit if their parent commits differ (ex. if they've been rebased).
So if you keep issuing `git pull`s after upstream has merged a rebased version of your PR, you will never re-synchronize with it, and will instead construct an alternate history polluted by pointless merge commits.
### The solution
## The solution
If you already have a messed-up fork and you have no changes on it that you're afraid to lose, the solution is simple:
@@ -67,7 +67,7 @@ If you do have some committed changes that haven't yet been merged upstream, you
If you do end up messing something up, don't worry, it most likely isn't really lost and `git reflog` is your friend.
(You can checkout an arbitrary commit, and make it into its own branch with `git checkout -b BRANCH` or set a pre-exisitng branch to it with `git reset --hard COMMIT`)
### Avoiding the problem
## Avoiding the problem
When working on a changeset you want to submit back upstream, don't do it on the main branch.
Create a work branch just for your changeset with `git checkout -b work`.
@@ -81,34 +81,3 @@ That way you can always keep master in sync with upstream with `git pull --ff-on
Creating new branches for the rebase isn't strictly necessary since you can always return a branch to its previous state with `git reflog`.
For moving uncommited changes around between branches, `git stash` is a real blessing.
## Code guidelines
Alright, you're up to speed on Git and ready to go. Here are a few specific code guidelines to try and follow:
### Match the styling
Pretty straightforward, make sure your code looks similar to the code around it. Match whitespacing, bracket styling, variable naming conventions, etc.
### Prefer short-circuiting
To minimize branching complexity (as this makes the code hard to read), we prefer to keep the number of `if-else` statements as low as possible. One easy way to achieve this is by doing an early return after branching into an `if` block and then writing the code for the other path outside the block entirely. You can find examples of this in practically every source file. Note that in a few select situations, this might actually make your code less elegant, which is why this isn't a strict rule. Lean towards short-circuiting and use your better judgement.
### Follow the include convention
This one matters a lot as it can cause cyclic dependencies and other code-breaking issues.
FOR HEADER FILES (.hpp):
- everything you use IN THE HEADER must be EXPLICITLY INCLUDED with the exception of things that fall under Core.hpp
- you may NOT include ANYTHING ELSE
FOR SOURCE FILES (.cpp):
- you can #include whatever you want as long as the partner header is included first
- anything that gets included by another include is fair game
- redundant includes are ok because they'll be harmless AS LONG AS our header files stay lean.
The point of this is NOT to optimize the number of includes used all around or make things more efficient necessarily. it's to improve readability & coherence and make it easier to avoid cyclical issues.
## When in doubt, ask
If you still have questions that were not answered here, feel free to ping a dev in the Discord server or on GitHub.

View File

@@ -6,7 +6,6 @@
#include "PlayerManager.hpp"
#include "Buffs.hpp"
#include "Nanos.hpp"
#include "MobAI.hpp"
using namespace Abilities;
@@ -76,20 +75,12 @@ static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombat
}
sSkillResult_Damage_N_Debuff result{};
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
result.iHP = target->getCurrentHP();
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = blocked;
result.iConditionBitFlag = target->getCompositeCondition();
// for player targets, make sure to update Nano stamina
if (target->getCharType() == 1) {
Player *plr = dynamic_cast<Player*>(target);
result.iStamina = plr->getActiveNano()->iStamina;
}
return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result);
}
@@ -120,8 +111,7 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
int duration = skill->durationTime[power];
int strength = skill->values[0][power];
BuffStack passiveBuff = {
// if the duration is 0, it needs to be recast every tick
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
skill->drainType == SkillDrainType::PASSIVE ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
strength, // value
source->getRef(), // source
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
@@ -129,25 +119,21 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
SkillDrainType drainType = skill->drainType;
int combatLifetime = 0;
if(!target->addBuff(timeBuffId,
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
if(buff->id == ECSB_BOUNDINGBALL) {
// drain
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
combatant->takeDamage(buff->getLastSource(), 0); // aggro
}
Buffs::timeBuffUpdate(self, buff, status, stack);
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
Buffs::timeBuffTimeout(self);
},
[combatLifetime](EntityRef self, Buff* buff, time_t currTime) mutable {
if(buff->id == ECSB_BOUNDINGBALL &&
combatLifetime % COMBAT_TICKS_PER_DRAIN_PROC == 0)
Buffs::tickDrain(self, buff, COMBAT_TICKS_PER_DRAIN_PROC); // drain
combatLifetime++;
[](EntityRef self, Buff* buff, time_t currTime) {
if(buff->id == ECSB_BOUNDINGBALL)
Buffs::tickDrain(self, buff); // drain
},
&passiveBuff)) return SkillResult();
&passiveBuff)) return SkillResult(); // no result if already buffed
sSkillResult_Buff result{};
result.eCT = target->getCharType();
@@ -163,24 +149,19 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
Player* plr = dynamic_cast<Player*>(target);
const double scalingFactor = (18 + source->getLevel()) / 36.0;
const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY);
int boostDrain = 0;
int potionDrain = 0;
if(!blocked) {
boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
int boostDrain = (int)(skill->values[0][power] * scalingFactor);
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
plr->batteryW -= boostDrain;
potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain;
}
int potionDrain = (int)(skill->values[1][power] * scalingFactor);
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
plr->batteryN -= potionDrain;
sSkillResult_BatteryDrain result{};
result.eCT = target->getCharType();
result.iID = target->getID();
result.bProtected = blocked;
result.bProtected = target->hasBuff(ECSB_PROTECT_BATTERY);
result.iDrainW = boostDrain;
result.iBatteryW = plr->batteryW;
result.iDrainN = potionDrain;
@@ -227,11 +208,6 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
switch(skill->skillType)
{
case SkillType::CORRUPTIONATTACK:
case SkillType::CORRUPTIONATTACKLOSE:
case SkillType::CORRUPTIONATTACKWIN:
// skillHandler = handleSkillCorruptionReflect;
// break;
case SkillType::DAMAGE:
skillHandler = handleSkillDamage;
break;
@@ -388,12 +364,6 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
pkt->iSkillID = skillID;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = (int32_t)results.size();
if(npc.kind == EntityKind::MOB) {
Mob* mob = dynamic_cast<Mob*>(entity);
pkt->iValue1 = mob->hitX;
pkt->iValue2 = mob->hitY;
pkt->iValue3 = mob->hitZ;
}
attachSkillResults(results, (uint8_t*)(pkt + 1));
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);

View File

@@ -9,18 +9,17 @@
#include <vector>
#include <assert.h>
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
enum class SkillType {
DAMAGE = 1,
HEAL_HP = 2,
KNOCKDOWN = 3, // uses DamageNDebuff
SLEEP = 4, // uses DamageNDebuff
SNARE = 5, // uses DamageNDebuff
KNOCKDOWN = 3, // dnd
SLEEP = 4, // dnd
SNARE = 5, // dnd
HEAL_STAMINA = 6,
STAMINA_SELF = 7,
STUN = 8, // uses DamageNDebuff
STUN = 8, // dnd
WEAPONSLOW = 9,
JUMP = 10,
RUN = 11,

View File

@@ -169,13 +169,12 @@ void Buffs::timeBuffTimeout(EntityRef self) {
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
}
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
void Buffs::tickDrain(EntityRef self, Buff* buff) {
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
return; // not implemented
Entity* entity = self.getEntity();
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
int damage = combatant->getMaxHP() / 100 * mult;
int dealt = combatant->takeDamage(buff->getLastSource(), damage);
int damage = combatant->takeDamage(buff->getLastSource(), combatant->getMaxHP() / 100);
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
@@ -188,7 +187,7 @@ void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
pkt->iTB_ID = ECSB_BOUNDINGBALL;
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
drain->iDamage = dealt;
drain->iDamage = damage;
drain->iHP = combatant->getCurrentHP();
drain->eCT = pkt->eCT;
drain->iID = pkt->iID;

View File

@@ -89,5 +89,5 @@ namespace Buffs {
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
void timeBuffTick(EntityRef self, Buff* buff);
void timeBuffTimeout(EntityRef self);
void tickDrain(EntityRef self, Buff* buff, int mult);
void tickDrain(EntityRef self, Buff* buff);
}

View File

@@ -175,13 +175,10 @@ void Player::step(time_t currTime) {
#pragma endregion
#pragma region CombatNPC
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { /* stubbed */
if(!isAlive())
return false;
if (this->state != AIState::COMBAT && this->state != AIState::ROAMING)
return false;
if(!hasBuff(buffId)) {
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
return true;
@@ -192,7 +189,7 @@ bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, Buff
return false;
}
Buff* CombatNPC::getBuff(int buffId) {
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
if(hasBuff(buffId)) {
return buffs[buffId];
}
@@ -310,19 +307,19 @@ void CombatNPC::step(time_t currTime) {
}
void CombatNPC::transition(AIState newState, EntityRef src) {
state = newState;
state = newState;
if (transitionHandlers.find(newState) != transitionHandlers.end())
transitionHandlers[newState](this, src);
else {
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
transition(AIState::INACTIVE, id);
}
// trigger special NPCEvents, if applicable
/* TODO: fire any triggered events
for (NPCEvent& event : NPCManager::NPCEvents)
if (event.triggerState == newState && event.npcType == type)
event.handler(this);
if (event.trigger == ON_KILLED && event.npcType == type)
event.handler(src, this);
*/
}
#pragma endregion

View File

@@ -26,7 +26,6 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
return;
}
SkillResult result = SkillResult();
SkillData* skill = &Abilities::SkillTable[skillId];
if(skill->drainType == SkillDrainType::PASSIVE) {
// apply buff
@@ -51,57 +50,12 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
// no-op
},
&eggBuff);
sSkillResult_Buff resultBuff{};
resultBuff.eCT = plr->getCharType();
resultBuff.iID = plr->getID();
resultBuff.bProtected = false;
resultBuff.iConditionBitFlag = plr->getCompositeCondition();
result = SkillResult(sizeof(sSkillResult_Buff), &resultBuff);
} else {
int value = plr->getMaxHP() * skill->values[0][0] / 1000;
sSkillResult_Damage resultDamage{};
sSkillResult_Heal_HP resultHeal{};
switch(skill->skillType)
{
case SkillType::DAMAGE:
resultDamage.bProtected = false;
resultDamage.eCT = plr->getCharType();
resultDamage.iID = plr->getID();
resultDamage.iDamage = plr->takeDamage(src, value);
resultDamage.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Damage), &resultDamage);
break;
case SkillType::HEAL_HP:
resultHeal.eCT = plr->getCharType();
resultHeal.iID = plr->getID();
resultHeal.iHealHP = plr->heal(src, value);
resultHeal.iHP = plr->getCurrentHP();
result = SkillResult(sizeof(sSkillResult_Heal_HP), &resultHeal);
break;
default:
std::cout << "[WARN] oops, egg with active skill type " << (int)skill->skillType << " unhandled";
return;
}
}
// initialize response struct
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
memset(respbuf, 0, resplen);
sP_FE2CL_NPC_SKILL_HIT* pkt = (sP_FE2CL_NPC_SKILL_HIT*)respbuf;
pkt->iNPC_ID = eggId;
pkt->iSkillID = skillId;
pkt->eST = (int32_t)skill->skillType;
pkt->iTargetCnt = 1;
if(result.size > 0) {
void* attached = (void*)(pkt + 1);
memcpy(attached, result.payload, result.size);
}
NPCManager::sendToViewable(src.getEntity(), pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
// use skill
std::vector<ICombatant*> targets;
targets.push_back(dynamic_cast<ICombatant*>(plr));
Abilities::useNPCSkill(src, skillId, targets);
}
static void eggStep(CNServer* serv, time_t currTime) {

View File

@@ -106,11 +106,11 @@ struct CombatNPC : public BaseNPC, public ICombatant {
std::unordered_map<int, Buff*> buffs = {};
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
this->spawnX = spawnX;
this->spawnY = spawnY;
this->spawnZ = spawnZ;
spawnX = x;
spawnY = y;
spawnZ = z;
kind = EntityKind::COMBAT_NPC;

View File

@@ -227,7 +227,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
return false;
}
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int mobStyle) {
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) {
Player *plr = PlayerManager::getPlayer(mob->target);
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
@@ -246,7 +246,7 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
resp->iNPC_ID = mob->id;
resp->iSkillID = skillID;
resp->iStyle = mobStyle;
resp->iStyle = style;
resp->iValue1 = plr->x;
resp->iValue2 = plr->y;
resp->iValue3 = plr->z;
@@ -280,38 +280,26 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
respdata[i].iActiveNanoSlotNum = n;
respdata[i].iNanoID = plr->activeNano;
int nanoStyle = Nanos::nanoStyle(plr->activeNano);
if (nanoStyle == -1) { // no nano
int style2 = Nanos::nanoStyle(plr->activeNano);
if (style2 == -1) { // no nano
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
} else if (mobStyle == nanoStyle) {
} else if (style == style2) {
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
} else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 2) {
} else if (style - style2 == 1 || style2 - style == 2) {
respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
respdata[i].iDamage = 0;
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
if (plr->Nanos[plr->activeNano].iStamina > 150)
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
// fire damage power disguised as a corruption attack back at the enemy
SkillData skill = {
SkillType::DAMAGE, // skillType
SkillEffectTarget::POINT, // effectTarget
1, // effectType
SkillTargetType::MOBS, // targetType
SkillDrainType::ACTIVE, // drainType
0, // effectArea
{0, 0, 0, 0}, // batteryUse
{0, 0, 0, 0}, // durationTime
{0, 0, 0}, // valueTypes (unused)
{
{200, 200, 200, 200},
{200, 200, 200, 200},
{200, 200, 200, 200},
}
};
Abilities::useNanoSkill(sock, &skill, *plr->getActiveNano(), { mob });
// TODO ABILITIES
/*std::vector<int> targetData2 = {1, mob->id, 0, 0, 0};
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == EST_DAMAGE)
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);*/
} else {
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
@@ -338,6 +326,12 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
}
static void useAbilities(Mob *mob, time_t currTime) {
/*
* targetData approach
* first integer is the count
* second to fifth integers are IDs, these can be either player iID or mob's iID
* whether the skill targets players or mobs is determined by the skill packet being fired
*/
Player *plr = PlayerManager::getPlayer(mob->target);
if (mob->skillStyle >= 0) { // corruption hit
@@ -352,7 +346,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (mob->skillStyle == -2) { // eruption hit
int skillID = (int)mob->data["m_iMegaType"];
std::vector<ICombatant*> targets{};
std::vector<int> targetData = {0, 0, 0, 0, 0};
// find the players within range of eruption
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
@@ -362,22 +356,26 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (ref.kind != EntityKind::PLAYER)
continue;
CNSocket *s = ref.sock;
CNSocket *s= ref.sock;
Player *plr = PlayerManager::getPlayer(s);
if (!plr->isAlive())
if (plr->HP <= 0)
continue;
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
if (distance < Abilities::SkillTable[skillID].effectArea) {
targets.push_back(plr);
if (targets.size() > 3) // make sure not to have more than 4
targetData[0] += 1;
targetData[targetData[0]] = plr->iID;
if (targetData[0] > 3) // make sure not to have more than 4
break;
}
}
}
Abilities::useNPCSkill(mob->id, skillID, targets);
// TODO ABILITIES
/*for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
mob->skillStyle = -3; // eruption cooldown
mob->nextAttack = currTime + 1000;
return;
@@ -395,11 +393,14 @@ static void useAbilities(Mob *mob, time_t currTime) {
if (random < prob1) { // active skill hit
int skillID = (int)mob->data["m_iActiveSkill1"];
SkillData* skill = &Abilities::SkillTable[skillID];
int debuffID = Abilities::getCSTBFromST(skill->skillType);
if(plr->hasBuff(debuffID))
return; // prevent debuffing a player twice
Abilities::useNPCSkill(mob->getRef(), skillID, { plr });
// TODO ABILITIES
//std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
//for (auto& pwr : Abilities::Powers)
// if (pwr.skillType == Abilities::SkillTable[skillID].skillType) {
// if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
// return; // prevent debuffing a player twice
// pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
// }
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
return;
}
@@ -753,8 +754,11 @@ void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
self->nextAttack = 0;
// cast a return home heal spell, this is the right way(tm)
Abilities::useNPCSkill(npc->getRef(), 110, { npc });
// TODO ABILITIES
/*std::vector<int> targetData = { 1, 0, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[110].skillType)
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);*/
// clear outlying debuffs
clearDebuff(self);
}
@@ -771,9 +775,12 @@ void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
self->roamY = self->y;
self->roamZ = self->z;
int skillID = (int)self->data["m_iPassiveBuff"];
if(skillID != 0) // cast passive
Abilities::useNPCSkill(npc->getRef(), skillID, { npc });
int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive
// TODO ABILITIES
/*std::vector<int> targetData = { 1, self->id, 0, 0, 0 };
for (auto& pwr : Abilities::Powers)
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);*/
}
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {

View File

@@ -50,8 +50,8 @@ struct Mob : public CombatNPC {
// temporary; until we're sure what's what
nlohmann::json data = {};
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
sightRange(d["m_iSightRange"]) {
state = AIState::ROAMING;
@@ -62,9 +62,9 @@ struct Mob : public CombatNPC {
idleRange = (int)data["m_iIdleRange"];
level = data["m_iNpcLevel"];
roamX = spawnX;
roamY = spawnY;
roamZ = spawnZ;
roamX = x;
roamY = y;
roamZ = z;
offsetX = 0;
offsetY = 0;

View File

@@ -122,15 +122,16 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
}
// type must already be checked and updateNPCPosition() must be called on the result
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, uint64_t instance, int type, bool respawn, bool baseInstance) {
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
//assert(nextId < INT32_MAX);
int id = nextId--;
int team = NPCData[type]["m_iTeam"];
BaseNPC *npc = nullptr;
if (team == 2) {
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
npc = new Mob(x, y, z, inst, type, NPCData[type], id);
// re-enable respawning, if desired
((Mob*)npc)->summoned = !respawn;
@@ -293,55 +294,57 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
return npc;
}
// TODO: Move this to separate file in ai/ subdir when implementing more events
// TODO: Move this to MobAI, possibly
#pragma region NPCEvents
// summon right arm and stage 2 body
static void lordFuseStageTwo(CombatNPC *npc) {
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc; // adaptium, stun
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage two" << std::endl;
// Fuse doesn't move
// Blastons, Heal
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2467);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
oldbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
// right arm, Adaptium, Stun
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
oldbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
}
// summon left arm and stage 3 body
static void lordFuseStageThree(CombatNPC *npc) {
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
Mob *oldbody = (Mob*)npc;
Player *plr = PlayerManager::getPlayer(sock);
std::cout << "Lord Fuse stage three" << std::endl;
// Cosmix, Damage Point
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, plr->instanceID, 2468);
newbody->angle = oldbody->angle;
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
newbody->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
plr->instanceID, oldbody->angle);
// Blastons, Heal
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470);
arm->angle = oldbody->angle;
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
arm->instanceID, oldbody->angle);
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
plr->instanceID, oldbody->angle);
}
std::vector<NPCEvent> NPCManager::NPCEvents = {
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
};
#pragma endregion NPCEvents

View File

@@ -14,15 +14,20 @@
#define RESURRECT_HEIGHT 400
typedef void (*NPCEventHandler)(CombatNPC*);
enum Trigger {
ON_KILLED,
ON_COMBAT
};
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
struct NPCEvent {
int32_t npcType;
AIState triggerState;
int trigger;
NPCEventHandler handler;
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
: npcType(t), triggerState(tr), handler(hndlr) {}
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
: npcType(t), trigger(tr), handler(hndlr) {}
};
namespace NPCManager {

View File

@@ -416,7 +416,6 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
default: // plain respawn
plr->HP = PC_MAXHEALTH(plr->level) / 2;
plr->clearBuffs(false);
// fallthrough
case ePCRegenType::Unstick: // warp away
move = true;