mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-10-16 18:40:16 +00:00
Compare commits
44 Commits
2249b5381b
...
1.5.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cd265af8e0 | ||
![]() |
38c68f351b | ||
edfbe4d005 | |||
96c430c994 | |||
![]() |
4592fc42af | ||
![]() |
70a27afad1 | ||
![]() |
6cfb3bf532 | ||
9b2a65f8fd | |||
6a69388822 | |||
2924a27eb4 | |||
ba20f5a401 | |||
![]() |
eb88fa05cb | ||
![]() |
0b73cef187 | ||
![]() |
7af39b3d04 | ||
![]() |
33206b1207 | ||
![]() |
e325f7a40b | ||
![]() |
82bee2051a | ||
![]() |
4ece1bb89b | ||
![]() |
31677e2638 | ||
![]() |
d32827b692 | ||
![]() |
13c009b448 | ||
![]() |
a032497bed | ||
![]() |
3b6b61d087 | ||
![]() |
6d760f5bce | ||
![]() |
2a622f901c | ||
![]() |
03d28bf4e4 | ||
![]() |
4b834579c5 | ||
![]() |
07fe8ca367 | ||
![]() |
2f3f8a3951 | ||
4f890a9c07 | |||
8517e0c7de | |||
5fb0cbbcf7 | |||
55e9f6531d | |||
![]() |
7726357fbe | ||
![]() |
564c275d51 | ||
![]() |
3ce9ae5f77 | ||
![]() |
7c5b9a8105 | ||
![]() |
258ff35e20 | ||
![]() |
ab480d88f1 | ||
![]() |
89772d763b | ||
bd0cc3c212 | |||
c636c538eb | |||
d3bef95a7f | |||
![]() |
650f947451 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
version.h
|
7
.github/workflows/check-builds.yaml
vendored
7
.github/workflows/check-builds.yaml
vendored
@@ -9,12 +9,13 @@ on:
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
pull_request:
|
||||
types: ready_for_review
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- src/**
|
||||
- vendor/**
|
||||
- CMakeLists.txt
|
||||
- Makefile
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ubuntu-build:
|
||||
@@ -53,7 +54,7 @@ jobs:
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||
path: bin
|
||||
|
||||
windows-build:
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
|
||||
copy-artifacts:
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [windows-build, ubuntu-build]
|
||||
env:
|
||||
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
|
||||
|
@@ -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,3 +81,34 @@ 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.
|
||||
|
39
README.md
39
README.md
@@ -13,13 +13,13 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
|
||||
|
||||
### Getting Started
|
||||
#### Method A: Installer (Easiest)
|
||||
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4-Installer.exe) - choose to run the file.
|
||||
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5-Installer.exe) - choose to run the file.
|
||||
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
|
||||
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
|
||||
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
|
||||
|
||||
#### Method B: Standalone .zip file
|
||||
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.4/OpenFusionClient-1.4.zip).
|
||||
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5/OpenFusionClient-1.5.zip).
|
||||
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
|
||||
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
|
||||
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
|
||||
@@ -28,8 +28,7 @@ OpenFusion is a reverse-engineered server for FusionFall. It primarily targets v
|
||||
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
|
||||
|
||||
### Hosting a server
|
||||
|
||||
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
|
||||
1. Grab `OpenFusionServer-1.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5).
|
||||
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
|
||||
3. Add a new server to the client's list:
|
||||
1. For Description, enter anything you want. This is what will show up in the server list.
|
||||
@@ -54,10 +53,7 @@ FusionFall consists of the following components:
|
||||
|
||||
The original game made use of the player's actual web browser to launch the game, but since then the NPAPI plugin interface the game relied on has been deprecated and is no longer available in most modern browsers. Both Retro and OpenFusion get around this issue by distributing an older version of Electron, a software package that is essentially a specialized web browser.
|
||||
|
||||
The browser/Electron client opens a web page with an `<embed>` tag of MIME type `application/vnd.unity`, where the `src` param is the address of the game's `.unity3d` entrypoint.
|
||||
|
||||
This triggers the browser to load an NPAPI plugin that handles this MIME type, the Unity Web Player, which the browser looks for in `C:\Users\%USERNAME%\AppData\LocalLow\Unity\WebPlayer`.
|
||||
The Web Player was previously copied there by `installUnity.bat`.
|
||||
The browser/Electron client opens a web page with an `<embed>` tag of the appropriate MIME type, where the `src` param is the address of the game's `.unity3d` entrypoint. This triggers the browser to load an NPAPI plugin that handles said MIME type, in this case the Unity Web Player.
|
||||
|
||||
Note that the version of the web player distributed with OpenFusion expects a standard `UnityWeb` magic number for all assets, instead of Retro's modified `streamed` magic number.
|
||||
This will potentially become relevant later, as people start experimenting and mixing and matching versions.
|
||||
@@ -66,7 +62,7 @@ The web player will execute the game code, which will request the following file
|
||||
|
||||
`/assetInfo.php` contains the address from which to fetch the rest of the game's assets (the "dongresources").
|
||||
Normally those would be hosted on the same web server as the gateway, but the OpenFusion distribution (in it's default configuration) doesn't use a web server at all!
|
||||
It loads the web pages locally using the `file://` schema, and fetches the game's assets from Turner's CDN (which is still hosting them to this day!).
|
||||
It instead loads the web pages locally using the `file://` schema, and fetches the game's assets from a standard web server.
|
||||
|
||||
`/loginInfo.php` contains the IP:port pair of the FusionFall login server, which the client will connect to. This login server drives the client while it's in the Character Selection menu, as well as Character Creation and the Tutorial.
|
||||
|
||||
@@ -102,26 +98,13 @@ If you'd like to contribute to this project, please read [CONTRIBUTING.md](CONTR
|
||||
## Gameplay
|
||||
|
||||
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
|
||||
The server is not yet complete, however, and some functionality is still missing.
|
||||
While most features are implemented and the game is playable start to finish, there may be missing functionality or bugs present.
|
||||
|
||||
Because the server is still in development, ordinary players are allowed access to a few admin commands:
|
||||
Depending on the server configuration, you'll have access to certain commands.
|
||||
|
||||

|
||||
For the public servers: Original has item spawning, the ability to set player speed/jump height, and teleportation enabled (default account level 50).
|
||||
Meanwhile the Academy server is more meant for legitimate playthroughs (default account level 99).
|
||||
|
||||
### Movement commands
|
||||
* A `/speed` of around 2400 or 3000 is nice.
|
||||
* A `/jump` of about 50 will send you soaring
|
||||
* [This map](res/dong_number_map.png) (credit to Danny O) is useful for `/warp` coordinates.
|
||||
* `/goto` is useful for more precise teleportation (ie. for getting into Infected Zones, etc.).
|
||||
When hosting a local server, you will have access to all commands by default (account level 1).
|
||||
|
||||
### Item commands
|
||||
* `/itemN [type] [itemId] [amount]`
|
||||
(Refer to the [item list](https://docs.google.com/spreadsheets/d/1mpoJ9iTHl_xLI4wQ_9UvIDYNcsDYscdkyaGizs43TCg/))
|
||||
|
||||
### Nano commands
|
||||
* `/nano [id] (1-36)`
|
||||
* `/nano_equip [id] (1-36) [slot] (0-2)`
|
||||
* `/nano_unequip [slot] (0-2)`
|
||||
* `/nano_active [slot] (0-2)`
|
||||
|
||||
### A full list of commands can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||
|
@@ -1,11 +1,12 @@
|
||||
#include "Abilities.hpp"
|
||||
|
||||
#include "servers/CNShardServer.hpp"
|
||||
|
||||
#include "NPCManager.hpp"
|
||||
#include "PlayerManager.hpp"
|
||||
#include "Buffs.hpp"
|
||||
#include "Nanos.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
#include "MobAI.hpp"
|
||||
|
||||
using namespace Abilities;
|
||||
|
||||
@@ -47,32 +48,106 @@ static SkillResult handleSkillHealHP(SkillData* skill, int power, ICombatant* so
|
||||
}
|
||||
|
||||
static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
// TODO abilities
|
||||
// take aggro
|
||||
target->takeDamage(source->getRef(), 0);
|
||||
|
||||
int duration = 0;
|
||||
int strength = 0;
|
||||
bool blocked = target->hasBuff(ECSB_FREEDOM);
|
||||
if(!blocked) {
|
||||
duration = skill->durationTime[power];
|
||||
strength = skill->values[0][power];
|
||||
BuffStack debuff = {
|
||||
(duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
||||
strength, // value
|
||||
source->getRef(), // source
|
||||
BuffClass::NANO, // buff class
|
||||
};
|
||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||
target->addBuff(timeBuffId,
|
||||
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
if(status == ETBU_DEL) Buffs::timeBuffTimeout(self);
|
||||
},
|
||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
||||
Buffs::timeBuffTick(self, buff);
|
||||
},
|
||||
&debuff);
|
||||
}
|
||||
|
||||
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 = false;
|
||||
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);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillLeech(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
EntityRef sourceRef = source->getRef();
|
||||
int heal = skill->values[0][power];
|
||||
int healed = source->heal(sourceRef, heal);
|
||||
int damage = heal * 2;
|
||||
int dealt = target->takeDamage(sourceRef, damage);
|
||||
|
||||
sSkillResult_Leech result{};
|
||||
|
||||
result.Damage.eCT = target->getCharType();
|
||||
result.Damage.iID = target->getID();
|
||||
result.Damage.bProtected = dealt <= 0;
|
||||
result.Damage.iDamage = dealt;
|
||||
result.Damage.iHP = target->getCurrentHP();
|
||||
|
||||
result.Heal.eCT = result.Damage.eCT;
|
||||
result.Heal.iID = result.Damage.iID;
|
||||
result.Heal.iHealHP = healed;
|
||||
result.Heal.iHP = source->getCurrentHP();
|
||||
|
||||
return SkillResult(sizeof(sSkillResult_Leech), &result);
|
||||
}
|
||||
|
||||
static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* source, ICombatant* target) {
|
||||
int duration = skill->durationTime[power];
|
||||
int strength = skill->values[0][power];
|
||||
BuffStack passiveBuff = {
|
||||
skill->drainType == SkillDrainType::PASSIVE ? 1 : skill->durationTime[power], // ticks
|
||||
skill->values[0][power], // value
|
||||
// if the duration is 0, it needs to be recast every tick
|
||||
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
||||
strength, // value
|
||||
source->getRef(), // source
|
||||
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
|
||||
};
|
||||
|
||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||
SkillDrainType drainType = skill->drainType;
|
||||
int combatLifetime = 0;
|
||||
if(!target->addBuff(timeBuffId,
|
||||
[](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||
},
|
||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
||||
// no-op
|
||||
},
|
||||
&passiveBuff)) return SkillResult(); // no result if already buffed
|
||||
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
|
||||
// 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++;
|
||||
},
|
||||
&passiveBuff)) return SkillResult();
|
||||
|
||||
sSkillResult_Buff result{};
|
||||
result.eCT = target->getCharType();
|
||||
@@ -88,19 +163,24 @@ 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 = (int)(skill->values[0][power] * scalingFactor);
|
||||
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
|
||||
plr->batteryW -= boostDrain;
|
||||
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 potionDrain = (int)(skill->values[1][power] * scalingFactor);
|
||||
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
|
||||
plr->batteryN -= potionDrain;
|
||||
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 = target->hasBuff(ECSB_PROTECT_BATTERY);
|
||||
result.bProtected = blocked;
|
||||
result.iDrainW = boostDrain;
|
||||
result.iBatteryW = plr->batteryW;
|
||||
result.iDrainN = potionDrain;
|
||||
@@ -142,54 +222,64 @@ static SkillResult handleSkillResurrect(SkillData* skill, int power, ICombatant*
|
||||
#pragma endregion
|
||||
|
||||
static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombatant* src, std::vector<ICombatant*> targets) {
|
||||
size_t resultSize = 0;
|
||||
SkillResult (*skillHandler)(SkillData*, int, ICombatant*, ICombatant*) = nullptr;
|
||||
std::vector<SkillResult> results;
|
||||
|
||||
switch(skill->skillType)
|
||||
{
|
||||
case SkillType::CORRUPTIONATTACK:
|
||||
case SkillType::CORRUPTIONATTACKLOSE:
|
||||
case SkillType::CORRUPTIONATTACKWIN:
|
||||
// skillHandler = handleSkillCorruptionReflect;
|
||||
// break;
|
||||
case SkillType::DAMAGE:
|
||||
resultSize = sizeof(sSkillResult_Damage);
|
||||
skillHandler = handleSkillDamage;
|
||||
break;
|
||||
case SkillType::HEAL_HP:
|
||||
case SkillType::RETURNHOMEHEAL:
|
||||
resultSize = sizeof(sSkillResult_Heal_HP);
|
||||
skillHandler = handleSkillHealHP;
|
||||
break;
|
||||
case SkillType::KNOCKDOWN:
|
||||
case SkillType::SLEEP:
|
||||
case SkillType::SNARE:
|
||||
case SkillType::STUN:
|
||||
skillHandler = handleSkillDamageNDebuff;
|
||||
break;
|
||||
case SkillType::JUMP:
|
||||
case SkillType::RUN:
|
||||
case SkillType::FREEDOM:
|
||||
case SkillType::PHOENIX:
|
||||
case SkillType::INVULNERABLE:
|
||||
case SkillType::STEALTH:
|
||||
case SkillType::MINIMAPENEMY:
|
||||
case SkillType::MINIMAPTRESURE:
|
||||
case SkillType::NANOSTIMPAK:
|
||||
case SkillType::PHOENIX:
|
||||
case SkillType::PROTECTBATTERY:
|
||||
case SkillType::PROTECTINFECTION:
|
||||
case SkillType::REWARDBLOB:
|
||||
case SkillType::REWARDCASH:
|
||||
// case SkillType::INFECTIONDAMAGE:
|
||||
case SkillType::FREEDOM:
|
||||
case SkillType::BOUNDINGBALL:
|
||||
case SkillType::INVULNERABLE:
|
||||
case SkillType::STAMINA_SELF:
|
||||
case SkillType::STEALTH:
|
||||
resultSize = sizeof(sSkillResult_Buff);
|
||||
case SkillType::NANOSTIMPAK:
|
||||
case SkillType::BUFFHEAL:
|
||||
skillHandler = handleSkillBuff;
|
||||
break;
|
||||
case SkillType::BATTERYDRAIN:
|
||||
resultSize = sizeof(sSkillResult_BatteryDrain);
|
||||
skillHandler = handleSkillBatteryDrain;
|
||||
break;
|
||||
case SkillType::RECALL: // still soft lock
|
||||
case SkillType::RECALL_GROUP: // works for player who uses it
|
||||
resultSize = sizeof(sSkillResult_Move);
|
||||
skillHandler = handleSkillMove;
|
||||
break;
|
||||
case SkillType::PHOENIX_GROUP: // broken
|
||||
resultSize = sizeof(sSkillResult_Resurrect);
|
||||
skillHandler = handleSkillResurrect;
|
||||
case SkillType::BLOODSUCKING:
|
||||
skillHandler = handleSkillLeech;
|
||||
break;
|
||||
case SkillType::RETROROCKET_SELF:
|
||||
// no-op
|
||||
return results;
|
||||
case SkillType::PHOENIX_GROUP:
|
||||
skillHandler = handleSkillResurrect;
|
||||
break;
|
||||
case SkillType::RECALL:
|
||||
case SkillType::RECALL_GROUP:
|
||||
skillHandler = handleSkillMove;
|
||||
break;
|
||||
case SkillType::BATTERYDRAIN:
|
||||
skillHandler = handleSkillBatteryDrain;
|
||||
break;
|
||||
default:
|
||||
std::cout << "[WARN] Unhandled skill type " << (int)skill->skillType << std::endl;
|
||||
return results;
|
||||
@@ -199,7 +289,7 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
|
||||
assert(target != nullptr);
|
||||
SkillResult result = skillHandler(skill, power, src != nullptr ? src : target, target);
|
||||
if(result.size == 0) continue; // skill not applicable
|
||||
if(result.size != resultSize) {
|
||||
if(result.size > MAX_SKILLRESULT_SIZE) {
|
||||
std::cout << "[WARN] bad skill result size for " << (int)skill->skillType << " from " << (void*)handleSkillBuff << std::endl;
|
||||
continue;
|
||||
}
|
||||
@@ -208,16 +298,18 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
|
||||
return results;
|
||||
}
|
||||
|
||||
static void attachSkillResults(std::vector<SkillResult> results, size_t resultSize, uint8_t* pivot) {
|
||||
static void attachSkillResults(std::vector<SkillResult> results, uint8_t* pivot) {
|
||||
for(SkillResult& result : results) {
|
||||
memcpy(pivot, result.payload, resultSize);
|
||||
pivot += resultSize;
|
||||
size_t sz = result.size;
|
||||
memcpy(pivot, result.payload, sz);
|
||||
pivot += sz;
|
||||
}
|
||||
}
|
||||
|
||||
void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std::vector<ICombatant*> affected) {
|
||||
|
||||
Player* plr = PlayerManager::getPlayer(sock);
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(plr);
|
||||
|
||||
int boost = 0;
|
||||
if (Nanos::getNanoBoost(plr))
|
||||
@@ -229,17 +321,19 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
|
||||
nano.iStamina = 0;
|
||||
}
|
||||
|
||||
std::vector<SkillResult> results = handleSkill(skill, boost, plr, affected);
|
||||
std::vector<SkillResult> results = handleSkill(skill, boost, combatant, affected);
|
||||
if(results.empty()) return; // no effect; no need for confirmation packets
|
||||
|
||||
size_t resultSize = results.back().size; // guaranteed to be the same for every item
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), resultSize)) {
|
||||
// lazy validation since skill results might be different sizes
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC), results.size(), MAX_SKILLRESULT_SIZE)) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NANO_SKILL_USE_SUCC packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC) + results.size() * resultSize;
|
||||
size_t resplen = sizeof(sP_FE2CL_NANO_SKILL_USE_SUCC);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
@@ -252,7 +346,7 @@ void Abilities::useNanoSkill(CNSocket* sock, SkillData* skill, sNano& nano, std:
|
||||
pkt->eST = (int32_t)skill->skillType;
|
||||
pkt->iTargetCnt = (int32_t)results.size();
|
||||
|
||||
attachSkillResults(results, resultSize, (uint8_t*)(pkt + 1));
|
||||
attachSkillResults(results, (uint8_t*)(pkt + 1));
|
||||
sock->sendPacket(pkt, P_FE2CL_NANO_SKILL_USE_SUCC, resplen);
|
||||
|
||||
if(skill->skillType == SkillType::RECALL_GROUP)
|
||||
@@ -276,15 +370,16 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
|
||||
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
|
||||
if(results.empty()) return; // no effect; no need for confirmation packets
|
||||
|
||||
size_t resultSize = results.back().size; // guaranteed to be the same for every item
|
||||
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), resultSize)) {
|
||||
// lazy validation since skill results might be different sizes
|
||||
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {
|
||||
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_HIT packet size\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize response struct
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + results.size() * resultSize;
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT);
|
||||
for(SkillResult& sr : results)
|
||||
resplen += sr.size;
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
@@ -293,8 +388,14 @@ 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, resultSize, (uint8_t*)(pkt + 1));
|
||||
attachSkillResults(results, (uint8_t*)(pkt + 1));
|
||||
NPCManager::sendToViewable(entity, pkt, P_FE2CL_NPC_SKILL_HIT, resplen);
|
||||
}
|
||||
|
||||
@@ -369,12 +470,6 @@ int Abilities::getCSTBFromST(SkillType skillType) {
|
||||
case SkillType::PROTECTINFECTION:
|
||||
result = ECSB_PROTECT_INFECTION;
|
||||
break;
|
||||
case SkillType::SNARE:
|
||||
result = ECSB_DN_MOVE_SPEED;
|
||||
break;
|
||||
case SkillType::SLEEP:
|
||||
result = ECSB_MEZ;
|
||||
break;
|
||||
case SkillType::MINIMAPENEMY:
|
||||
result = ECSB_MINIMAP_ENEMY;
|
||||
break;
|
||||
@@ -387,15 +482,9 @@ int Abilities::getCSTBFromST(SkillType skillType) {
|
||||
case SkillType::REWARDCASH:
|
||||
result = ECSB_REWARD_CASH;
|
||||
break;
|
||||
case SkillType::INFECTIONDAMAGE:
|
||||
result = ECSB_INFECTION;
|
||||
break;
|
||||
case SkillType::FREEDOM:
|
||||
result = ECSB_FREEDOM;
|
||||
break;
|
||||
case SkillType::BOUNDINGBALL:
|
||||
result = ECSB_BOUNDINGBALL;
|
||||
break;
|
||||
case SkillType::INVULNERABLE:
|
||||
result = ECSB_INVULNERABLE;
|
||||
break;
|
||||
@@ -404,6 +493,22 @@ int Abilities::getCSTBFromST(SkillType skillType) {
|
||||
break;
|
||||
case SkillType::NANOSTIMPAK:
|
||||
result = ECSB_STIMPAKSLOT1;
|
||||
// shift as necessary
|
||||
break;
|
||||
case SkillType::SNARE:
|
||||
result = ECSB_DN_MOVE_SPEED;
|
||||
break;
|
||||
case SkillType::STUN:
|
||||
result = ECSB_STUN;
|
||||
break;
|
||||
case SkillType::SLEEP:
|
||||
result = ECSB_MEZ;
|
||||
break;
|
||||
case SkillType::INFECTIONDAMAGE:
|
||||
result = ECSB_INFECTION;
|
||||
break;
|
||||
case SkillType::BOUNDINGBALL:
|
||||
result = ECSB_BOUNDINGBALL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@@ -7,18 +7,20 @@
|
||||
|
||||
#include <map>
|
||||
#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,
|
||||
SLEEP = 4,
|
||||
SNARE = 5,
|
||||
KNOCKDOWN = 3, // uses DamageNDebuff
|
||||
SLEEP = 4, // uses DamageNDebuff
|
||||
SNARE = 5, // uses DamageNDebuff
|
||||
HEAL_STAMINA = 6,
|
||||
STAMINA_SELF = 7,
|
||||
STUN = 8,
|
||||
STUN = 8, // uses DamageNDebuff
|
||||
WEAPONSLOW = 9,
|
||||
JUMP = 10,
|
||||
RUN = 11,
|
||||
@@ -75,6 +77,7 @@ struct SkillResult {
|
||||
size_t size;
|
||||
uint8_t payload[MAX_SKILLRESULT_SIZE];
|
||||
SkillResult(size_t len, void* dat) {
|
||||
assert(len <= MAX_SKILLRESULT_SIZE);
|
||||
size = len;
|
||||
memcpy(payload, dat, len);
|
||||
}
|
||||
|
@@ -95,6 +95,12 @@ int Buff::getValue(BuffValueSelector selector) {
|
||||
return value;
|
||||
}
|
||||
|
||||
EntityRef Buff::getLastSource() {
|
||||
if(stacks.empty())
|
||||
return self;
|
||||
return stacks.back().source;
|
||||
}
|
||||
|
||||
bool Buff::isStale() {
|
||||
return stacks.empty();
|
||||
}
|
||||
@@ -137,15 +143,56 @@ void Buffs::timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* st
|
||||
self.sock->sendPacket((void*)&pkt, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
||||
}
|
||||
|
||||
void Buffs::timeBuffTick(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);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK, pkt);
|
||||
pkt.eCT = combatant->getCharType();
|
||||
pkt.iID = combatant->getID();
|
||||
pkt.iTB_ID = buff->id;
|
||||
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
}
|
||||
|
||||
void Buffs::timeBuffTimeout(EntityRef self) {
|
||||
if(self.kind != EntityKind::PLAYER && self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
||||
return; // not a combatant
|
||||
Entity* entity = self.getEntity();
|
||||
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt); // send a buff timeout to other players
|
||||
pkt.eCT = combatant->getCharType();
|
||||
int32_t eCharType = combatant->getCharType();
|
||||
pkt.eCT = eCharType == 4 ? 2 : eCharType; // convention not followed by client here
|
||||
pkt.iID = combatant->getID();
|
||||
pkt.iConditionBitFlag = combatant->getCompositeCondition();
|
||||
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) {
|
||||
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);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
pkt->iID = self.id;
|
||||
pkt->eCT = combatant->getCharType();
|
||||
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
||||
|
||||
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
drain->iDamage = dealt;
|
||||
drain->iHP = combatant->getCurrentHP();
|
||||
drain->eCT = pkt->eCT;
|
||||
drain->iID = pkt->iID;
|
||||
|
||||
NPCManager::sendToViewable(self.getEntity(), (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
}
|
||||
#pragma endregion
|
||||
|
@@ -66,6 +66,7 @@ public:
|
||||
BuffClass maxClass();
|
||||
|
||||
int getValue(BuffValueSelector selector);
|
||||
EntityRef getLastSource();
|
||||
|
||||
/*
|
||||
* In general, a Buff object won't exist
|
||||
@@ -86,5 +87,7 @@ public:
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -72,31 +72,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
|
||||
|
||||
// Handle serverside value-changes
|
||||
switch (setData->iSetValueType) {
|
||||
case 1:
|
||||
plr->HP = setData->iSetValue;
|
||||
case CN_GM_SET_VALUE_TYPE__HP:
|
||||
response.iSetValue = plr->HP = setData->iSetValue;
|
||||
break;
|
||||
case 2:
|
||||
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
|
||||
plr->batteryW = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryW > 9999)
|
||||
plr->batteryW = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryW;
|
||||
break;
|
||||
case 3:
|
||||
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
|
||||
plr->batteryN = setData->iSetValue;
|
||||
|
||||
// caps
|
||||
if (plr->batteryN > 9999)
|
||||
plr->batteryN = 9999;
|
||||
|
||||
response.iSetValue = plr->batteryN;
|
||||
break;
|
||||
case 4:
|
||||
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
|
||||
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
||||
response.iSetValue = plr->fusionmatter;
|
||||
break;
|
||||
case 5:
|
||||
plr->money = setData->iSetValue;
|
||||
case CN_GM_SET_VALUE_TYPE__CANDY:
|
||||
response.iSetValue = plr->money = setData->iSetValue;
|
||||
break;
|
||||
case CN_GM_SET_VALUE_TYPE__SPEED:
|
||||
case CN_GM_SET_VALUE_TYPE__JUMP:
|
||||
response.iSetValue = setData->iSetValue;
|
||||
break;
|
||||
}
|
||||
|
||||
response.iPC_ID = setData->iPC_ID;
|
||||
response.iSetValue = setData->iSetValue;
|
||||
response.iSetValueType = setData->iSetValueType;
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
|
||||
|
112
src/Combat.cpp
112
src/Combat.cpp
@@ -21,13 +21,14 @@ std::map<int32_t, std::map<int8_t, Bullet>> Combat::Bullets;
|
||||
|
||||
#pragma region Player
|
||||
bool Player::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
|
||||
EntityRef self = PlayerManager::getSockFromID(iID);
|
||||
if(!isAlive())
|
||||
return false;
|
||||
|
||||
if(!hasBuff(buffId)) {
|
||||
buffs[buffId] = new Buff(buffId, self, onUpdate, onTick, stack);
|
||||
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
buffs[buffId]->updateCallbacks(onUpdate, onTick);
|
||||
buffs[buffId]->addStack(stack);
|
||||
return false;
|
||||
@@ -48,9 +49,10 @@ void Player::removeBuff(int buffId) {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::removeBuff(int buffId, int buffClass) {
|
||||
void Player::removeBuff(int buffId, BuffClass buffClass) {
|
||||
if(hasBuff(buffId)) {
|
||||
buffs[buffId]->clear((BuffClass)buffClass);
|
||||
buffs[buffId]->clear(buffClass);
|
||||
// buff might not be stale since another buff class might remain
|
||||
if(buffs[buffId]->isStale()) {
|
||||
delete buffs[buffId];
|
||||
buffs.erase(buffId);
|
||||
@@ -58,6 +60,16 @@ void Player::removeBuff(int buffId, int buffClass) {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::clearBuffs(bool force) {
|
||||
auto it = buffs.begin();
|
||||
while(it != buffs.end()) {
|
||||
Buff* buff = (*it).second;
|
||||
if(!force) buff->clear();
|
||||
delete buff;
|
||||
it = buffs.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::hasBuff(int buffId) {
|
||||
auto buff = buffs.find(buffId);
|
||||
return buff != buffs.end() && !buff->second->isStale();
|
||||
@@ -156,29 +168,78 @@ void Player::step(time_t currTime) {
|
||||
// buffs
|
||||
for(auto buffEntry : buffs) {
|
||||
buffEntry.second->combatTick(currTime);
|
||||
if(!isAlive())
|
||||
break; // unsafe to keep ticking if we're dead
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region CombatNPC
|
||||
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) { /* stubbed */
|
||||
bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) {
|
||||
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;
|
||||
}
|
||||
|
||||
buffs[buffId]->updateCallbacks(onUpdate, onTick);
|
||||
buffs[buffId]->addStack(stack);
|
||||
return false;
|
||||
}
|
||||
|
||||
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
|
||||
Buff* CombatNPC::getBuff(int buffId) {
|
||||
if(hasBuff(buffId)) {
|
||||
return buffs[buffId];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void CombatNPC::removeBuff(int buffId) { /* stubbed */ }
|
||||
|
||||
void CombatNPC::removeBuff(int buffId, int buffClass) { /* stubbed */ }
|
||||
|
||||
bool CombatNPC::hasBuff(int buffId) { /* stubbed */
|
||||
return false;
|
||||
void CombatNPC::removeBuff(int buffId) {
|
||||
if(hasBuff(buffId)) {
|
||||
buffs[buffId]->clear();
|
||||
delete buffs[buffId];
|
||||
buffs.erase(buffId);
|
||||
}
|
||||
}
|
||||
|
||||
int CombatNPC::getCompositeCondition() { /* stubbed */
|
||||
return 0;
|
||||
void CombatNPC::removeBuff(int buffId, BuffClass buffClass) {
|
||||
if(hasBuff(buffId)) {
|
||||
buffs[buffId]->clear(buffClass);
|
||||
// buff might not be stale since another buff class might remain
|
||||
if(buffs[buffId]->isStale()) {
|
||||
delete buffs[buffId];
|
||||
buffs.erase(buffId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CombatNPC::clearBuffs(bool force) {
|
||||
auto it = buffs.begin();
|
||||
while(it != buffs.end()) {
|
||||
Buff* buff = (*it).second;
|
||||
if(!force) buff->clear();
|
||||
delete buff;
|
||||
it = buffs.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool CombatNPC::hasBuff(int buffId) {
|
||||
auto buff = buffs.find(buffId);
|
||||
return buff != buffs.end() && !buff->second->isStale();
|
||||
}
|
||||
|
||||
int CombatNPC::getCompositeCondition() {
|
||||
int conditionBitFlag = 0;
|
||||
for(auto buff : buffs) {
|
||||
if(!buff.second->isStale() && buff.second->id > 0)
|
||||
conditionBitFlag |= CSB_FROM_ECSB(buff.first);
|
||||
}
|
||||
return conditionBitFlag;
|
||||
}
|
||||
|
||||
int CombatNPC::takeDamage(EntityRef src, int amt) {
|
||||
@@ -249,19 +310,19 @@ void CombatNPC::step(time_t currTime) {
|
||||
}
|
||||
|
||||
void CombatNPC::transition(AIState newState, EntityRef src) {
|
||||
|
||||
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);
|
||||
}
|
||||
/* TODO: fire any triggered events
|
||||
|
||||
// trigger special NPCEvents, if applicable
|
||||
for (NPCEvent& event : NPCManager::NPCEvents)
|
||||
if (event.trigger == ON_KILLED && event.npcType == type)
|
||||
event.handler(src, this);
|
||||
*/
|
||||
if (event.triggerState == newState && event.npcType == type)
|
||||
event.handler(this);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -304,7 +365,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
||||
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
|
||||
Player *plr = PlayerManager::getPlayer(sock);
|
||||
time_t currTime = getTime();
|
||||
|
||||
@@ -316,7 +377,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
||||
plr->lastShot = currTime;
|
||||
|
||||
// 3+ targets should never be possible
|
||||
if (targetCount > 3)
|
||||
if (!allowManyTargets && targetCount > 3)
|
||||
plr->suspicionRating += 10001;
|
||||
|
||||
// kill the socket when the player is too suspicious
|
||||
@@ -335,7 +396,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
|
||||
auto targets = (int32_t*)data->trailers;
|
||||
|
||||
// kick the player if firing too rapidly
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
|
||||
return;
|
||||
|
||||
/*
|
||||
@@ -776,6 +837,10 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// kick the player if firing too rapidly
|
||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
|
||||
return;
|
||||
|
||||
/*
|
||||
* initialize response struct
|
||||
* rocket style hit doesn't work properly, so we're always sending this one
|
||||
@@ -899,6 +964,7 @@ static void playerTick(CNServer *serv, time_t currTime) {
|
||||
auto it = plr->buffs.begin();
|
||||
while(it != plr->buffs.end()) {
|
||||
Buff* buff = (*it).second;
|
||||
//buff->combatTick() gets called in Player::step
|
||||
buff->tick(currTime);
|
||||
if(buff->isStale()) {
|
||||
// garbage collect
|
||||
|
@@ -359,23 +359,19 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
|
||||
int angle = (plr->angle + 180) % 360;
|
||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
||||
|
||||
// if it's a gruntwork NPC, rotate in-place
|
||||
if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) {
|
||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
||||
bool isGruntworkNpc = true;
|
||||
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
|
||||
+ std::to_string(npc->id));
|
||||
} else {
|
||||
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
|
||||
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) {
|
||||
TableData::RunningNPCRotations[npc->id] = angle;
|
||||
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
|
||||
+ std::to_string(npc->id));
|
||||
isGruntworkNpc = false;
|
||||
}
|
||||
|
||||
// update rotation clientside
|
||||
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
|
||||
pkt.NPCAppearanceData = npc->getAppearanceData();
|
||||
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
|
||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
|
||||
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id));
|
||||
|
||||
// update rotation clientside by refreshing the player's chunks (same as the /refresh command)
|
||||
PlayerManager::updatePlayerPositionForWarp(sock, plr->x, plr->y, plr->z, plr->instanceID);
|
||||
}
|
||||
|
||||
static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
|
||||
@@ -678,7 +674,6 @@ static void whoisCommand(std::string full, std::vector<std::string>& args, CNSoc
|
||||
Chat::sendServerMessage(sock, "[WHOIS] ID: " + std::to_string(npc->id));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] Type: " + std::to_string(npc->type));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] HP: " + std::to_string(npc->hp));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] CBF: " + std::to_string(npc->cbf));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] EntityType: " + std::to_string((int)npc->kind));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] X: " + std::to_string(npc->x));
|
||||
Chat::sendServerMessage(sock, "[WHOIS] Y: " + std::to_string(npc->y));
|
||||
|
54
src/Eggs.cpp
54
src/Eggs.cpp
@@ -26,6 +26,7 @@ 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
|
||||
@@ -50,12 +51,57 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
// use skill
|
||||
std::vector<ICombatant*> targets;
|
||||
targets.push_back(dynamic_cast<ICombatant*>(plr));
|
||||
Abilities::useNPCSkill(src, skillId, targets);
|
||||
// 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);
|
||||
}
|
||||
|
||||
static void eggStep(CNServer* serv, time_t currTime) {
|
||||
|
@@ -40,7 +40,7 @@ sNPCAppearanceData BaseNPC::getAppearanceData() {
|
||||
sNPCAppearanceData data = {};
|
||||
data.iAngle = angle;
|
||||
data.iBarkerType = 0; // unused?
|
||||
data.iConditionBitFlag = cbf;
|
||||
data.iConditionBitFlag = 0;
|
||||
data.iHP = hp;
|
||||
data.iNPCType = type;
|
||||
data.iNPC_ID = id;
|
||||
@@ -50,6 +50,12 @@ sNPCAppearanceData BaseNPC::getAppearanceData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
sNPCAppearanceData CombatNPC::getAppearanceData() {
|
||||
sNPCAppearanceData data = BaseNPC::getAppearanceData();
|
||||
data.iConditionBitFlag = getCompositeCondition();
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity coming into view.
|
||||
*/
|
||||
|
@@ -47,7 +47,8 @@ public:
|
||||
virtual bool addBuff(int, BuffCallback<int, BuffStack*>, BuffCallback<time_t>, BuffStack*) = 0;
|
||||
virtual Buff* getBuff(int) = 0;
|
||||
virtual void removeBuff(int) = 0;
|
||||
virtual void removeBuff(int, int) = 0;
|
||||
virtual void removeBuff(int, BuffClass) = 0;
|
||||
virtual void clearBuffs(bool) = 0;
|
||||
virtual bool hasBuff(int) = 0;
|
||||
virtual int getCompositeCondition() = 0;
|
||||
virtual int takeDamage(EntityRef, int) = 0;
|
||||
@@ -72,7 +73,6 @@ public:
|
||||
int type;
|
||||
int hp;
|
||||
int angle;
|
||||
int cbf;
|
||||
bool loopingPath = false;
|
||||
|
||||
BaseNPC(int _A, uint64_t iID, int t, int _id) {
|
||||
@@ -80,7 +80,6 @@ public:
|
||||
type = t;
|
||||
hp = 400;
|
||||
angle = _A;
|
||||
cbf = 0;
|
||||
id = _id;
|
||||
instanceID = iID;
|
||||
};
|
||||
@@ -88,7 +87,7 @@ public:
|
||||
virtual void enterIntoViewOf(CNSocket *sock) override;
|
||||
virtual void disappearFromViewOf(CNSocket *sock) override;
|
||||
|
||||
sNPCAppearanceData getAppearanceData();
|
||||
virtual sNPCAppearanceData getAppearanceData();
|
||||
};
|
||||
|
||||
struct CombatNPC : public BaseNPC, public ICombatant {
|
||||
@@ -105,11 +104,13 @@ struct CombatNPC : public BaseNPC, public ICombatant {
|
||||
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
|
||||
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
|
||||
|
||||
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
|
||||
std::unordered_map<int, Buff*> buffs = {};
|
||||
|
||||
CombatNPC(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, int id, int maxHP)
|
||||
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
|
||||
spawnX = x;
|
||||
spawnY = y;
|
||||
spawnZ = z;
|
||||
this->spawnX = spawnX;
|
||||
this->spawnY = spawnY;
|
||||
this->spawnZ = spawnZ;
|
||||
|
||||
kind = EntityKind::COMBAT_NPC;
|
||||
|
||||
@@ -117,12 +118,15 @@ struct CombatNPC : public BaseNPC, public ICombatant {
|
||||
transitionHandlers[AIState::INACTIVE] = {};
|
||||
}
|
||||
|
||||
virtual sNPCAppearanceData getAppearanceData() override;
|
||||
|
||||
virtual bool isExtant() override { return hp > 0; }
|
||||
|
||||
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
|
||||
virtual Buff* getBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId, int buffClass) override;
|
||||
virtual void removeBuff(int buffId, BuffClass buffClass) override;
|
||||
virtual void clearBuffs(bool force) override;
|
||||
virtual bool hasBuff(int buffId) override;
|
||||
virtual int getCompositeCondition() override;
|
||||
virtual int takeDamage(EntityRef src, int amt) override;
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#include "EntityRef.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <assert.h>
|
||||
|
||||
struct Group {
|
||||
std::vector<EntityRef> members;
|
||||
@@ -14,6 +15,10 @@ struct Group {
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
EntityRef getLeader() {
|
||||
assert(members.size() > 0);
|
||||
return members[0];
|
||||
}
|
||||
|
||||
Group(EntityRef leader);
|
||||
};
|
||||
|
179
src/MobAI.cpp
179
src/MobAI.cpp
@@ -51,13 +51,13 @@ int Mob::takeDamage(EntityRef src, int amt) {
|
||||
}
|
||||
|
||||
// wake up sleeping monster
|
||||
if (cbf & CSB_BIT_MEZ) {
|
||||
cbf &= ~CSB_BIT_MEZ;
|
||||
if (hasBuff(ECSB_MEZ)) {
|
||||
removeBuff(ECSB_MEZ);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = id;
|
||||
pkt1.iConditionBitFlag = cbf;
|
||||
pkt1.iConditionBitFlag = getCompositeCondition();
|
||||
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
@@ -96,13 +96,12 @@ static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
|
||||
|
||||
void MobAI::clearDebuff(Mob *mob) {
|
||||
mob->skillStyle = -1;
|
||||
mob->cbf = 0;
|
||||
mob->unbuffTimes.clear();
|
||||
mob->clearBuffs(false);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = mob->id;
|
||||
pkt1.iConditionBitFlag = mob->cbf;
|
||||
pkt1.iConditionBitFlag = mob->getCompositeCondition();
|
||||
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
}
|
||||
|
||||
@@ -228,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 style) {
|
||||
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int mobStyle) {
|
||||
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||
|
||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
|
||||
@@ -247,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 = style;
|
||||
resp->iStyle = mobStyle;
|
||||
resp->iValue1 = plr->x;
|
||||
resp->iValue2 = plr->y;
|
||||
resp->iValue3 = plr->z;
|
||||
@@ -281,26 +280,38 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
|
||||
respdata[i].iActiveNanoSlotNum = n;
|
||||
respdata[i].iNanoID = plr->activeNano;
|
||||
|
||||
int style2 = Nanos::nanoStyle(plr->activeNano);
|
||||
if (style2 == -1) { // no nano
|
||||
int nanoStyle = Nanos::nanoStyle(plr->activeNano);
|
||||
if (nanoStyle == -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 (style == style2) {
|
||||
} else if (mobStyle == nanoStyle) {
|
||||
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
||||
respdata[i].iDamage = 0;
|
||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
||||
} else if (style - style2 == 1 || style2 - style == 2) {
|
||||
} else if (mobStyle - nanoStyle == 1 || nanoStyle - mobStyle == 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
|
||||
// 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);*/
|
||||
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 });
|
||||
} 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;
|
||||
@@ -327,12 +338,6 @@ 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
|
||||
@@ -347,7 +352,7 @@ static void useAbilities(Mob *mob, time_t currTime) {
|
||||
|
||||
if (mob->skillStyle == -2) { // eruption hit
|
||||
int skillID = (int)mob->data["m_iMegaType"];
|
||||
std::vector<int> targetData = {0, 0, 0, 0, 0};
|
||||
std::vector<ICombatant*> targets{};
|
||||
|
||||
// find the players within range of eruption
|
||||
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
||||
@@ -357,26 +362,22 @@ 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->HP <= 0)
|
||||
if (!plr->isAlive())
|
||||
continue;
|
||||
|
||||
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
|
||||
if (distance < Abilities::SkillTable[skillID].effectArea) {
|
||||
targetData[0] += 1;
|
||||
targetData[targetData[0]] = plr->iID;
|
||||
if (targetData[0] > 3) // make sure not to have more than 4
|
||||
targets.push_back(plr);
|
||||
if (targets.size() > 3) // make sure not to have more than 4
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]);*/
|
||||
Abilities::useNPCSkill(mob->id, skillID, targets);
|
||||
mob->skillStyle = -3; // eruption cooldown
|
||||
mob->nextAttack = currTime + 1000;
|
||||
return;
|
||||
@@ -394,14 +395,11 @@ static void useAbilities(Mob *mob, time_t currTime) {
|
||||
|
||||
if (random < prob1) { // active skill hit
|
||||
int skillID = (int)mob->data["m_iActiveSkill1"];
|
||||
// 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]);
|
||||
// }
|
||||
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 });
|
||||
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
||||
return;
|
||||
}
|
||||
@@ -442,31 +440,6 @@ static void useAbilities(Mob *mob, time_t currTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
static void drainMobHP(Mob *mob, int amount) {
|
||||
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||
|
||||
memset(respbuf, 0, resplen);
|
||||
|
||||
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
||||
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||
|
||||
pkt->iID = mob->id;
|
||||
pkt->eCT = 4; // mob
|
||||
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
||||
|
||||
drain->eCT = 4;
|
||||
drain->iID = mob->id;
|
||||
drain->iDamage = amount;
|
||||
drain->iHP = mob->hp -= amount;
|
||||
|
||||
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
||||
|
||||
if (mob->hp <= 0)
|
||||
mob->transition(AIState::DEAD, mob->target);
|
||||
}
|
||||
|
||||
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
|
||||
if (currTime == 0)
|
||||
currTime = getTime();
|
||||
@@ -553,38 +526,27 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// drain
|
||||
if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000)
|
||||
&& self->cbf & CSB_BIT_BOUNDINGBALL) {
|
||||
drainMobHP(self, self->maxHealth / 20); // lose 5% every second
|
||||
self->lastDrainTime = currTime;
|
||||
}
|
||||
// tick buffs
|
||||
auto it = npc->buffs.begin();
|
||||
while(it != npc->buffs.end()) {
|
||||
Buff* buff = (*it).second;
|
||||
buff->combatTick(currTime);
|
||||
|
||||
// if drain killed the mob, return early
|
||||
if (self->hp <= 0)
|
||||
return;
|
||||
// if mob state changed, end the step
|
||||
if(self->state != AIState::COMBAT)
|
||||
return;
|
||||
|
||||
// unbuffing
|
||||
std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin();
|
||||
while (it != self->unbuffTimes.end()) {
|
||||
|
||||
if (currTime >= it->second) {
|
||||
self->cbf &= ~it->first;
|
||||
|
||||
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
||||
pkt1.eCT = 2;
|
||||
pkt1.iID = self->id;
|
||||
pkt1.iConditionBitFlag = self->cbf;
|
||||
NPCManager::sendToViewable(self, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||
|
||||
it = self->unbuffTimes.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
buff->tick(currTime);
|
||||
if(buff->isStale()) {
|
||||
// garbage collect
|
||||
it = npc->buffs.erase(it);
|
||||
delete buff;
|
||||
}
|
||||
else it++;
|
||||
}
|
||||
|
||||
// skip attack if stunned or asleep
|
||||
if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
|
||||
if (self->hasBuff(ECSB_STUN) || self->hasBuff(ECSB_MEZ)) {
|
||||
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
|
||||
return;
|
||||
}
|
||||
@@ -600,6 +562,7 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
}
|
||||
|
||||
int distanceToTravel = INT_MAX;
|
||||
int speed = self->speed;
|
||||
// movement logic: move when out of range but don't move while casting a skill
|
||||
if (distance > mobRange && self->skillStyle == -1) {
|
||||
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||
@@ -609,8 +572,8 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
self->nextAttack = 0;
|
||||
|
||||
// halve movement speed if snared
|
||||
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
||||
self->speed /= 2;
|
||||
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
|
||||
speed /= 2;
|
||||
|
||||
int targetX = plr->x;
|
||||
int targetY = plr->y;
|
||||
@@ -619,9 +582,9 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
targetY += self->offsetY*distance/(self->idleRange + 1);
|
||||
}
|
||||
|
||||
distanceToTravel = std::min(distance-mobRange+1, self->speed*2/5);
|
||||
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
|
||||
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
|
||||
if (distanceToTravel < self->speed*2/5 && currTime >= self->nextAttack)
|
||||
if (distanceToTravel < speed*2/5 && currTime >= self->nextAttack)
|
||||
self->nextAttack = 0;
|
||||
|
||||
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
|
||||
@@ -629,7 +592,7 @@ void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||
|
||||
pkt.iNPC_ID = self->id;
|
||||
pkt.iSpeed = self->speed;
|
||||
pkt.iSpeed = speed;
|
||||
pkt.iToX = self->x = targ.first;
|
||||
pkt.iToY = self->y = targ.second;
|
||||
pkt.iToZ = plr->z;
|
||||
@@ -715,7 +678,7 @@ void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
|
||||
farY = std::clamp(farY, yStart, yStart + self->idleRange);
|
||||
|
||||
// halve movement speed if snared
|
||||
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
||||
if (self->hasBuff(ECSB_DN_MOVE_SPEED))
|
||||
self->speed /= 2;
|
||||
|
||||
std::queue<Vec3> queue;
|
||||
@@ -788,14 +751,10 @@ void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
|
||||
self->hp = self->maxHealth;
|
||||
self->killedTime = 0;
|
||||
self->nextAttack = 0;
|
||||
self->cbf = 0;
|
||||
|
||||
// cast a return home heal spell, this is the right way(tm)
|
||||
// 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]);*/
|
||||
Abilities::useNPCSkill(npc->getRef(), 110, { npc });
|
||||
|
||||
// clear outlying debuffs
|
||||
clearDebuff(self);
|
||||
}
|
||||
@@ -812,12 +771,9 @@ void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
|
||||
self->roamY = self->y;
|
||||
self->roamZ = self->z;
|
||||
|
||||
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]);*/
|
||||
int skillID = (int)self->data["m_iPassiveBuff"];
|
||||
if(skillID != 0) // cast passive
|
||||
Abilities::useNPCSkill(npc->getRef(), skillID, { npc });
|
||||
}
|
||||
|
||||
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
|
||||
@@ -833,9 +789,8 @@ void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
|
||||
Mob* self = (Mob*)npc;
|
||||
|
||||
self->target = nullptr;
|
||||
self->cbf = 0;
|
||||
self->skillStyle = -1;
|
||||
self->unbuffTimes.clear();
|
||||
self->clearBuffs(true);
|
||||
self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
||||
|
||||
// check for the edge case where hitting the mob did not aggro it
|
||||
|
@@ -21,8 +21,6 @@ namespace MobAI {
|
||||
}
|
||||
|
||||
struct Mob : public CombatNPC {
|
||||
// general
|
||||
std::unordered_map<int32_t,time_t> unbuffTimes = {};
|
||||
|
||||
// dead
|
||||
time_t killedTime = 0;
|
||||
@@ -52,8 +50,8 @@ struct Mob : public CombatNPC {
|
||||
// temporary; until we're sure what's what
|
||||
nlohmann::json data = {};
|
||||
|
||||
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"]),
|
||||
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"]),
|
||||
sightRange(d["m_iSightRange"]) {
|
||||
state = AIState::ROAMING;
|
||||
|
||||
@@ -64,15 +62,13 @@ struct Mob : public CombatNPC {
|
||||
idleRange = (int)data["m_iIdleRange"];
|
||||
level = data["m_iNpcLevel"];
|
||||
|
||||
roamX = x;
|
||||
roamY = y;
|
||||
roamZ = z;
|
||||
roamX = spawnX;
|
||||
roamY = spawnY;
|
||||
roamZ = spawnZ;
|
||||
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
|
||||
cbf = 0;
|
||||
|
||||
// NOTE: there appear to be discrepancies in the dump
|
||||
hp = maxHealth;
|
||||
|
||||
|
@@ -122,16 +122,15 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
|
||||
}
|
||||
|
||||
// type must already be checked and updateNPCPosition() must be called on the result
|
||||
BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, bool respawn, bool baseInstance) {
|
||||
BaseNPC *NPCManager::summonNPC(int spawnX, int spawnY, int spawnZ, 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(x, y, z, inst, type, NPCData[type], id);
|
||||
npc = new Mob(spawnX, spawnY, spawnZ, inst, type, NPCData[type], id);
|
||||
|
||||
// re-enable respawning, if desired
|
||||
((Mob*)npc)->summoned = !respawn;
|
||||
@@ -294,57 +293,55 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
|
||||
return npc;
|
||||
}
|
||||
|
||||
// TODO: Move this to MobAI, possibly
|
||||
// TODO: Move this to separate file in ai/ subdir when implementing more events
|
||||
#pragma region NPCEvents
|
||||
|
||||
// summon right arm and stage 2 body
|
||||
static void lordFuseStageTwo(CNSocket *sock, BaseNPC *npc) {
|
||||
static void lordFuseStageTwo(CombatNPC *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, plr->instanceID, 2467);
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2467);
|
||||
|
||||
newbody->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
|
||||
plr->instanceID, oldbody->angle);
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
oldbody->instanceID, oldbody->angle);
|
||||
|
||||
// right arm, Adaptium, Stun
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, plr->instanceID, 2469);
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x - 600, oldbody->y, oldbody->z, oldbody->instanceID, 2469);
|
||||
|
||||
arm->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
|
||||
plr->instanceID, oldbody->angle);
|
||||
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
oldbody->instanceID, oldbody->angle);
|
||||
}
|
||||
|
||||
// summon left arm and stage 3 body
|
||||
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
|
||||
static void lordFuseStageThree(CombatNPC *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, plr->instanceID, 2468);
|
||||
Mob *newbody = (Mob*)NPCManager::summonNPC(oldbody->x, oldbody->y, oldbody->z, oldbody->instanceID, 2468);
|
||||
|
||||
newbody->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
|
||||
plr->instanceID, oldbody->angle);
|
||||
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||
newbody->instanceID, oldbody->angle);
|
||||
|
||||
// Blastons, Heal
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, plr->instanceID, 2470);
|
||||
Mob *arm = (Mob*)NPCManager::summonNPC(oldbody->x + 600, oldbody->y, oldbody->z, oldbody->instanceID, 2470);
|
||||
|
||||
arm->angle = oldbody->angle;
|
||||
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
|
||||
plr->instanceID, oldbody->angle);
|
||||
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||
arm->instanceID, oldbody->angle);
|
||||
}
|
||||
|
||||
std::vector<NPCEvent> NPCManager::NPCEvents = {
|
||||
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
|
||||
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
|
||||
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
|
||||
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
|
||||
};
|
||||
|
||||
#pragma endregion NPCEvents
|
||||
@@ -376,5 +373,5 @@ void NPCManager::init() {
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_NPC_UNSUMMON, npcUnsummonHandler);
|
||||
REGISTER_SHARD_PACKET(P_CL2FE_REQ_BARKER, npcBarkHandler);
|
||||
|
||||
REGISTER_SHARD_TIMER(step, 200);
|
||||
REGISTER_SHARD_TIMER(step, MS_PER_COMBAT_TICK);
|
||||
}
|
||||
|
@@ -14,20 +14,15 @@
|
||||
|
||||
#define RESURRECT_HEIGHT 400
|
||||
|
||||
enum Trigger {
|
||||
ON_KILLED,
|
||||
ON_COMBAT
|
||||
};
|
||||
|
||||
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
|
||||
typedef void (*NPCEventHandler)(CombatNPC*);
|
||||
|
||||
struct NPCEvent {
|
||||
int32_t npcType;
|
||||
int trigger;
|
||||
AIState triggerState;
|
||||
NPCEventHandler handler;
|
||||
|
||||
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
|
||||
: npcType(t), trigger(tr), handler(hndlr) {}
|
||||
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
|
||||
: npcType(t), triggerState(tr), handler(hndlr) {}
|
||||
};
|
||||
|
||||
namespace NPCManager {
|
||||
|
@@ -92,7 +92,8 @@ struct Player : public Entity, public ICombatant {
|
||||
virtual bool addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, BuffCallback<time_t> onTick, BuffStack* stack) override;
|
||||
virtual Buff* getBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId) override;
|
||||
virtual void removeBuff(int buffId, int buffClass) override;
|
||||
virtual void removeBuff(int buffId, BuffClass buffClass) override;
|
||||
virtual void clearBuffs(bool force) override;
|
||||
virtual bool hasBuff(int buffId) override;
|
||||
virtual int getCompositeCondition() override;
|
||||
virtual int takeDamage(EntityRef src, int amt) override;
|
||||
|
@@ -77,7 +77,10 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
|
||||
plr->x = X;
|
||||
plr->y = Y;
|
||||
plr->z = Z;
|
||||
plr->instanceID = I;
|
||||
if (plr->instanceID != I) {
|
||||
plr->instanceID = I;
|
||||
plr->recallInstance = INSTANCE_OVERWORLD;
|
||||
}
|
||||
if (oldChunk == newChunk)
|
||||
return; // didn't change chunks
|
||||
Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
|
||||
@@ -123,24 +126,6 @@ void PlayerManager::sendPlayerTo(CNSocket* sock, int X, int Y, int Z, uint64_t I
|
||||
sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_NPC_SUCC);
|
||||
}
|
||||
|
||||
if (I != INSTANCE_OVERWORLD) {
|
||||
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
|
||||
pkt.iInstanceMapNum = (int32_t)MAPNUM(I); // lower 32 bits are mapnum
|
||||
if (I != fromInstance // do not retransmit MAP_INFO on recall
|
||||
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
|
||||
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
|
||||
pkt.iEP_ID = ep->EPID;
|
||||
pkt.iMapCoordX_Min = ep->zoneX * 51200;
|
||||
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
|
||||
pkt.iMapCoordY_Min = ep->zoneY * 51200;
|
||||
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
|
||||
pkt.iMapCoordZ_Min = INT32_MIN;
|
||||
pkt.iMapCoordZ_Max = INT32_MAX;
|
||||
}
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
|
||||
}
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
|
||||
pkt2.iX = X;
|
||||
pkt2.iY = Y;
|
||||
@@ -374,6 +359,24 @@ static void loadPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
|
||||
|
||||
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
|
||||
|
||||
if (plr->instanceID != INSTANCE_OVERWORLD) {
|
||||
INITSTRUCT(sP_FE2CL_INSTANCE_MAP_INFO, pkt);
|
||||
pkt.iInstanceMapNum = (int32_t)MAPNUM(plr->instanceID); // lower 32 bits are mapnum
|
||||
if (pkt.iInstanceMapNum != plr->recallInstance // do not retransmit MAP_INFO on recall
|
||||
&& Racing::EPData.find(pkt.iInstanceMapNum) != Racing::EPData.end()) {
|
||||
EPInfo* ep = &Racing::EPData[pkt.iInstanceMapNum];
|
||||
pkt.iEP_ID = ep->EPID;
|
||||
pkt.iMapCoordX_Min = ep->zoneX * 51200;
|
||||
pkt.iMapCoordX_Max = (ep->zoneX + 1) * 51200;
|
||||
pkt.iMapCoordY_Min = ep->zoneY * 51200;
|
||||
pkt.iMapCoordY_Max = (ep->zoneY + 1) * 51200;
|
||||
pkt.iMapCoordZ_Min = INT32_MIN;
|
||||
pkt.iMapCoordZ_Max = INT32_MAX;
|
||||
}
|
||||
|
||||
sock->sendPacket(pkt, P_FE2CL_INSTANCE_MAP_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
|
||||
@@ -416,6 +419,7 @@ 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;
|
||||
@@ -576,7 +580,7 @@ static void setFirstUseFlag(CNSocket* sock, CNPacketData* data) {
|
||||
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (flag->iFlagCode <= 64)
|
||||
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
|
||||
else
|
||||
|
@@ -66,7 +66,7 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
|
||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
|
||||
|
||||
/*
|
||||
/*
|
||||
* This request packet is used for both cancelling the race via the
|
||||
* NPC at the start, *and* failing the race by running out of time.
|
||||
* If the latter is to happen, the client disables movement until it
|
||||
@@ -99,31 +99,37 @@ static void racingEnd(CNSocket* sock, CNPacketData* data) {
|
||||
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||
return; // IZ not found
|
||||
|
||||
uint64_t now = getTime() / 1000;
|
||||
EPInfo& epInfo = EPData[mapNum];
|
||||
EPRace& epRace = EPRaces[sock];
|
||||
|
||||
int timeDiff = now - EPRaces[sock].startTime;
|
||||
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
|
||||
if (score < 0) score = 0; // lol
|
||||
int fm = score * plr->level * (1.0f / 36) * 0.3f;
|
||||
uint64_t now = getTime() / 1000;
|
||||
int timeDiff = now - epRace.startTime;
|
||||
int podsCollected = epRace.collectedRings.size();
|
||||
|
||||
int score = std::min(epInfo.maxScore, (int)std::exp(
|
||||
(epInfo.podFactor * podsCollected) / epInfo.maxPods
|
||||
- (epInfo.timeFactor * timeDiff) / epInfo.maxTime
|
||||
+ epInfo.scaleFactor));
|
||||
int fm = (1.0 + std::exp(epInfo.scaleFactor - 1.0) * epInfo.podFactor * podsCollected) / epInfo.maxPods;
|
||||
|
||||
// we submit the ranking first...
|
||||
Database::RaceRanking postRanking = {};
|
||||
postRanking.EPID = EPData[mapNum].EPID;
|
||||
postRanking.EPID = epInfo.EPID;
|
||||
postRanking.PlayerID = plr->iID;
|
||||
postRanking.RingCount = EPRaces[sock].collectedRings.size();
|
||||
postRanking.RingCount = podsCollected;
|
||||
postRanking.Score = score;
|
||||
postRanking.Time = timeDiff;
|
||||
postRanking.Timestamp = getTimestamp();
|
||||
Database::postRaceRanking(postRanking);
|
||||
|
||||
// ...then we get the top ranking, which may or may not be what we just submitted
|
||||
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(EPData[mapNum].EPID, plr->iID);
|
||||
Database::RaceRanking topRankingPlayer = Database::getTopRaceRanking(epInfo.EPID, plr->iID);
|
||||
|
||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
|
||||
|
||||
// get rank scores and rewards
|
||||
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
|
||||
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
|
||||
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
|
||||
|
||||
// top ranking
|
||||
int topRank = 0;
|
||||
|
@@ -7,7 +7,11 @@
|
||||
#include <set>
|
||||
|
||||
struct EPInfo {
|
||||
int zoneX, zoneY, EPID, maxScore, maxTime;
|
||||
// available through XDT (maxScore may be updated by drops)
|
||||
int zoneX, zoneY, EPID, maxScore;
|
||||
// available through drops
|
||||
int maxTime, maxPods;
|
||||
double scaleFactor, podFactor, timeFactor;
|
||||
};
|
||||
|
||||
struct EPRace {
|
||||
|
@@ -375,7 +375,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
|
||||
Transport::NPCPaths.push_back(pathTemplate);
|
||||
}
|
||||
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
|
||||
|
||||
|
||||
}
|
||||
catch (const std::exception& err) {
|
||||
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
|
||||
@@ -584,8 +584,17 @@ static void loadDrops(json& dropData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// time limit isn't stored in the XDT, so we include it in the reward table instead
|
||||
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
|
||||
EPInfo& epInfo = Racing::EPData[EPMap];
|
||||
|
||||
// max score is specified in the XDT, but can be updated if specified in the drops JSON
|
||||
epInfo.maxScore = (int)race["ScoreCap"];
|
||||
// time limit and total pods are not stored in the XDT, so we include it in the drops JSON
|
||||
epInfo.maxTime = (int)race["TimeLimit"];
|
||||
epInfo.maxPods = (int)race["TotalPods"];
|
||||
// IZ-specific calculated constants included in the drops JSON
|
||||
epInfo.scaleFactor = (double)race["ScaleFactor"];
|
||||
epInfo.podFactor = (double)race["PodFactor"];
|
||||
epInfo.timeFactor = (double)race["TimeFactor"];
|
||||
|
||||
// score cutoffs
|
||||
std::vector<int> rankScores;
|
||||
@@ -686,7 +695,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Load gruntwork output, if it exists
|
||||
*/
|
||||
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
|
||||
@@ -1361,7 +1370,7 @@ void TableData::flush() {
|
||||
targetIDs.push_back(tID);
|
||||
for (int32_t tType : path.targetTypes)
|
||||
targetTypes.push_back(tType);
|
||||
|
||||
|
||||
pathObj["iBaseSpeed"] = path.speed;
|
||||
pathObj["iTaskID"] = path.escortTaskID;
|
||||
pathObj["bRelative"] = path.isRelative;
|
||||
|
@@ -64,7 +64,7 @@ void terminate(int arg) {
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static BOOL winTerminate(DWORD arg) {
|
||||
static BOOL WINAPI winTerminate(DWORD arg) {
|
||||
terminate(0);
|
||||
return FALSE;
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@
|
||||
#define REGISTER_SHARD_PACKET(pactype, handlr) CNShardServer::ShardPackets[pactype] = handlr;
|
||||
#define REGISTER_SHARD_TIMER(handlr, delta) CNShardServer::Timers.push_back(TimerEvent(handlr, delta));
|
||||
#define MS_PER_PLAYER_TICK 500
|
||||
#define MS_PER_COMBAT_TICK 200
|
||||
|
||||
class CNShardServer : public CNServer {
|
||||
private:
|
||||
|
2
tdata
2
tdata
Submodule tdata updated: cc65dbb402...8c98c83682
Reference in New Issue
Block a user