mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-11-06 03:10:22 +00:00
Compare commits
44 Commits
a70b1a3543
...
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
|
- CMakeLists.txt
|
||||||
- Makefile
|
- Makefile
|
||||||
pull_request:
|
pull_request:
|
||||||
types: ready_for_review
|
types: [opened, reopened, synchronize, ready_for_review]
|
||||||
paths:
|
paths:
|
||||||
- src/**
|
- src/**
|
||||||
- vendor/**
|
- vendor/**
|
||||||
- CMakeLists.txt
|
- CMakeLists.txt
|
||||||
- Makefile
|
- Makefile
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ubuntu-build:
|
ubuntu-build:
|
||||||
@@ -53,7 +54,7 @@ jobs:
|
|||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: 'ubuntu20_04-bin-x64-${{ env.SHORT_SHA }}'
|
name: 'ubuntu22_04-bin-x64-${{ env.SHORT_SHA }}'
|
||||||
path: bin
|
path: bin
|
||||||
|
|
||||||
windows-build:
|
windows-build:
|
||||||
@@ -112,7 +113,7 @@ jobs:
|
|||||||
|
|
||||||
copy-artifacts:
|
copy-artifacts:
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
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]
|
needs: [windows-build, ubuntu-build]
|
||||||
env:
|
env:
|
||||||
BOT_SSH_KEY: ${{ secrets.BOT_SSH_KEY }}
|
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.
|
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.
|
Many Pull Requests OpenFusion receives fail to present a clean set of commits to merge.
|
||||||
These are generally either:
|
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.
|
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.
|
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.
|
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).
|
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).
|
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.
|
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:
|
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.
|
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`)
|
(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.
|
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`.
|
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`.
|
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.
|
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
|
### Getting Started
|
||||||
#### Method A: Installer (Easiest)
|
#### 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.
|
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.
|
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.
|
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
|
#### 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.
|
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.
|
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.
|
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).
|
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
|
### Hosting a server
|
||||||
|
1. Grab `OpenFusionServer-1.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5).
|
||||||
1. Grab `OpenFusionServer-1.4-original.zip` or `OpenFusionServer-1.4-academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.4).
|
|
||||||
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
|
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:
|
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.
|
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 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.
|
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.
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
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.
|
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.
|
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").
|
`/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!
|
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.
|
`/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
|
## Gameplay
|
||||||
|
|
||||||
The goal of the project is to faithfully recreate the game as it was at the time of the targeted build.
|
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
|
When hosting a local server, you will have access to all commands by default (account level 1).
|
||||||
* 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.).
|
|
||||||
|
|
||||||
### Item commands
|
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
|
||||||
* `/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).
|
|
||||||
|
|||||||
@@ -76,12 +76,20 @@ static SkillResult handleSkillDamageNDebuff(SkillData* skill, int power, ICombat
|
|||||||
}
|
}
|
||||||
|
|
||||||
sSkillResult_Damage_N_Debuff result{};
|
sSkillResult_Damage_N_Debuff result{};
|
||||||
|
|
||||||
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
|
result.iDamage = duration / 10; // we use the duration as the damage number (why?)
|
||||||
result.iHP = target->getCurrentHP();
|
result.iHP = target->getCurrentHP();
|
||||||
result.eCT = target->getCharType();
|
result.eCT = target->getCharType();
|
||||||
result.iID = target->getID();
|
result.iID = target->getID();
|
||||||
result.bProtected = blocked;
|
result.bProtected = blocked;
|
||||||
result.iConditionBitFlag = target->getCompositeCondition();
|
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);
|
return SkillResult(sizeof(sSkillResult_Damage_N_Debuff), &result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +120,8 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
|
|||||||
int duration = skill->durationTime[power];
|
int duration = skill->durationTime[power];
|
||||||
int strength = skill->values[0][power];
|
int strength = skill->values[0][power];
|
||||||
BuffStack passiveBuff = {
|
BuffStack passiveBuff = {
|
||||||
skill->drainType == SkillDrainType::PASSIVE ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
// if the duration is 0, it needs to be recast every tick
|
||||||
|
duration == 0 ? 1 : (duration * 100) / MS_PER_COMBAT_TICK, // ticks
|
||||||
strength, // value
|
strength, // value
|
||||||
source->getRef(), // source
|
source->getRef(), // source
|
||||||
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
|
source == target ? BuffClass::NANO : BuffClass::GROUP_NANO, // buff class
|
||||||
@@ -120,21 +129,25 @@ static SkillResult handleSkillBuff(SkillData* skill, int power, ICombatant* sour
|
|||||||
|
|
||||||
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
int timeBuffId = Abilities::getCSTBFromST(skill->skillType);
|
||||||
SkillDrainType drainType = skill->drainType;
|
SkillDrainType drainType = skill->drainType;
|
||||||
|
int combatLifetime = 0;
|
||||||
if(!target->addBuff(timeBuffId,
|
if(!target->addBuff(timeBuffId,
|
||||||
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
[drainType](EntityRef self, Buff* buff, int status, BuffStack* stack) {
|
||||||
if(buff->id == ECSB_BOUNDINGBALL) {
|
if(buff->id == ECSB_BOUNDINGBALL && status == ETBU_ADD) {
|
||||||
// drain
|
// drain
|
||||||
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
|
ICombatant* combatant = dynamic_cast<ICombatant*>(self.getEntity());
|
||||||
combatant->takeDamage(buff->getLastSource(), 0); // aggro
|
combatant->takeDamage(buff->getLastSource(), 0); // aggro
|
||||||
}
|
}
|
||||||
|
Buffs::timeBuffUpdate(self, buff, status, stack);
|
||||||
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
|
if(drainType == SkillDrainType::ACTIVE && status == ETBU_DEL)
|
||||||
Buffs::timeBuffTimeout(self);
|
Buffs::timeBuffTimeout(self);
|
||||||
},
|
},
|
||||||
[](EntityRef self, Buff* buff, time_t currTime) {
|
[combatLifetime](EntityRef self, Buff* buff, time_t currTime) mutable {
|
||||||
if(buff->id == ECSB_BOUNDINGBALL)
|
if(buff->id == ECSB_BOUNDINGBALL &&
|
||||||
Buffs::tickDrain(self, buff); // drain
|
combatLifetime % COMBAT_TICKS_PER_DRAIN_PROC == 0)
|
||||||
|
Buffs::tickDrain(self, buff, COMBAT_TICKS_PER_DRAIN_PROC); // drain
|
||||||
|
combatLifetime++;
|
||||||
},
|
},
|
||||||
&passiveBuff)) return SkillResult(); // no result if already buffed
|
&passiveBuff)) return SkillResult();
|
||||||
|
|
||||||
sSkillResult_Buff result{};
|
sSkillResult_Buff result{};
|
||||||
result.eCT = target->getCharType();
|
result.eCT = target->getCharType();
|
||||||
@@ -150,19 +163,24 @@ static SkillResult handleSkillBatteryDrain(SkillData* skill, int power, ICombata
|
|||||||
Player* plr = dynamic_cast<Player*>(target);
|
Player* plr = dynamic_cast<Player*>(target);
|
||||||
|
|
||||||
const double scalingFactor = (18 + source->getLevel()) / 36.0;
|
const double scalingFactor = (18 + source->getLevel()) / 36.0;
|
||||||
|
const bool blocked = target->hasBuff(ECSB_PROTECT_BATTERY);
|
||||||
|
|
||||||
int boostDrain = (int)(skill->values[0][power] * scalingFactor);
|
int boostDrain = 0;
|
||||||
if(boostDrain > plr->batteryW) boostDrain = plr->batteryW;
|
int potionDrain = 0;
|
||||||
plr->batteryW -= boostDrain;
|
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);
|
potionDrain = (int)(skill->values[1][power] * scalingFactor);
|
||||||
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
|
if(potionDrain > plr->batteryN) potionDrain = plr->batteryN;
|
||||||
plr->batteryN -= potionDrain;
|
plr->batteryN -= potionDrain;
|
||||||
|
}
|
||||||
|
|
||||||
sSkillResult_BatteryDrain result{};
|
sSkillResult_BatteryDrain result{};
|
||||||
result.eCT = target->getCharType();
|
result.eCT = target->getCharType();
|
||||||
result.iID = target->getID();
|
result.iID = target->getID();
|
||||||
result.bProtected = target->hasBuff(ECSB_PROTECT_BATTERY);
|
result.bProtected = blocked;
|
||||||
result.iDrainW = boostDrain;
|
result.iDrainW = boostDrain;
|
||||||
result.iBatteryW = plr->batteryW;
|
result.iBatteryW = plr->batteryW;
|
||||||
result.iDrainN = potionDrain;
|
result.iDrainN = potionDrain;
|
||||||
@@ -209,6 +227,11 @@ static std::vector<SkillResult> handleSkill(SkillData* skill, int power, ICombat
|
|||||||
|
|
||||||
switch(skill->skillType)
|
switch(skill->skillType)
|
||||||
{
|
{
|
||||||
|
case SkillType::CORRUPTIONATTACK:
|
||||||
|
case SkillType::CORRUPTIONATTACKLOSE:
|
||||||
|
case SkillType::CORRUPTIONATTACKWIN:
|
||||||
|
// skillHandler = handleSkillCorruptionReflect;
|
||||||
|
// break;
|
||||||
case SkillType::DAMAGE:
|
case SkillType::DAMAGE:
|
||||||
skillHandler = handleSkillDamage;
|
skillHandler = handleSkillDamage;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -9,17 +9,18 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
|
const int COMBAT_TICKS_PER_DRAIN_PROC = 2;
|
||||||
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
|
constexpr size_t MAX_SKILLRESULT_SIZE = sizeof(sSkillResult_BatteryDrain);
|
||||||
|
|
||||||
enum class SkillType {
|
enum class SkillType {
|
||||||
DAMAGE = 1,
|
DAMAGE = 1,
|
||||||
HEAL_HP = 2,
|
HEAL_HP = 2,
|
||||||
KNOCKDOWN = 3, // dnd
|
KNOCKDOWN = 3, // uses DamageNDebuff
|
||||||
SLEEP = 4, // dnd
|
SLEEP = 4, // uses DamageNDebuff
|
||||||
SNARE = 5, // dnd
|
SNARE = 5, // uses DamageNDebuff
|
||||||
HEAL_STAMINA = 6,
|
HEAL_STAMINA = 6,
|
||||||
STAMINA_SELF = 7,
|
STAMINA_SELF = 7,
|
||||||
STUN = 8, // dnd
|
STUN = 8, // uses DamageNDebuff
|
||||||
WEAPONSLOW = 9,
|
WEAPONSLOW = 9,
|
||||||
JUMP = 10,
|
JUMP = 10,
|
||||||
RUN = 11,
|
RUN = 11,
|
||||||
|
|||||||
@@ -169,12 +169,13 @@ void Buffs::timeBuffTimeout(EntityRef self) {
|
|||||||
NPCManager::sendToViewable(entity, &pkt, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
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) {
|
void Buffs::tickDrain(EntityRef self, Buff* buff, int mult) {
|
||||||
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
if(self.kind != EntityKind::COMBAT_NPC && self.kind != EntityKind::MOB)
|
||||||
return; // not implemented
|
return; // not implemented
|
||||||
Entity* entity = self.getEntity();
|
Entity* entity = self.getEntity();
|
||||||
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
ICombatant* combatant = dynamic_cast<ICombatant*>(entity);
|
||||||
int damage = combatant->takeDamage(buff->getLastSource(), combatant->getMaxHP() / 100);
|
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);
|
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
||||||
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
||||||
@@ -187,7 +188,7 @@ void Buffs::tickDrain(EntityRef self, Buff* buff) {
|
|||||||
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
||||||
|
|
||||||
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
||||||
drain->iDamage = damage;
|
drain->iDamage = dealt;
|
||||||
drain->iHP = combatant->getCurrentHP();
|
drain->iHP = combatant->getCurrentHP();
|
||||||
drain->eCT = pkt->eCT;
|
drain->eCT = pkt->eCT;
|
||||||
drain->iID = pkt->iID;
|
drain->iID = pkt->iID;
|
||||||
|
|||||||
@@ -89,5 +89,5 @@ namespace Buffs {
|
|||||||
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
|
void timeBuffUpdate(EntityRef self, Buff* buff, int status, BuffStack* stack);
|
||||||
void timeBuffTick(EntityRef self, Buff* buff);
|
void timeBuffTick(EntityRef self, Buff* buff);
|
||||||
void timeBuffTimeout(EntityRef self);
|
void timeBuffTimeout(EntityRef self);
|
||||||
void tickDrain(EntityRef self, Buff* buff);
|
void tickDrain(EntityRef self, Buff* buff, int mult);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,31 +72,41 @@ static void setValuePlayer(CNSocket* sock, CNPacketData* data) {
|
|||||||
|
|
||||||
// Handle serverside value-changes
|
// Handle serverside value-changes
|
||||||
switch (setData->iSetValueType) {
|
switch (setData->iSetValueType) {
|
||||||
case 1:
|
case CN_GM_SET_VALUE_TYPE__HP:
|
||||||
plr->HP = setData->iSetValue;
|
response.iSetValue = plr->HP = setData->iSetValue;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case CN_GM_SET_VALUE_TYPE__WEAPON_BATTERY :
|
||||||
plr->batteryW = setData->iSetValue;
|
plr->batteryW = setData->iSetValue;
|
||||||
|
|
||||||
// caps
|
// caps
|
||||||
if (plr->batteryW > 9999)
|
if (plr->batteryW > 9999)
|
||||||
plr->batteryW = 9999;
|
plr->batteryW = 9999;
|
||||||
|
|
||||||
|
response.iSetValue = plr->batteryW;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case CN_GM_SET_VALUE_TYPE__NANO_BATTERY:
|
||||||
plr->batteryN = setData->iSetValue;
|
plr->batteryN = setData->iSetValue;
|
||||||
|
|
||||||
// caps
|
// caps
|
||||||
if (plr->batteryN > 9999)
|
if (plr->batteryN > 9999)
|
||||||
plr->batteryN = 9999;
|
plr->batteryN = 9999;
|
||||||
|
|
||||||
|
response.iSetValue = plr->batteryN;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case CN_GM_SET_VALUE_TYPE__FUSION_MATTER:
|
||||||
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
Missions::updateFusionMatter(sock, setData->iSetValue - plr->fusionmatter);
|
||||||
|
response.iSetValue = plr->fusionmatter;
|
||||||
break;
|
break;
|
||||||
case 5:
|
case CN_GM_SET_VALUE_TYPE__CANDY:
|
||||||
plr->money = setData->iSetValue;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.iPC_ID = setData->iPC_ID;
|
response.iPC_ID = setData->iPC_ID;
|
||||||
response.iSetValue = setData->iSetValue;
|
|
||||||
response.iSetValueType = setData->iSetValueType;
|
response.iSetValueType = setData->iSetValueType;
|
||||||
|
|
||||||
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
|
sock->sendPacket(response, P_FE2CL_GM_REP_PC_SET_VALUE);
|
||||||
|
|||||||
@@ -175,10 +175,13 @@ void Player::step(time_t currTime) {
|
|||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
#pragma region CombatNPC
|
#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())
|
if(!isAlive())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (this->state != AIState::COMBAT && this->state != AIState::ROAMING)
|
||||||
|
return false;
|
||||||
|
|
||||||
if(!hasBuff(buffId)) {
|
if(!hasBuff(buffId)) {
|
||||||
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
|
buffs[buffId] = new Buff(buffId, getRef(), onUpdate, onTick, stack);
|
||||||
return true;
|
return true;
|
||||||
@@ -189,7 +192,7 @@ bool CombatNPC::addBuff(int buffId, BuffCallback<int, BuffStack*> onUpdate, Buff
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Buff* CombatNPC::getBuff(int buffId) { /* stubbed */
|
Buff* CombatNPC::getBuff(int buffId) {
|
||||||
if(hasBuff(buffId)) {
|
if(hasBuff(buffId)) {
|
||||||
return buffs[buffId];
|
return buffs[buffId];
|
||||||
}
|
}
|
||||||
@@ -307,19 +310,19 @@ void CombatNPC::step(time_t currTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CombatNPC::transition(AIState newState, EntityRef src) {
|
void CombatNPC::transition(AIState newState, EntityRef src) {
|
||||||
|
|
||||||
state = newState;
|
state = newState;
|
||||||
|
|
||||||
if (transitionHandlers.find(newState) != transitionHandlers.end())
|
if (transitionHandlers.find(newState) != transitionHandlers.end())
|
||||||
transitionHandlers[newState](this, src);
|
transitionHandlers[newState](this, src);
|
||||||
else {
|
else {
|
||||||
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
|
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
|
||||||
transition(AIState::INACTIVE, id);
|
transition(AIState::INACTIVE, id);
|
||||||
}
|
}
|
||||||
/* TODO: fire any triggered events
|
|
||||||
|
// trigger special NPCEvents, if applicable
|
||||||
for (NPCEvent& event : NPCManager::NPCEvents)
|
for (NPCEvent& event : NPCManager::NPCEvents)
|
||||||
if (event.trigger == ON_KILLED && event.npcType == type)
|
if (event.triggerState == newState && event.npcType == type)
|
||||||
event.handler(src, this);
|
event.handler(this);
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
@@ -362,7 +365,7 @@ static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shou
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
static bool checkRapidFire(CNSocket *sock, int targetCount, bool allowManyTargets) {
|
||||||
Player *plr = PlayerManager::getPlayer(sock);
|
Player *plr = PlayerManager::getPlayer(sock);
|
||||||
time_t currTime = getTime();
|
time_t currTime = getTime();
|
||||||
|
|
||||||
@@ -374,7 +377,7 @@ static bool checkRapidFire(CNSocket *sock, int targetCount) {
|
|||||||
plr->lastShot = currTime;
|
plr->lastShot = currTime;
|
||||||
|
|
||||||
// 3+ targets should never be possible
|
// 3+ targets should never be possible
|
||||||
if (targetCount > 3)
|
if (!allowManyTargets && targetCount > 3)
|
||||||
plr->suspicionRating += 10001;
|
plr->suspicionRating += 10001;
|
||||||
|
|
||||||
// kill the socket when the player is too suspicious
|
// kill the socket when the player is too suspicious
|
||||||
@@ -393,7 +396,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
|
|||||||
auto targets = (int32_t*)data->trailers;
|
auto targets = (int32_t*)data->trailers;
|
||||||
|
|
||||||
// kick the player if firing too rapidly
|
// kick the player if firing too rapidly
|
||||||
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt))
|
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iNPCCnt, false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -834,6 +837,10 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kick the player if firing too rapidly
|
||||||
|
if (settings::ANTICHEAT && checkRapidFire(sock, pkt->iTargetCnt, true))
|
||||||
|
return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* initialize response struct
|
* initialize response struct
|
||||||
* rocket style hit doesn't work properly, so we're always sending this one
|
* rocket style hit doesn't work properly, so we're always sending this one
|
||||||
|
|||||||
@@ -359,23 +359,19 @@ static void npcRotateCommand(std::string full, std::vector<std::string>& args, C
|
|||||||
int angle = (plr->angle + 180) % 360;
|
int angle = (plr->angle + 180) % 360;
|
||||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
||||||
|
|
||||||
// if it's a gruntwork NPC, rotate in-place
|
bool isGruntworkNpc = true;
|
||||||
if (TableData::RunningMobs.find(npc->id) != TableData::RunningMobs.end()) {
|
|
||||||
NPCManager::updateNPCPosition(npc->id, npc->x, npc->y, npc->z, npc->instanceID, angle);
|
|
||||||
|
|
||||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for gruntwork NPC "
|
// add a rotation entry to the gruntwork file, unless it's already a gruntwork NPC
|
||||||
+ std::to_string(npc->id));
|
if (TableData::RunningMobs.find(npc->id) == TableData::RunningMobs.end()) {
|
||||||
} else {
|
|
||||||
TableData::RunningNPCRotations[npc->id] = angle;
|
TableData::RunningNPCRotations[npc->id] = angle;
|
||||||
|
isGruntworkNpc = false;
|
||||||
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) + " for NPC "
|
|
||||||
+ std::to_string(npc->id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update rotation clientside
|
Chat::sendServerMessage(sock, "[NPCR] Successfully set angle to " + std::to_string(angle) +
|
||||||
INITSTRUCT(sP_FE2CL_NPC_ENTER, pkt);
|
" for " + (isGruntworkNpc ? "gruntwork " : "") + "NPC " + std::to_string(npc->id));
|
||||||
pkt.NPCAppearanceData = npc->getAppearanceData();
|
|
||||||
sock->sendPacket(pkt, P_FE2CL_NPC_ENTER);
|
// 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) {
|
static void refreshCommand(std::string full, std::vector<std::string>& args, CNSocket* sock) {
|
||||||
|
|||||||
54
src/Eggs.cpp
54
src/Eggs.cpp
@@ -26,6 +26,7 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SkillResult result = SkillResult();
|
||||||
SkillData* skill = &Abilities::SkillTable[skillId];
|
SkillData* skill = &Abilities::SkillTable[skillId];
|
||||||
if(skill->drainType == SkillDrainType::PASSIVE) {
|
if(skill->drainType == SkillDrainType::PASSIVE) {
|
||||||
// apply buff
|
// apply buff
|
||||||
@@ -50,12 +51,57 @@ void Eggs::eggBuffPlayer(CNSocket* sock, int skillId, int eggId, int duration) {
|
|||||||
// no-op
|
// no-op
|
||||||
},
|
},
|
||||||
&eggBuff);
|
&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
|
// initialize response struct
|
||||||
std::vector<ICombatant*> targets;
|
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_HIT) + result.size;
|
||||||
targets.push_back(dynamic_cast<ICombatant*>(plr));
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
||||||
Abilities::useNPCSkill(src, skillId, targets);
|
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) {
|
static void eggStep(CNServer* serv, time_t currTime) {
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ struct CombatNPC : public BaseNPC, public ICombatant {
|
|||||||
|
|
||||||
std::unordered_map<int, Buff*> buffs = {};
|
std::unordered_map<int, Buff*> buffs = {};
|
||||||
|
|
||||||
CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP)
|
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) {
|
: BaseNPC(angle, iID, t, id), maxHealth(maxHP) {
|
||||||
spawnX = x;
|
this->spawnX = spawnX;
|
||||||
spawnY = y;
|
this->spawnY = spawnY;
|
||||||
spawnZ = z;
|
this->spawnZ = spawnZ;
|
||||||
|
|
||||||
kind = EntityKind::COMBAT_NPC;
|
kind = EntityKind::COMBAT_NPC;
|
||||||
|
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
|
|||||||
return false;
|
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);
|
Player *plr = PlayerManager::getPlayer(mob->target);
|
||||||
|
|
||||||
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
|
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->iNPC_ID = mob->id;
|
||||||
resp->iSkillID = skillID;
|
resp->iSkillID = skillID;
|
||||||
resp->iStyle = style;
|
resp->iStyle = mobStyle;
|
||||||
resp->iValue1 = plr->x;
|
resp->iValue1 = plr->x;
|
||||||
resp->iValue2 = plr->y;
|
resp->iValue2 = plr->y;
|
||||||
resp->iValue3 = plr->z;
|
resp->iValue3 = plr->z;
|
||||||
@@ -280,26 +280,38 @@ static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, i
|
|||||||
respdata[i].iActiveNanoSlotNum = n;
|
respdata[i].iActiveNanoSlotNum = n;
|
||||||
respdata[i].iNanoID = plr->activeNano;
|
respdata[i].iNanoID = plr->activeNano;
|
||||||
|
|
||||||
int style2 = Nanos::nanoStyle(plr->activeNano);
|
int nanoStyle = Nanos::nanoStyle(plr->activeNano);
|
||||||
if (style2 == -1) { // no nano
|
if (nanoStyle == -1) { // no nano
|
||||||
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
||||||
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
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].iHitFlag = HF_BIT_STYLE_TIE;
|
||||||
respdata[i].iDamage = 0;
|
respdata[i].iDamage = 0;
|
||||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
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].iHitFlag = HF_BIT_STYLE_WIN;
|
||||||
respdata[i].iDamage = 0;
|
respdata[i].iDamage = 0;
|
||||||
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
|
||||||
if (plr->Nanos[plr->activeNano].iStamina > 150)
|
if (plr->Nanos[plr->activeNano].iStamina > 150)
|
||||||
respdata[i].iNanoStamina = 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
|
// fire damage power disguised as a corruption attack back at the enemy
|
||||||
// TODO ABILITIES
|
SkillData skill = {
|
||||||
/*std::vector<int> targetData2 = {1, mob->id, 0, 0, 0};
|
SkillType::DAMAGE, // skillType
|
||||||
for (auto& pwr : Abilities::Powers)
|
SkillEffectTarget::POINT, // effectTarget
|
||||||
if (pwr.skillType == EST_DAMAGE)
|
1, // effectType
|
||||||
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);*/
|
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 {
|
} else {
|
||||||
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
|
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
|
||||||
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
respdata[i].iDamage = Abilities::SkillTable[skillID].values[0][0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ struct Mob : public CombatNPC {
|
|||||||
// temporary; until we're sure what's what
|
// temporary; until we're sure what's what
|
||||||
nlohmann::json data = {};
|
nlohmann::json data = {};
|
||||||
|
|
||||||
Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
Mob(int spawnX, int spawnY, int spawnZ, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id)
|
||||||
: CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]),
|
: CombatNPC(spawnX, spawnY, spawnZ, angle, iID, t, id, d["m_iHP"]),
|
||||||
sightRange(d["m_iSightRange"]) {
|
sightRange(d["m_iSightRange"]) {
|
||||||
state = AIState::ROAMING;
|
state = AIState::ROAMING;
|
||||||
|
|
||||||
@@ -62,9 +62,9 @@ struct Mob : public CombatNPC {
|
|||||||
idleRange = (int)data["m_iIdleRange"];
|
idleRange = (int)data["m_iIdleRange"];
|
||||||
level = data["m_iNpcLevel"];
|
level = data["m_iNpcLevel"];
|
||||||
|
|
||||||
roamX = x;
|
roamX = spawnX;
|
||||||
roamY = y;
|
roamY = spawnY;
|
||||||
roamZ = z;
|
roamZ = spawnZ;
|
||||||
|
|
||||||
offsetX = 0;
|
offsetX = 0;
|
||||||
offsetY = 0;
|
offsetY = 0;
|
||||||
|
|||||||
@@ -122,16 +122,15 @@ static void npcUnsummonHandler(CNSocket* sock, CNPacketData* data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// type must already be checked and updateNPCPosition() must be called on the result
|
// 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;
|
uint64_t inst = baseInstance ? MAPNUM(instance) : instance;
|
||||||
|
|
||||||
//assert(nextId < INT32_MAX);
|
|
||||||
int id = nextId--;
|
int id = nextId--;
|
||||||
int team = NPCData[type]["m_iTeam"];
|
int team = NPCData[type]["m_iTeam"];
|
||||||
BaseNPC *npc = nullptr;
|
BaseNPC *npc = nullptr;
|
||||||
|
|
||||||
if (team == 2) {
|
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
|
// re-enable respawning, if desired
|
||||||
((Mob*)npc)->summoned = !respawn;
|
((Mob*)npc)->summoned = !respawn;
|
||||||
@@ -294,57 +293,55 @@ BaseNPC* NPCManager::getNearestNPC(std::set<Chunk*>* chunks, int X, int Y, int Z
|
|||||||
return npc;
|
return npc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to MobAI, possibly
|
// TODO: Move this to separate file in ai/ subdir when implementing more events
|
||||||
#pragma region NPCEvents
|
#pragma region NPCEvents
|
||||||
|
|
||||||
// summon right arm and stage 2 body
|
// 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
|
Mob *oldbody = (Mob*)npc; // adaptium, stun
|
||||||
Player *plr = PlayerManager::getPlayer(sock);
|
|
||||||
|
|
||||||
std::cout << "Lord Fuse stage two" << std::endl;
|
std::cout << "Lord Fuse stage two" << std::endl;
|
||||||
|
|
||||||
// Fuse doesn't move
|
// Fuse doesn't move
|
||||||
// Blastons, Heal
|
// 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;
|
newbody->angle = oldbody->angle;
|
||||||
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
|
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||||
plr->instanceID, oldbody->angle);
|
oldbody->instanceID, oldbody->angle);
|
||||||
|
|
||||||
// right arm, Adaptium, Stun
|
// 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;
|
arm->angle = oldbody->angle;
|
||||||
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
|
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||||
plr->instanceID, oldbody->angle);
|
oldbody->instanceID, oldbody->angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// summon left arm and stage 3 body
|
// summon left arm and stage 3 body
|
||||||
static void lordFuseStageThree(CNSocket *sock, BaseNPC *npc) {
|
static void lordFuseStageThree(CombatNPC *npc) {
|
||||||
Mob *oldbody = (Mob*)npc;
|
Mob *oldbody = (Mob*)npc;
|
||||||
Player *plr = PlayerManager::getPlayer(sock);
|
|
||||||
|
|
||||||
std::cout << "Lord Fuse stage three" << std::endl;
|
std::cout << "Lord Fuse stage three" << std::endl;
|
||||||
|
|
||||||
// Cosmix, Damage Point
|
// 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;
|
newbody->angle = oldbody->angle;
|
||||||
NPCManager::updateNPCPosition(newbody->id, newbody->x, newbody->y, newbody->z,
|
NPCManager::updateNPCPosition(newbody->id, newbody->spawnX, newbody->spawnY, newbody->spawnZ,
|
||||||
plr->instanceID, oldbody->angle);
|
newbody->instanceID, oldbody->angle);
|
||||||
|
|
||||||
// Blastons, Heal
|
// 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;
|
arm->angle = oldbody->angle;
|
||||||
NPCManager::updateNPCPosition(arm->id, arm->x, arm->y, arm->z,
|
NPCManager::updateNPCPosition(arm->id, arm->spawnX, arm->spawnY, arm->spawnZ,
|
||||||
plr->instanceID, oldbody->angle);
|
arm->instanceID, oldbody->angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<NPCEvent> NPCManager::NPCEvents = {
|
std::vector<NPCEvent> NPCManager::NPCEvents = {
|
||||||
NPCEvent(2466, ON_KILLED, lordFuseStageTwo),
|
NPCEvent(2466, AIState::DEAD, lordFuseStageTwo),
|
||||||
NPCEvent(2467, ON_KILLED, lordFuseStageThree),
|
NPCEvent(2467, AIState::DEAD, lordFuseStageThree),
|
||||||
};
|
};
|
||||||
|
|
||||||
#pragma endregion NPCEvents
|
#pragma endregion NPCEvents
|
||||||
|
|||||||
@@ -14,20 +14,15 @@
|
|||||||
|
|
||||||
#define RESURRECT_HEIGHT 400
|
#define RESURRECT_HEIGHT 400
|
||||||
|
|
||||||
enum Trigger {
|
typedef void (*NPCEventHandler)(CombatNPC*);
|
||||||
ON_KILLED,
|
|
||||||
ON_COMBAT
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef void (*NPCEventHandler)(CNSocket*, BaseNPC*);
|
|
||||||
|
|
||||||
struct NPCEvent {
|
struct NPCEvent {
|
||||||
int32_t npcType;
|
int32_t npcType;
|
||||||
int trigger;
|
AIState triggerState;
|
||||||
NPCEventHandler handler;
|
NPCEventHandler handler;
|
||||||
|
|
||||||
NPCEvent(int32_t t, int tr, NPCEventHandler hndlr)
|
NPCEvent(int32_t t, AIState tr, NPCEventHandler hndlr)
|
||||||
: npcType(t), trigger(tr), handler(hndlr) {}
|
: npcType(t), triggerState(tr), handler(hndlr) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace NPCManager {
|
namespace NPCManager {
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ void PlayerManager::updatePlayerPosition(CNSocket* sock, int X, int Y, int Z, ui
|
|||||||
plr->x = X;
|
plr->x = X;
|
||||||
plr->y = Y;
|
plr->y = Y;
|
||||||
plr->z = Z;
|
plr->z = Z;
|
||||||
plr->instanceID = I;
|
if (plr->instanceID != I) {
|
||||||
|
plr->instanceID = I;
|
||||||
|
plr->recallInstance = INSTANCE_OVERWORLD;
|
||||||
|
}
|
||||||
if (oldChunk == newChunk)
|
if (oldChunk == newChunk)
|
||||||
return; // didn't change chunks
|
return; // didn't change chunks
|
||||||
Chunking::updateEntityChunk({sock}, oldChunk, newChunk);
|
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);
|
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);
|
INITSTRUCT(sP_FE2CL_REP_PC_GOTO_SUCC, pkt2);
|
||||||
pkt2.iX = X;
|
pkt2.iX = X;
|
||||||
pkt2.iY = Y;
|
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);
|
updatePlayerPosition(sock, plr->x, plr->y, plr->z, plr->instanceID, plr->angle);
|
||||||
|
|
||||||
sock->sendPacket(response, P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC);
|
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) {
|
static void heartbeatPlayer(CNSocket* sock, CNPacketData* data) {
|
||||||
@@ -416,6 +419,7 @@ static void revivePlayer(CNSocket* sock, CNPacketData* data) {
|
|||||||
|
|
||||||
default: // plain respawn
|
default: // plain respawn
|
||||||
plr->HP = PC_MAXHEALTH(plr->level) / 2;
|
plr->HP = PC_MAXHEALTH(plr->level) / 2;
|
||||||
|
plr->clearBuffs(false);
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case ePCRegenType::Unstick: // warp away
|
case ePCRegenType::Unstick: // warp away
|
||||||
move = true;
|
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;
|
std::cout << "[WARN] Client submitted invalid first use flag number?!" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag->iFlagCode <= 64)
|
if (flag->iFlagCode <= 64)
|
||||||
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
|
plr->iFirstUseFlag[0] |= (1ULL << (flag->iFlagCode - 1));
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ static void racingCancel(CNSocket* sock, CNPacketData* data) {
|
|||||||
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
|
INITSTRUCT(sP_FE2CL_REP_EP_RACE_CANCEL_SUCC, resp);
|
||||||
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
|
sock->sendPacket(resp, P_FE2CL_REP_EP_RACE_CANCEL_SUCC);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This request packet is used for both cancelling the race via the
|
* 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.
|
* 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
|
* 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)
|
if (EPData.find(mapNum) == EPData.end() || EPData[mapNum].EPID == 0)
|
||||||
return; // IZ not found
|
return; // IZ not found
|
||||||
|
|
||||||
uint64_t now = getTime() / 1000;
|
EPInfo& epInfo = EPData[mapNum];
|
||||||
|
EPRace& epRace = EPRaces[sock];
|
||||||
|
|
||||||
int timeDiff = now - EPRaces[sock].startTime;
|
uint64_t now = getTime() / 1000;
|
||||||
int score = 500 * EPRaces[sock].collectedRings.size() - 10 * timeDiff;
|
int timeDiff = now - epRace.startTime;
|
||||||
if (score < 0) score = 0; // lol
|
int podsCollected = epRace.collectedRings.size();
|
||||||
int fm = score * plr->level * (1.0f / 36) * 0.3f;
|
|
||||||
|
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...
|
// we submit the ranking first...
|
||||||
Database::RaceRanking postRanking = {};
|
Database::RaceRanking postRanking = {};
|
||||||
postRanking.EPID = EPData[mapNum].EPID;
|
postRanking.EPID = epInfo.EPID;
|
||||||
postRanking.PlayerID = plr->iID;
|
postRanking.PlayerID = plr->iID;
|
||||||
postRanking.RingCount = EPRaces[sock].collectedRings.size();
|
postRanking.RingCount = podsCollected;
|
||||||
postRanking.Score = score;
|
postRanking.Score = score;
|
||||||
postRanking.Time = timeDiff;
|
postRanking.Time = timeDiff;
|
||||||
postRanking.Timestamp = getTimestamp();
|
postRanking.Timestamp = getTimestamp();
|
||||||
Database::postRaceRanking(postRanking);
|
Database::postRaceRanking(postRanking);
|
||||||
|
|
||||||
// ...then we get the top ranking, which may or may not be what we just submitted
|
// ...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);
|
INITSTRUCT(sP_FE2CL_REP_EP_RACE_END_SUCC, resp);
|
||||||
|
|
||||||
// get rank scores and rewards
|
// get rank scores and rewards
|
||||||
std::vector<int>* rankScores = &EPRewards[EPData[mapNum].EPID].first;
|
std::vector<int>* rankScores = &EPRewards[epInfo.EPID].first;
|
||||||
std::vector<int>* rankRewards = &EPRewards[EPData[mapNum].EPID].second;
|
std::vector<int>* rankRewards = &EPRewards[epInfo.EPID].second;
|
||||||
|
|
||||||
// top ranking
|
// top ranking
|
||||||
int topRank = 0;
|
int topRank = 0;
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
struct EPInfo {
|
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 {
|
struct EPRace {
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ static void loadPaths(json& pathData, int32_t* nextId) {
|
|||||||
Transport::NPCPaths.push_back(pathTemplate);
|
Transport::NPCPaths.push_back(pathTemplate);
|
||||||
}
|
}
|
||||||
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
|
std::cout << "[INFO] Loaded " << Transport::NPCPaths.size() << " NPC paths" << std::endl;
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (const std::exception& err) {
|
catch (const std::exception& err) {
|
||||||
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
|
std::cerr << "[FATAL] Malformed paths.json file! Reason:" << err.what() << std::endl;
|
||||||
@@ -584,8 +584,17 @@ static void loadDrops(json& dropData) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// time limit isn't stored in the XDT, so we include it in the reward table instead
|
EPInfo& epInfo = Racing::EPData[EPMap];
|
||||||
Racing::EPData[EPMap].maxTime = race["TimeLimit"];
|
|
||||||
|
// 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
|
// score cutoffs
|
||||||
std::vector<int> rankScores;
|
std::vector<int> rankScores;
|
||||||
@@ -686,7 +695,7 @@ static void loadEggs(json& eggData, int32_t* nextId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load gruntwork output, if it exists
|
* Load gruntwork output, if it exists
|
||||||
*/
|
*/
|
||||||
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
|
static void loadGruntworkPre(json& gruntwork, int32_t* nextId) {
|
||||||
@@ -1361,7 +1370,7 @@ void TableData::flush() {
|
|||||||
targetIDs.push_back(tID);
|
targetIDs.push_back(tID);
|
||||||
for (int32_t tType : path.targetTypes)
|
for (int32_t tType : path.targetTypes)
|
||||||
targetTypes.push_back(tType);
|
targetTypes.push_back(tType);
|
||||||
|
|
||||||
pathObj["iBaseSpeed"] = path.speed;
|
pathObj["iBaseSpeed"] = path.speed;
|
||||||
pathObj["iTaskID"] = path.escortTaskID;
|
pathObj["iTaskID"] = path.escortTaskID;
|
||||||
pathObj["bRelative"] = path.isRelative;
|
pathObj["bRelative"] = path.isRelative;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ void terminate(int arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
static BOOL winTerminate(DWORD arg) {
|
static BOOL WINAPI winTerminate(DWORD arg) {
|
||||||
terminate(0);
|
terminate(0);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|||||||
2
tdata
2
tdata
Submodule tdata updated: cc65dbb402...8c98c83682
Reference in New Issue
Block a user