From 62f7b1278a76adf94ccb60845bdba4b7c244b2cf Mon Sep 17 00:00:00 2001 From: Yacht Date: Mon, 24 Aug 2026 22:20:36 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + Callbacks/attack.js | 23 + Callbacks/die.js | 253 +++ Callbacks/reward.js | 183 +++ Core.js | 191 +++ Data/admiralTable.js | 35 + Data/ammunitions.js | 15 + Data/batchSave.js | 5 + Data/cannons.js | 34 + Data/cannonsSchemeNPC.js | 70 + Data/constants.js | 316 ++++ Data/designs.js | 62 + Data/effects.js | 18 + Data/eliteRewards.js | 151 ++ Data/entityGroups.js | 77 + Data/harpooners.js | 22 + Data/harpoons.js | 12 + Data/items.js | 33 + Data/leaderboard.js | 20 + Data/levelRewards.js | 162 ++ Data/levels.js | 45 + Data/loginRewards.js | 31 + Data/mapMatrix.js | 9 + Data/maps.js | 436 +++++ Data/overtime.js | 7 + Data/packetRelevancies.js | 18 + Data/packets.js | 71 + Data/payment.js | 8 + Data/queries.js | 30 + Data/quests.js | 97 ++ Data/schemeTower.js | 49 + Data/shop.js | 123 ++ Data/startRewards.js | 12 + Data/stripe.js | 16 + Events/close.js | 42 + Events/drain.js | 3 + Events/message.js | 94 ++ Events/open.js | 239 +++ Events/upgrade.js | 64 + Game/AdmiralCounter.js | 39 + Game/Cannon.js | 71 + Game/Collectable.js | 70 + Game/Entity.js | 106 ++ Game/EntityGroup.js | 96 ++ Game/EquipmentLimiter.js | 20 + Game/EventScheduler.js | 154 ++ Game/Game.js | 43 + Game/Grid.js | 125 ++ Game/Grids.js | 15 + Game/Guild.js | 143 ++ Game/GuildIsland.js | 69 + Game/Harpooner.js | 63 + Game/Inventory.js | 303 ++++ Game/InventoryList.js | 41 + Game/Item.js | 61 + Game/Map.js | 511 ++++++ Game/Maps.js | 7 + Game/Monster.js | 147 ++ Game/NPC.js | 763 +++++++++ Game/PacketLimiter.js | 21 + Game/Player.js | 1536 ++++++++++++++++++ Game/PlayerBehaviour.js | 39 + Game/Quest.js | 68 + Game/QuestHandler.js | 86 + Game/Reward.js | 134 ++ Game/Session.js | 15 + Game/Tower.js | 552 +++++++ Game/Utility.js | 11 + Handlers/Collectable.js | 54 + Handlers/Map.js | 26 + Handlers/Monster.js | 72 + Handlers/NPC.js | 78 + Handlers/NPCTest.js | 93 ++ Handlers/Player.js | 364 +++++ Handlers/Tower.js | 184 +++ Maps/Map_1.js | 5 + Maps/Map_10.js | 6 + Maps/Map_11.js | 5 + Maps/Map_12.js | 5 + Maps/Map_13.js | 6 + Maps/Map_14.js | 6 + Maps/Map_15.js | 6 + Maps/Map_16.js | 6 + Maps/Map_17.js | 5 + Maps/Map_18.js | 5 + Maps/Map_19.js | 5 + Maps/Map_2.js | 5 + Maps/Map_20.js | 5 + Maps/Map_3.js | 5 + Maps/Map_4.js | 6 + Maps/Map_5.js | 6 + Maps/Map_6.js | 5 + Maps/Map_7.js | 6 + Maps/Map_8.js | 6 + Maps/Map_9.js | 6 + Messages/AmmunitionChangeMessage.js | 34 + Messages/AttackAbortMessage.js | 26 + Messages/AttackStartMessage.js | 18 + Messages/CauldronMessage.js | 44 + Messages/CloseMessage.js | 13 + Messages/DesignChangeMessage.js | 23 + Messages/EquipmentChangeMessage.js | 75 + Messages/GuildCreateMessage.js | 79 + Messages/GuildDataMessage.js | 150 ++ Messages/GuildDetailsIdentityEditMessage.js | 102 ++ Messages/GuildDetailsTaxEditMessage.js | 30 + Messages/GuildDonateMessage.js | 58 + Messages/GuildIslandTowerBuildMessage.js | 69 + Messages/GuildIslandTowerRepairMessage.js | 18 + Messages/GuildJoinRequestRevokeMessage.js | 40 + Messages/GuildJoinRequestSubmitMessage.js | 55 + Messages/GuildMemberJoinMessage.js | 91 ++ Messages/GuildMemberLeaveMessage.js | 57 + Messages/GuildSearchMessage.js | 70 + Messages/ItemBuyMessage.js | 75 + Messages/ItemUseMessage.js | 113 ++ Messages/LeaderboardDataMessage.js | 25 + Messages/LeaderboardPageMessage.js | 42 + Messages/MapChangeMessage.js | 3 + Messages/MovementMessage.js | 44 + Messages/QuestMessage.js | 42 + Messages/ReadyMessage.js | 242 +++ Messages/RepairMessage.js | 20 + Messages/RespawnMessage.js | 5 + Messages/SpyMessage.js | 34 + Messages/TopicInterestedMessage.js | 13 + Messages/_.js | 44 + Messages/message.js | 94 ++ Miscellaneous/mysql.js | 54 + Miscellaneous/redis.js | 7 + Miscellaneous/stripe.js | 3 + Miscellaneous/winston.js | 26 + Packets/ActiveStatusEffects.js | 19 + Packets/AdmiralSpawned.js | 23 + Packets/AmmunitionChangeConfirmation.js | 20 + Packets/AmmunitionUsed.js | 20 + Packets/AttackAborted.js | 22 + Packets/AttackInformation.js | 20 + Packets/AttackerInformation.js | 22 + Packets/Attacking.js | 25 + Packets/Cannonballs.js | 30 + Packets/CauldronMessage.js | 40 + Packets/DesignChanged.js | 23 + Packets/Economy.js | 24 + Packets/EffectAdd.js | 23 + Packets/EntityDie.js | 23 + Packets/EntityNew.js | 214 +++ Packets/EntityOld.js | 22 + Packets/EntitySpeedChanged.js | 21 + Packets/EnvironmentSetup.js | 17 + Packets/EquipmentCapacitiesMaximum.js | 22 + Packets/EquipmentConfirmation.js | 19 + Packets/Equipments.js | 36 + Packets/GuildDataApplicant.js | 27 + Packets/GuildDataApplicantRemove.js | 19 + Packets/GuildDataCreationFeedback.js | 19 + Packets/GuildDataEconomy.js | 20 + Packets/GuildDataEconomyDonated.js | 20 + Packets/GuildDataGeneral.js | 44 + Packets/GuildDataIdentityModified.js | 35 + Packets/GuildDataIsland.js | 22 + Packets/GuildDataIslandCaptured.js | 19 + Packets/GuildDataIslandTower.js | 24 + Packets/GuildDataIslandTowerBuilt.js | 21 + Packets/GuildDataIslandTowerDestroyed.js | 21 + Packets/GuildDataIslandTowerHealthChanged.js | 22 + Packets/GuildDataMember.js | 29 + Packets/GuildDataMemberChanged.js | 60 + Packets/GuildDataMemberRemoved.js | 19 + Packets/GuildDataTaxRatesModified.js | 20 + Packets/GuildIslandCaptured.js | 11 + Packets/GuildLeaveConfirmation.js | 19 + Packets/GuildMemberNew.js | 20 + Packets/GuildMemberOld.js | 28 + Packets/GuildSearchResult.js | 34 + Packets/GuildSearchResultStateChanged.js | 20 + Packets/Harpoons.js | 30 + Packets/HealthChanged.js | 25 + Packets/HealthChangedAuthorless.js | 23 + Packets/ItemBought.js | 21 + Packets/ItemUsed.js | 22 + Packets/Items.js | 23 + Packets/LeaderboardPage.js | 41 + Packets/LeaderboardPositions.js | 23 + Packets/LevelIsLow.js | 18 + Packets/LevelUp.js | 23 + Packets/LogoutCancelled.js | 18 + Packets/MainPlayer.js | 52 + Packets/MaintenanceScheduled.js | 20 + Packets/MapChangeState.js | 20 + Packets/MapChanged.js | 23 + Packets/MaximumCannonsSize.js | 19 + Packets/MiniMapEntryMove.js | 21 + Packets/MiniMapEntryRemove.js | 19 + Packets/MojoRemoved.js | 19 + Packets/NewDestinationNode.js | 22 + Packets/ObtainedDesigns.js | 20 + Packets/PropertyChanged.js | 20 + Packets/PropertyChangedGeneral.js | 22 + Packets/QuestAdded.js | 21 + Packets/QuestRemoval.js | 22 + Packets/Quests.js | 35 + Packets/QuestsCompletedAmount.js | 24 + Packets/RankPoint.js | 20 + Packets/RankThresholdPoints.js | 24 + Packets/RepairConfirmation.js | 19 + Packets/RespawnConfirmation.js | 18 + Packets/Reward.js | 66 + Packets/ShopEntry.js | 24 + Packets/ShopEntryRemoved.js | 20 + Packets/SpyResult.js | 23 + Packets/StatusPoints.js | 27 + Packets/WorldMapGuildEntry.js | 23 + Packets/packet.js | 55 + Packets/packet_255.js | 22 + Packets/packet_57.js | 20 + Packets/packet_58.js | 20 + Packets/packet_61.js | 40 + Packets/packet_62.js | 22 + Packets/packets.js | 65 + Routes/Assets.js | 29 + Routes/Auth/Logout.js | 33 + Routes/Game/Game.js | 56 + Routes/Routes.js | 29 + Routes/api/Game/Shutdown.js | 40 + Routes/api/Game/State.js | 20 + Routes/api/Payment/payment_intent.js | 73 + Routes/api/Payment/webhook.js | 75 + Routes/api/Player/Kick.js | 23 + Routes/api/Shop/Add.js | 65 + Routes/api/Shop/Remove.js | 38 + Timers/Behaviour.js | 49 + Timers/Leaderboard.js | 49 + Timers/Resource.js | 29 + Utility/DoubleLinkedList.js | 134 ++ Utility/LinkedList.js | 73 + Utility/PriorityQueue.js | 30 + Utility/Q.js | 273 ++++ Utility/Router.js | 71 + Utility/chooseWeightedRandom.js | 15 + Utility/extractBehaviourFeatures.js | 76 + Utility/getFixedDate.js | 5 + Utility/isAuthenticated.js | 36 + Utility/setTimer.js | 131 ++ Worker/NPC.js | 80 + Worker/Player.js | 77 + Worker/Statistics.js | 89 + metrics.js | 17 + package-lock.json | 895 ++++++++++ package.json | 14 + server.js | 325 ++++ 251 files changed, 16670 insertions(+) create mode 100644 .gitignore create mode 100644 Callbacks/attack.js create mode 100644 Callbacks/die.js create mode 100644 Callbacks/reward.js create mode 100644 Core.js create mode 100644 Data/admiralTable.js create mode 100644 Data/ammunitions.js create mode 100644 Data/batchSave.js create mode 100644 Data/cannons.js create mode 100644 Data/cannonsSchemeNPC.js create mode 100644 Data/constants.js create mode 100644 Data/designs.js create mode 100644 Data/effects.js create mode 100644 Data/eliteRewards.js create mode 100644 Data/entityGroups.js create mode 100644 Data/harpooners.js create mode 100644 Data/harpoons.js create mode 100644 Data/items.js create mode 100644 Data/leaderboard.js create mode 100644 Data/levelRewards.js create mode 100644 Data/levels.js create mode 100644 Data/loginRewards.js create mode 100644 Data/mapMatrix.js create mode 100644 Data/maps.js create mode 100644 Data/overtime.js create mode 100644 Data/packetRelevancies.js create mode 100644 Data/packets.js create mode 100644 Data/payment.js create mode 100644 Data/queries.js create mode 100644 Data/quests.js create mode 100644 Data/schemeTower.js create mode 100644 Data/shop.js create mode 100644 Data/startRewards.js create mode 100644 Data/stripe.js create mode 100644 Events/close.js create mode 100644 Events/drain.js create mode 100644 Events/message.js create mode 100644 Events/open.js create mode 100644 Events/upgrade.js create mode 100644 Game/AdmiralCounter.js create mode 100644 Game/Cannon.js create mode 100644 Game/Collectable.js create mode 100644 Game/Entity.js create mode 100644 Game/EntityGroup.js create mode 100644 Game/EquipmentLimiter.js create mode 100644 Game/EventScheduler.js create mode 100644 Game/Game.js create mode 100644 Game/Grid.js create mode 100644 Game/Grids.js create mode 100644 Game/Guild.js create mode 100644 Game/GuildIsland.js create mode 100644 Game/Harpooner.js create mode 100644 Game/Inventory.js create mode 100644 Game/InventoryList.js create mode 100644 Game/Item.js create mode 100644 Game/Map.js create mode 100644 Game/Maps.js create mode 100644 Game/Monster.js create mode 100644 Game/NPC.js create mode 100644 Game/PacketLimiter.js create mode 100644 Game/Player.js create mode 100644 Game/PlayerBehaviour.js create mode 100644 Game/Quest.js create mode 100644 Game/QuestHandler.js create mode 100644 Game/Reward.js create mode 100644 Game/Session.js create mode 100644 Game/Tower.js create mode 100644 Game/Utility.js create mode 100644 Handlers/Collectable.js create mode 100644 Handlers/Map.js create mode 100644 Handlers/Monster.js create mode 100644 Handlers/NPC.js create mode 100644 Handlers/NPCTest.js create mode 100644 Handlers/Player.js create mode 100644 Handlers/Tower.js create mode 100644 Maps/Map_1.js create mode 100644 Maps/Map_10.js create mode 100644 Maps/Map_11.js create mode 100644 Maps/Map_12.js create mode 100644 Maps/Map_13.js create mode 100644 Maps/Map_14.js create mode 100644 Maps/Map_15.js create mode 100644 Maps/Map_16.js create mode 100644 Maps/Map_17.js create mode 100644 Maps/Map_18.js create mode 100644 Maps/Map_19.js create mode 100644 Maps/Map_2.js create mode 100644 Maps/Map_20.js create mode 100644 Maps/Map_3.js create mode 100644 Maps/Map_4.js create mode 100644 Maps/Map_5.js create mode 100644 Maps/Map_6.js create mode 100644 Maps/Map_7.js create mode 100644 Maps/Map_8.js create mode 100644 Maps/Map_9.js create mode 100644 Messages/AmmunitionChangeMessage.js create mode 100644 Messages/AttackAbortMessage.js create mode 100644 Messages/AttackStartMessage.js create mode 100644 Messages/CauldronMessage.js create mode 100644 Messages/CloseMessage.js create mode 100644 Messages/DesignChangeMessage.js create mode 100644 Messages/EquipmentChangeMessage.js create mode 100644 Messages/GuildCreateMessage.js create mode 100644 Messages/GuildDataMessage.js create mode 100644 Messages/GuildDetailsIdentityEditMessage.js create mode 100644 Messages/GuildDetailsTaxEditMessage.js create mode 100644 Messages/GuildDonateMessage.js create mode 100644 Messages/GuildIslandTowerBuildMessage.js create mode 100644 Messages/GuildIslandTowerRepairMessage.js create mode 100644 Messages/GuildJoinRequestRevokeMessage.js create mode 100644 Messages/GuildJoinRequestSubmitMessage.js create mode 100644 Messages/GuildMemberJoinMessage.js create mode 100644 Messages/GuildMemberLeaveMessage.js create mode 100644 Messages/GuildSearchMessage.js create mode 100644 Messages/ItemBuyMessage.js create mode 100644 Messages/ItemUseMessage.js create mode 100644 Messages/LeaderboardDataMessage.js create mode 100644 Messages/LeaderboardPageMessage.js create mode 100644 Messages/MapChangeMessage.js create mode 100644 Messages/MovementMessage.js create mode 100644 Messages/QuestMessage.js create mode 100644 Messages/ReadyMessage.js create mode 100644 Messages/RepairMessage.js create mode 100644 Messages/RespawnMessage.js create mode 100644 Messages/SpyMessage.js create mode 100644 Messages/TopicInterestedMessage.js create mode 100644 Messages/_.js create mode 100644 Messages/message.js create mode 100644 Miscellaneous/mysql.js create mode 100644 Miscellaneous/redis.js create mode 100644 Miscellaneous/stripe.js create mode 100644 Miscellaneous/winston.js create mode 100644 Packets/ActiveStatusEffects.js create mode 100644 Packets/AdmiralSpawned.js create mode 100644 Packets/AmmunitionChangeConfirmation.js create mode 100644 Packets/AmmunitionUsed.js create mode 100644 Packets/AttackAborted.js create mode 100644 Packets/AttackInformation.js create mode 100644 Packets/AttackerInformation.js create mode 100644 Packets/Attacking.js create mode 100644 Packets/Cannonballs.js create mode 100644 Packets/CauldronMessage.js create mode 100644 Packets/DesignChanged.js create mode 100644 Packets/Economy.js create mode 100644 Packets/EffectAdd.js create mode 100644 Packets/EntityDie.js create mode 100644 Packets/EntityNew.js create mode 100644 Packets/EntityOld.js create mode 100644 Packets/EntitySpeedChanged.js create mode 100644 Packets/EnvironmentSetup.js create mode 100644 Packets/EquipmentCapacitiesMaximum.js create mode 100644 Packets/EquipmentConfirmation.js create mode 100644 Packets/Equipments.js create mode 100644 Packets/GuildDataApplicant.js create mode 100644 Packets/GuildDataApplicantRemove.js create mode 100644 Packets/GuildDataCreationFeedback.js create mode 100644 Packets/GuildDataEconomy.js create mode 100644 Packets/GuildDataEconomyDonated.js create mode 100644 Packets/GuildDataGeneral.js create mode 100644 Packets/GuildDataIdentityModified.js create mode 100644 Packets/GuildDataIsland.js create mode 100644 Packets/GuildDataIslandCaptured.js create mode 100644 Packets/GuildDataIslandTower.js create mode 100644 Packets/GuildDataIslandTowerBuilt.js create mode 100644 Packets/GuildDataIslandTowerDestroyed.js create mode 100644 Packets/GuildDataIslandTowerHealthChanged.js create mode 100644 Packets/GuildDataMember.js create mode 100644 Packets/GuildDataMemberChanged.js create mode 100644 Packets/GuildDataMemberRemoved.js create mode 100644 Packets/GuildDataTaxRatesModified.js create mode 100644 Packets/GuildIslandCaptured.js create mode 100644 Packets/GuildLeaveConfirmation.js create mode 100644 Packets/GuildMemberNew.js create mode 100644 Packets/GuildMemberOld.js create mode 100644 Packets/GuildSearchResult.js create mode 100644 Packets/GuildSearchResultStateChanged.js create mode 100644 Packets/Harpoons.js create mode 100644 Packets/HealthChanged.js create mode 100644 Packets/HealthChangedAuthorless.js create mode 100644 Packets/ItemBought.js create mode 100644 Packets/ItemUsed.js create mode 100644 Packets/Items.js create mode 100644 Packets/LeaderboardPage.js create mode 100644 Packets/LeaderboardPositions.js create mode 100644 Packets/LevelIsLow.js create mode 100644 Packets/LevelUp.js create mode 100644 Packets/LogoutCancelled.js create mode 100644 Packets/MainPlayer.js create mode 100644 Packets/MaintenanceScheduled.js create mode 100644 Packets/MapChangeState.js create mode 100644 Packets/MapChanged.js create mode 100644 Packets/MaximumCannonsSize.js create mode 100644 Packets/MiniMapEntryMove.js create mode 100644 Packets/MiniMapEntryRemove.js create mode 100644 Packets/MojoRemoved.js create mode 100644 Packets/NewDestinationNode.js create mode 100644 Packets/ObtainedDesigns.js create mode 100644 Packets/PropertyChanged.js create mode 100644 Packets/PropertyChangedGeneral.js create mode 100644 Packets/QuestAdded.js create mode 100644 Packets/QuestRemoval.js create mode 100644 Packets/Quests.js create mode 100644 Packets/QuestsCompletedAmount.js create mode 100644 Packets/RankPoint.js create mode 100644 Packets/RankThresholdPoints.js create mode 100644 Packets/RepairConfirmation.js create mode 100644 Packets/RespawnConfirmation.js create mode 100644 Packets/Reward.js create mode 100644 Packets/ShopEntry.js create mode 100644 Packets/ShopEntryRemoved.js create mode 100644 Packets/SpyResult.js create mode 100644 Packets/StatusPoints.js create mode 100644 Packets/WorldMapGuildEntry.js create mode 100644 Packets/packet.js create mode 100644 Packets/packet_255.js create mode 100644 Packets/packet_57.js create mode 100644 Packets/packet_58.js create mode 100644 Packets/packet_61.js create mode 100644 Packets/packet_62.js create mode 100644 Packets/packets.js create mode 100644 Routes/Assets.js create mode 100644 Routes/Auth/Logout.js create mode 100644 Routes/Game/Game.js create mode 100644 Routes/Routes.js create mode 100644 Routes/api/Game/Shutdown.js create mode 100644 Routes/api/Game/State.js create mode 100644 Routes/api/Payment/payment_intent.js create mode 100644 Routes/api/Payment/webhook.js create mode 100644 Routes/api/Player/Kick.js create mode 100644 Routes/api/Shop/Add.js create mode 100644 Routes/api/Shop/Remove.js create mode 100644 Timers/Behaviour.js create mode 100644 Timers/Leaderboard.js create mode 100644 Timers/Resource.js create mode 100644 Utility/DoubleLinkedList.js create mode 100644 Utility/LinkedList.js create mode 100644 Utility/PriorityQueue.js create mode 100644 Utility/Q.js create mode 100644 Utility/Router.js create mode 100644 Utility/chooseWeightedRandom.js create mode 100644 Utility/extractBehaviourFeatures.js create mode 100644 Utility/getFixedDate.js create mode 100644 Utility/isAuthenticated.js create mode 100644 Utility/setTimer.js create mode 100644 Worker/NPC.js create mode 100644 Worker/Player.js create mode 100644 Worker/Statistics.js create mode 100644 metrics.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8866fb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +.env.* +*.log +Logs/ \ No newline at end of file diff --git a/Callbacks/attack.js b/Callbacks/attack.js new file mode 100644 index 0000000..a00a1ee --- /dev/null +++ b/Callbacks/attack.js @@ -0,0 +1,23 @@ +const { TASK_TYPES } = require("../Data/constants"); +const { TASK_TYPE_DEAL_DAMAGE } = TASK_TYPES; + +const Game = require("../Game/Game"); +const { players } = Game; + +function globalAttackCallback(data) { + const { typeID, entityTypeID, authorID, damage } = data; + + const player = players[authorID]; + if (!player) return; + + /* + * only quest is updated here + * if player is unavailable, it means that they have logged out: damage without player being online??? + * conclusion: only overtime damage can cause this issue + */ + + const toBeAdjusted = [{ taskCategoryID: TASK_TYPE_DEAL_DAMAGE, typeID, entityTypeID }]; + Object.values(player.quests.list).forEach(quest => quest.adjustTask(toBeAdjusted, damage)); +}; + +module.exports = globalAttackCallback; \ No newline at end of file diff --git a/Callbacks/die.js b/Callbacks/die.js new file mode 100644 index 0000000..6208617 --- /dev/null +++ b/Callbacks/die.js @@ -0,0 +1,253 @@ +const Reward = require("../Game/Reward"); + +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const admiralTable = require("../Data/admiralTable"); + +const leaderboards = require("../Data/leaderboard"); +const { TASK_TYPES, TYPES } = require("../Data/constants"); + +const rewardCallback = require("./reward"); + + +const { TASK_TYPE_DESTROY } = TASK_TYPES; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_MONSTER } = TYPES; + +const Game = require("../Game/Game"); +const { players } = Game; + +const percentages = [0.40, 0.25, 0.15, 0.10, 0.07, 0.03]; +const amounts = [1, 3, 5, 10, 20, 25, 35]; +const totalAmount = amounts.reduce((acc, cur) => acc + cur, 0); + +let aa = new Array(amounts.length); +setTimeout(async () => { + const q = await execute("SELECT * FROM leaderboard_1 ORDER BY amount DESC LIMIT 1099"); + const batches = []; + + const values = q.data; + let totalSize = 0; + + for (let i = 0; i < percentages.length; i++) { // this loop is for slicing based on percentages + if (values.length - totalAmount < 0) { + batches.push([]); + continue; + }; + + const batchSize = (values.length - totalAmount) * percentages[i]; + const batch = values.slice(totalSize, totalSize + batchSize); + batches.push(batch); + + totalSize += batchSize; + }; + + let rest = values.slice(totalSize, totalSize + totalAmount); + //require("../Miscellaneous/winston").error(`Total length: ${values.length} (these are from DB, raw)`) + + // require("../Miscellaneous/winston").error(`Rest length: ${rest.length} (these are based on fixed amounts instead of percentages)`) + + let x = 0; + + for (let i = 0; i < amounts.length; i++) { + const batchSize = amounts[i]; + const batch = rest.slice(totalAmount - (totalAmount - x), totalAmount - (totalAmount - x - batchSize)); + // require("../Miscellaneous/winston").error(`amounts array index: ${i} | slicing between: ${totalAmount - (totalAmount - x)} - ${totalAmount - (totalAmount - x - batchSize)}`) + aa[amounts.length - 1 - i] = batch; + + x += batchSize; + }; + + batches.push(...aa); + + const thresholds = new Array(batches.length); + + for (let j = 0; j < batches.length; j++) { + const batch = batches[j]; + const lastElemWithValue = batch.filter(e => e.id); + // require("../Miscellaneous/winston").error(`Batch ${j}: length is ${batch.length}, last elem with value: ${lastElemWithValue[lastElemWithValue.length - 1]?.id} ${lastElemWithValue[lastElemWithValue.length - 1]?.amount}`) + + thresholds[j] = lastElemWithValue[lastElemWithValue.length - 1] ? lastElemWithValue[lastElemWithValue.length - 1].amount : 0; + + for (let k = 0; k < batch.length; k++) + db.execute("UPDATE playerdata SET globalrank = ? WHERE playerID = ?", [j, batch[k].id]); + }; + + global.thresholds = thresholds; + + // require("../Miscellaneous/winston").error(thresholds); +}, 2000) + +const tracker = new DoubleLinkedList(); + +function globalDieCallback(source) { + const { typeID, entityTypeID, parentEntityTypeID, isAdmiral, authorID, authorTypeID } = source; + const id = `${typeID}_${isAdmiral ? entityTypeID : parentEntityTypeID}`; + const ids = { + typeID, + parentEntityTypeID + }; + console.trace("death", source); + const player = players[authorID]; + + switch (typeID) { + case 1: + leaderboards[4].updatePosition(authorID, 1); + break; + case 2: + leaderboards[5].updatePosition(authorID, 1); + break; + case 3: + leaderboards[6].updatePosition(authorID, 1); + break; + }; + + if (authorTypeID === ENTITY_TYPE_PLAYER && typeID === ENTITY_TYPE_PLAYER) { + const targetPlayer = players[source.id]; + + const keyAuthor = `${player.id}-${targetPlayer.id}`; + const killCountAuthor = tracker.getElement(keyAuthor); + if (killCountAuthor === 2) return console.info("reached threshold"); + + const keyTarget = `${targetPlayer.id}-${player.id}`; + const killCountTarget = tracker.getElement(keyTarget); + if (killCountTarget) tracker.accumulateItem(keyTarget, -1); + + tracker.accumulateItem(keyAuthor, 1); + + const amount = 10; + + const reward = new Reward({ + distributionType: 3, + baseReward: [ + { + categoryID: 6, + id: 4, + amountMinimum: amount + } + ] + }); + + reward.dynamic(rewards => rewardCallback({ isRegular: true, authorID, source, rewards })); + + const battlePointsTarget = targetPlayer.statusPoints[3] && targetPlayer.statusPoints[3].amount; + console.info(battlePointsTarget, amount, "sp") + if (battlePointsTarget >= amount) { + const reward_2 = new Reward({ + distributionType: 3, + baseReward: [ + { + categoryID: 6, + id: 4, + amountMinimum: amount * -1 + } + ] + }); + + reward_2.dynamic(rewards => rewardCallback({ isRegular: true, authorID: source.id, source, rewards })); + }; + }; + + if (typeID === ENTITY_TYPE_NPC) { + + } else if (typeID === ENTITY_TYPE_MONSTER) { + + }; + + const toBeAdjusted = [{ + taskCategoryID: TASK_TYPE_DESTROY, + typeID, + entityTypeID + }]; + + for (questID in player.quests.list) player.quests.list[questID].adjustTask(toBeAdjusted, 1); + + if (this.reward) { + const playerInventory = player.inventory.getPosessions(); + + switch (this.reward.distributionType) { + case 1: + this.reward.multiple({ + playerInventory, + damage: this.damageReceived, + maximum: this.totalDamageReceived, + lastShotAuthorID: authorID + }, (ids, rewards) => this.rewardCallback({ isRegular: true, authorID: ids.id, source, rewards })); + break; + + case 2: + this.reward.single(1, playerInventory, rewards => this.rewardCallback({ isRegular: true, authorID, source, rewards })); + break; + }; + }; + + const playerInventory = player.inventory.getPosessions(); + + if (isAdmiral) return admiralTable.descreaseAdmiralCount(id); + + const isAdmiralSpawnReady = parentEntityTypeID ? admiralTable.increaseCounter(ids) : false; + return isAdmiralSpawnReady; +}; + +module.exports = globalDieCallback; + + //const globalPoints = Math.floor(player.statusPoints[0] / 10000 + player.statusPoints[1] / 2000 + player.statusPoints[2] / 2000); + //if (globalPoints) leaderboards[0].updatePosition(authorID, globalPoints, true); +/* + +let aa = new Array(amounts.length); +setTimeout(async () => { + const q = await execute("SELECT * FROM leaderboard_1 ORDER BY amount DESC"); + const batches = []; + + const values = q.data; + let totalSize = 0; + + for (let i = 0; i < percentages.length; i++) { // this loop is for slicing based on percentages + if (values.length - totalAmount < 0) { + batches.push([]); + continue; + }; + + const batchSize = (values.length - totalAmount) * percentages[i]; + const batch = values.slice(totalSize, totalSize + batchSize); + batches.push(batch); + + totalSize += batchSize; + }; + + let rest = values.slice(totalSize, totalSize + totalAmount); + require("../Miscellaneous/winston").error(`Total length: ${values.length} (these are from DB, raw)`) + + require("../Miscellaneous/winston").error(`Rest length: ${rest.length} (these are based on fixed amounts instead of percentages)`) + + let x = 0; + + for (let i = 0; i < amounts.length; i++) { + const batchSize = amounts[i]; + const batch = rest.slice(totalAmount - (totalAmount - x), totalAmount - (totalAmount - x - batchSize)); + require("../Miscellaneous/winston").error(`amounts array index: ${i} | slicing between: ${totalAmount - (totalAmount - x)} - ${totalAmount - (totalAmount - x - batchSize)}`) + aa[amounts.length - 1 - i] = batch; + + x += batchSize; + }; + + batches.push(...aa); + + const thresholds = new Array(batches.length); + + for (let j = 0; j < batches.length; j++) { + const batch = batches[j]; + const lastElemWithValue = batch.filter(e => e.id); + require("../Miscellaneous/winston").error(`Batch ${j}: length is ${batch.length}, last elem with value: ${lastElemWithValue[lastElemWithValue.length - 1]?.id} ${lastElemWithValue[lastElemWithValue.length - 1]?.amount}`) + + thresholds[j] = lastElemWithValue[lastElemWithValue.length - 1] ? lastElemWithValue[lastElemWithValue.length - 1].amount : 0; + + for (let k = 0; k < batch.length; k++) + db.execute("UPDATE playerdata SET globalrank = ? WHERE playerID = ?", [j, batch[k].id]); + }; + + global.thresholds = thresholds; + + require("../Miscellaneous/winston").error(thresholds); +}, 2000) +*/ \ No newline at end of file diff --git a/Callbacks/reward.js b/Callbacks/reward.js new file mode 100644 index 0000000..865d7c0 --- /dev/null +++ b/Callbacks/reward.js @@ -0,0 +1,183 @@ +const RewardPacket = require("../Packets/Reward"); +const DesignChangedPacket = require("../Packets/DesignChanged"); +const LevelUpPacket = require("../Packets/LevelUp"); +const GuildDataEconomyDonatedPacket = require("../Packets/GuildDataEconomyDonated"); + +const StatisticsWorker = global.StatisticsWorker; + +const Game = require("../Game/Game"); +const { players } = Game; + +const constants = require("../Data/constants"); +const leaderboards = require("../Data/leaderboard"); +const eliteRewards = require("../Data/eliteRewards"); + +const [playerEconomy, guildEconomy] = require("../Data/batchSave"); + +const { CATEGORIES } = constants; +const { CATEGORY_ECONOMY, CATEGORY_DESIGN, CATEGORY_STATUS, CATEGORY_RANKING_POINTS } = CATEGORIES; + +const statusMappedToLeaderboard = { + 1: 1, + 2: 2, + 3: 3 +}; + +const EXP_GUILD_ALLOWED_SOURCES = { + 2: true, + 3: true +}; + +async function rewardCallback(reward) { + if (!reward) return; + + const player = players[reward.authorID]; + if (!player) return; + + const initializedPacket = RewardPacket.init(reward.isRegular, reward.rewards.length); + const initializedGuildPacket = RewardPacket.init(true, 1); + let designRewardID = 0; + + reward.rewards.forEach(r => { + if (r.categoryID === CATEGORY_STATUS) { + if (player.guild.id && r.id === 1 && EXP_GUILD_ALLOWED_SOURCES[reward.source.typeID]) { + const newLevel = player.guild.levelUpdate(r.amount); + + guildEconomy.accumulateItem(`${player.guild.id}-${r.categoryID}-${r.id}`, r.amount); + + const guildExperienceReward = { + categoryID: 9, + id: r.id, + amount: r.amount + }; + + initializedGuildPacket.update(guildExperienceReward); + const guildRewardPacket = initializedGuildPacket.get(); + + const topic = Game.getTopic(player.guild.topic); + topic.push(guildRewardPacket); + + if (newLevel !== -1) { + const levelUp = { + level: newLevel, + statusLevelID: 4, + id: player.id + }; + + const levelUpPacket = LevelUpPacket(levelUp); + topic.push(levelUpPacket); + + /*player.emit("levelUp", { + level: newLevel, + statusLevelID: 4, + id: player.id + });*/ + }; + }; + + const leaderboardID = statusMappedToLeaderboard[r.id]; + const leaderboard = leaderboards[leaderboardID]; + if (leaderboard) leaderboard.updatePosition(player.id, r.amount); + + player.updateLevels(r.id, r.amount); + }// else if (r.categoryID === CATEGORY_RANKING_POINTS) player.updateRankingPoints(r.id, r.amount); + else player.inventory.refreshSingle(r); + + if (r.categoryID === CATEGORY_ECONOMY) { + if (player.guild.id) { + let guildTax = 0; + + switch (r.id) { + case 1: + guildTax = player.guild.taxRates[0]; + break; + + case 2: + guildTax = player.guild.taxRates[1]; + break; + }; + + const guildTaxAmount = Math.floor(r.amount * guildTax); + const playerRewardAmount = r.amount - guildTaxAmount; + console.info("guild tax reward", guildTaxAmount, r.categoryID, r.id); + player.guild.bank.depositTax({ + categoryID: r.categoryID, + id: r.id, + amount: guildTaxAmount + }); + + const guildEconomyDonated = { + id: r.id, + amount: guildTaxAmount + }; + + const guildEconomyDonatedPacket = GuildDataEconomyDonatedPacket(guildEconomyDonated); + + const topic = Game.getTopic(player.guild.topic); + topic.push(guildEconomyDonatedPacket); + + if (guildTax) guildEconomy.accumulateItem(`${player.guild.id}-${r.categoryID}-${r.id}`, guildTaxAmount); + + r.amount -= guildTaxAmount; + }; + }; + + if (r.categoryID === CATEGORY_DESIGN && !designRewardID) designRewardID = r.id; + console.info("reward amount", r.amount) + playerEconomy.accumulateItem(`${player.id}-${r.categoryID}-${r.id}`, r.amount); + initializedPacket.update(r); + + const now = new Date(); + + StatisticsWorker.postMessage({ + id: 1, + data: { + timestamp: now, + playerID: player.id, + categoryID: r.categoryID, + id: r.id, + amount: r.amount, + source: { + typeID: reward.source.typeID, + entityTypeID: reward.source.entityTypeID, + } + } + }); + }); + + const packet = initializedPacket.get(); + player.map.registerIndividualNetworkPacket(player.id, packet); + + if (designRewardID) { + player.textureID = designRewardID; + + const designReward = { + typeID: player.typeID, + id: player.id, + designID: designRewardID + }; + + const packet = DesignChangedPacket(designReward); + player.map.registerNetworkPacket(player.id, packet); + + if (designRewardID === 5) { + for (let i = 1, length = player.eliteLevel; i < length; i++) { + const currentEliteLevelRewards = eliteRewards[i]; + if (currentEliteLevelRewards) { + currentEliteLevelRewards.single(0, player.inventory.getPosessions(), rewards => player.rewardCallback({ isRegular: true, authorID: player.id, source: {}, rewards })); + + player.inventory.resourceSharedLimiter.cannon.quantity.maximum += 2; + }; + }; + + const ePropertyChanged = { + typeID: 5, + value: player.inventory.resourceSharedLimiter.cannon.quantity.maximum + }; + + player.emit("propertyChanged", ePropertyChanged, player.id); + }; + }; +}; + +module.exports = rewardCallback; \ No newline at end of file diff --git a/Core.js b/Core.js new file mode 100644 index 0000000..b327558 --- /dev/null +++ b/Core.js @@ -0,0 +1,191 @@ +const GAME_TICKRATE = 1000 / 30; +const INACTIVITY_THRESHOLD_MS = 5 * 60000; + +let looseAccumulator = 0; +let accumulator = 0; +let difference = 0; +let n = performance.now(); +let nn = Date.now(); +let batch = {}; +let isLooseIteration = false; + +const packet = require("./Packets/packet"); + +const Game = require("./Game/Game"); + +const getFixedDate = require("./Utility/getFixedDate"); + +const packetRelevancies = require("./Data/packetRelevancies"); +const constants = require("./Data/constants"); +const { TIMING_EVENTS_BY_ID } = constants; + +function gameLoop() { + difference = performance.now() - n; + accumulator += difference; + n = performance.now(); + nn = Date.now(); + + if (accumulator > GAME_TICKRATE) { + Game.maps.forEach(map => { + map.eventAuthors.forEach(id => { + const currentFlush = map.eventBuffer[id]; + const entity = map.getEntityFromSubgroup(currentFlush.target); + if (entity) entity.emit("attacking", currentFlush.data, currentFlush.author); + }); + + if (looseAccumulator === 15) { + looseAccumulator = 0; + isLooseIteration = true; + }; + + map.players.forEach(player => { + if (isLooseIteration) { + player.isInactive = player.lastInteractionTimestamp + INACTIVITY_THRESHOLD_MS < n; + + player.refreshVisibilities(false); + + if (player.isInactive) player.setIndependentEvent(4, 0); + }; + + if (player.hasUnexpectedlyLeft) return; + + player.flush(1); + + batch.packet = null; + batch.offset = 0; + + const localPacketsLength = map.networkPacketsLocal.length; + const individual = map.networkPacketsInvidiual[player.id]; + + if (!localPacketsLength && individual) + for (let i = 0, length = individual.length; i < length; i++) batch = packet(individual[i].packet, batch, player.id, individual[i].metadata); + + if (localPacketsLength) + for (let i = 0, j = 0; i < localPacketsLength; i++) { + const p = map.networkPacketsLocal[i]; + const relevancyRequirement = packetRelevancies[p.header]; + if (relevancyRequirement(player, p)) batch = packet(p.packet, batch, player.id, p.metadata); + + if (individual) + while (j < individual.length && (i === individual[j].index || i === localPacketsLength - 1)) { + batch = packet(individual[j].packet, batch, player.id, individual[j].metadata); + j++; + }; + }; + + for (let i = 0, length = map.testPackets.length; i < length; i++) { + const p = map.testPackets[i]; + const relevancyRequirement = packetRelevancies[p.header]; + if (relevancyRequirement(player, p)) batch = packet(p.packet, batch, player.id, p.metadata); + }; + + for (let i = 0, length = map.networkPacketsGlobal.length; i < length; i++) { + const p = map.networkPacketsGlobal[i]; + batch = packet(p.packet, batch, player.id, p.metadata); + }; + + if (player.topics) { + player.topics.each((key, value) => { + for (let i = 0, length = value.length; i < length; i++) { + const p = value[i]; + batch = packet(p, batch, player.id); + }; + }); + }; + + if (batch.offset) { + const packet = batch.packet.slice(0, batch.offset); + const session = socket.sockets[player.sessionID]; + session.send(packet, true, true); + }; + + if (individual) individual.length = 0; + }); + + map.testPackets.length = 0; + map.networkPacketsGlobal.length = 0; + + map.eventAuthors.length = 0; + map.eventBuffer = {}; + + map.packetCount = map.networkPacketsLocal.length; + map.networkPacketsLocal.length = 0; + + if (isLooseIteration) { + map.towers.forEach(tower => { + if (!tower.searchedTarget) return; + + tower.initializeAttackWithCannon(tower.searchedTarget); + tower.searchedTarget = null; + }); + + map.lookingForTarget.forEach(e => e.initializeAttackWithCannon(e.searchedTarget)); + map.lookingForTarget.length = 0; + + map.dirtyChunks.forEach(c => { + c.map.clear(); + c.head = null; + c.tail = null; + }); + }; + }); + + Game.packets.each((_, value) => { + if (value.length > 0) console.info("Topic length", value.length, "All topics length", Game.packets.map.size) + value.length = 0 + }); + + const closest = Game.events.peek(); + if (closest && n >= closest.value) + Game.events.forEach(entry => { + if (n < entry.value) return false; + + const event = Game.events.poll(); + const { id, entity, index, getLocalization, data, isSingle } = event.obj; + const mapID = getLocalization(); + const map = Game.maps[mapID - 1]; + const entityInstance = map.getEntityFromSubgroup(entity.id); + + const eventExists = entityInstance && entityInstance.events[id] && entityInstance.events[id][index]; + const isCancelled = eventExists && entityInstance.events[id][index].isCancelled; + + if (isSingle && eventExists) entityInstance.events[id][index].isOver = true; + + if (!isCancelled && eventExists) { + const methodName = TIMING_EVENTS_BY_ID[id]; + entityInstance[methodName](data); + }; + + return true; + }); + + const closestFixed = Game.eventsFixed.peek(); + if (closestFixed && nn >= closestFixed.value) + Game.eventsFixed.forEach(entry => { + if (nn < entry.value) return false; + + const event = Game.eventsFixed.poll(); + + const { fn, arguments, next } = event.obj; + + fn(...arguments); + + if (next) { + const nextRunDate = getFixedDate(30); + next(nextRunDate, event.obj); + }; + + return true; + }); + + isLooseIteration = false; + looseAccumulator++; + + accumulator -= GAME_TICKRATE; + }; + + if (difference < GAME_TICKRATE - 32) setTimeout(() => gameLoop()); + else setImmediate(() => gameLoop()); +}; + +module.exports = gameLoop; \ No newline at end of file diff --git a/Data/admiralTable.js b/Data/admiralTable.js new file mode 100644 index 0000000..0fc7c2c --- /dev/null +++ b/Data/admiralTable.js @@ -0,0 +1,35 @@ +const AdmiralCounter = require("../Game/AdmiralCounter"); + +const admiralTable = new AdmiralCounter({ + 2: { + 2: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 4: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 6: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 8: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 10: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 12: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 14: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 16: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 18: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 20: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 22: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 24: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 26: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 28: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 30: { childToBeKilledCount: 2, maximumAdmiralCount: 2 } + }, + 3: { + 2: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 4: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 6: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 8: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 10: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 12: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 14: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 16: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 18: { childToBeKilledCount: 2, maximumAdmiralCount: 2 }, + 20: { childToBeKilledCount: 2, maximumAdmiralCount: 2 } + } +}); + +module.exports = admiralTable; \ No newline at end of file diff --git a/Data/ammunitions.js b/Data/ammunitions.js new file mode 100644 index 0000000..3278503 --- /dev/null +++ b/Data/ammunitions.js @@ -0,0 +1,15 @@ +const ammunitions = { + 1: { damage: 1 }, + 2: { damage: 2 }, + 3: { damage: 3 }, + 4: { damage: 4 }, + 5: { damage: 5 }, + 6: { damage: 6 }, + 7: { damage: 7, overtimeHealthEffectID: 1 }, + 8: { damage: 8, chanceToTerminateMovement: 20 }, + 9: { damage: -9 }, + 10: { damage: -10 }, + 11: { damage: 11 } +}; + +module.exports = ammunitions; \ No newline at end of file diff --git a/Data/batchSave.js b/Data/batchSave.js new file mode 100644 index 0000000..57e5035 --- /dev/null +++ b/Data/batchSave.js @@ -0,0 +1,5 @@ +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const saveLists = [new DoubleLinkedList(), new DoubleLinkedList()]; + +module.exports = saveLists; \ No newline at end of file diff --git a/Data/cannons.js b/Data/cannons.js new file mode 100644 index 0000000..698c391 --- /dev/null +++ b/Data/cannons.js @@ -0,0 +1,34 @@ +const cannons = { + 1: { + range: 150, damage: 1, reload: 6500, scatter: 0 + }, + 2: { + range: 200, damage: 3, reload: 5750, scatter: 0 + }, + 3: { + range: 250, damage: 5, reload: 5000, scatter: 0 + }, + 4: { + range: 300, damage: 8, reload: 4500, scatter: 0 + }, + 5: { + range: 350, damage: 11, reload: 4000, scatter: 0 + }, + 6: { + range: 400, damage: 10, reload: 4000, scatter: 0 + }, + 7: { + range: 400, damage: 11, reload: 3500, scatter: 0 + }, + 8: { + range: 450, damage: 11, reload: 3500, scatter: 0 + }, + 9: { + range: 350, damage: 4, reload: 1000, scatter: 0 + }, + 10: { + range: 500, damage: 15, reload: 3000, scatter: 0 + } +}; + +module.exports = cannons; \ No newline at end of file diff --git a/Data/cannonsSchemeNPC.js b/Data/cannonsSchemeNPC.js new file mode 100644 index 0000000..efa6095 --- /dev/null +++ b/Data/cannonsSchemeNPC.js @@ -0,0 +1,70 @@ +const cannonsSchemeNPC = { + 1: [ + [1, 20] + ], + 2: [ + [1, 1], + [4, 2], + [8, 3], + [10, 400] + ], + 3: [ + [10, 30] + ], + 4: [ + [1, 10], + [4, 5], + [8, 15], + [10, 300] + ], + 5: [ + [1, 10], + [4, 5], + [8, 15], + [10, 3] + ], + 6: [ + [1, 10], + [4, 5], + [8, 15], + [10, 300] + ], + 7: [ + [1, 10], + [4, 5], + [8, 15], + [10, 3] + ], + 8: [ + [1, 10], + [4, 5], + [8, 15], + [10, 300] + ], + 9: [ + [1, 10], + [4, 5], + [8, 15], + [10, 30] + ], + 10: [ + [1, 10], + [4, 5], + [8, 15], + [10, 30] + ], + 11: [ + [1, 10], + [4, 5], + [8, 15], + [10, 3] + ], + 12: [ + [1, 10], + [4, 5], + [8, 15], + [10, 30] + ] +}; + +module.exports = cannonsSchemeNPC; \ No newline at end of file diff --git a/Data/constants.js b/Data/constants.js new file mode 100644 index 0000000..5bb9c1a --- /dev/null +++ b/Data/constants.js @@ -0,0 +1,316 @@ +/* + MAXIMUM VALUES, DO NOT CHANGE AT ALL +*/ +const MAX_SAFE_INTEGER = Math.pow(2, 32) - 1; +const MAX_LEVEL_ELITE = 18; +const MAX_GUILD_TAX_RATE_GOLD = 30; +const MAX_GUILD_TAX_RATE_EMERALD = 10; + +/* + TYPE OF ENTITES +*/ +const ENTITY_TYPE_PLAYER = 1; +const ENTITY_TYPE_NPC = 2; +const ENTITY_TYPE_MONSTER = 3; +const ENTITY_TYPE_TOWER = 4; +const ENTITY_TYPE_COLLECTABLE = 5; + +/* + ECONOMY +*/ +const ECONOMY_GOLD = 1; +const ECONOMY_EMERALD = 2; + +/* + UNATTACKABLE ENTITIES +*/ +const UNATTACKABLE_ENTITY_TYPES = [ENTITY_TYPE_COLLECTABLE]; + +/* + EVENTS +*/ +const EVENT_DISCONNECT = "disconnect"; +const EVENT_DISCONNECT_CANCEL = "disconnectCancel" +const EVENT_DISCONNECT_IDLE = "disconnectIdle" +const EVENT_RESPAWN = "respawn"; +const EVENT_MAP_CHANGED = "mapChanged"; +const EVENT_PROPERTY_CHANGED = "propertyChanged"; +const EVENT_ATTACK_STARTED = "attackStarted"; +const EVENT_ATTACKING = "attacking"; +const EVENT_DIE = "die"; +const EVENT_LEVEL_UP = "levelUp"; +const EVENT_SPEED_UPDATE = "speedUpdate"; +const EVENT_ATTACK_ABORTED_BY_USER = "attackAbortedByUser"; +const EVENT_STOP_MOVEMENT = "stopMovement"; +const EVENT_CANCEL_STATE_CHANGE_TIMEOUT = "cancelStateChangeTimeout"; +const EVENT_ABLE_TO_JUMP = "ableToJump"; +const EVENT_UNABLE_TO_JUMP = "unableToJump"; +const EVENT_LEVEL_IS_LOW = "levelIsLow"; +const EVENT_UPDATE_VISIBILITY = "updateVisibility"; +const EVENT_DESTINATION_REACHED = "destinationReached"; +const EVENT_DESTINATION_NEW = "newDestination"; +const EVENT_SEEK_TARGET = "seekTarget"; +const EVENT_ADD_EVENTS = "addEvents"; +const EVENT_READY = "ready"; +const EVENT_DISTRIBUTE_REWARD = "distributeReward"; +const EVENT_DAMAGE = "damage"; +const EVENT_ITEM_USED = "itemUsed"; +const EVENT_CANNONS_USED = "cannonsUsed"; +const EVENT_HARPOONERS_USED = "harpoonersUsed"; +const EVENT_QUEST_DONE = "questDone"; +const EVENT_EFFECT_ADD = "effectAdd"; +const EVENT_DESINATION_NEW = "newDestination"; +const EVENT_ATTACK_BAR = "attackBar"; +const EVENT_PATH_READY = "scheduledPathReady" +const EVENT_SPAWN_ADMIRAL = "admiralSpawnNotification"; +const EVENT_HEALTH_CHANGED = "healthChanged"; +const EVENT_HEALTH_CHANGED_AUTHORLESS = "healthChangedAuthorless"; +const EVENT_GUILD_ISLAND_CAPTURE = "guildIslandCapture"; +const EVENT_GUILD_ISLAND_TOWER_BUILT = "guildDataIslandTowerBuilt"; +const EVENT_GUILD_ISLAND_TOWER_HEALTH_CHANGED = "guildDataIslandTowerHealthChanged"; + +/* + BEHAVIOUR +*/ +const BEHAVIOUR_AGGRESSIVE = "aggressive"; +const BEHAVIOUR_STOP_IF_ATTACKED = "stopIfAttacked"; + +/* + MODE OF MAPS +*/ +const MAP_MODE_PVE = "PvE"; +const MAP_MODE_PVP = "PvP"; + +/* + LOGOUT TIMEOUTS +*/ +const LOGOUT_TIME_BASIC = 30 * 1000; +const LOGOUT_TIME_PREMIUM = 10 * 1000; +const LOGOUT_TIME_IDLE = 10 * 60 * 10000; + +/* + BASE VALUES FOR CALCULATIONS +*/ +const BASE_REPAIR_AMOUNT_MINIMUM = 1000; +const BASE_REPAIR_AMOUNT_MAXIMUM = 2000; +const BASE_SPEED_PLAYER_REPAIR = 1500; +const BASE_SPEED_PLAYER = 100; + +/* + TASKS +*/ +const TASK_TYPE_DEAL_DAMAGE = 1; +const TASK_TYPE_DESTROY = 2; + +/* + TIMING EVENTS +*/ +const TIMING_EVENT_CANNON_ATTACK = 1; +const TIMING_EVENT_HARPOONER_ATTACK = 2; +const TIMING_EVENT_REPAIR = 3; +const TIMING_EVENT_DISCONNECT = 4; +const TIMING_EVENT_DISCONNECT_IDLE = 5; +const TIMING_EVENT_MAP_CHANGE = 6; +const TIMING_EVENT_MOVE = 7; +const TIMING_EVENT_RESPAWN = 8; +const TIMING_EVENT_OVERTIME_DECREASE_HITPOINT = 9; +const TIMING_EVENT_OVERTIME_INCREASE_HITPOINT = 10; +const TIMING_EVENT_DIE = 11; +const TIMING_EVENT_OVERTIME_DECREASE_HITPOINT_2 = 12; + +/* + ACQUIRABLE ITEM CATEGORIES +*/ +const CATEGORY_ECONOMY = 1; +const CATEGORY_ITEM = 2; +const CATEGORY_AMMUNITION = 3; +const CATEGORY_HARPOON = 4; +const CATEGORY_DESIGN = 5; +const CATEGORY_STATUS = 6; +const CATEGORY_CANNON = 7; +const CATEGORY_HARPOONER = 8; +const CATEGORY_SAIL = 9; +const CATEGORY_PIRATE = 10; +const CATEGORY_RANKING_POINTS = 11; + +/* + QUEST STATUS +*/ +const QUEST_STATE_ACTIVE = 0; +const QUEST_STATE_COMPLETED = 0; + +/* + PROPERTY ALTERING CATEGORIES +*/ +const AFFECT_CANNON_DAMAGE = 1; +const AFFECT_HARPOONER_DAMAGE = 2; +const AFFECT_PROTECTION = 3; +const AFFECT_SAILING_SPEED = 4; +const AFFECT_HITPOINT = 5; + +/* + GUILD PERMISSION BITS +*/ +const GUILD_PERMISSION_KICK = 1; +const GUILD_PERMISSION_ACCEPT_JOIN_REQUEST = 2; +const GUILD_PERMISSION_EDIT_IDENTITY = 4; +const GUILD_PERMISSION_EDIT_TAX = 8; +const GUILD_PERMISSION_EDIT_ISLAND = 16; + +/* + LEADERBOARD TYPES +*/ +const LEADERBOARD_EXPERIENCE_POINTS = 1; +const LEADERBOARD_NPC_SINK = 2; +const LEADERBOARD_MONSTER_SINK = 3; +const LEADERBOARD_PLAYER_SINK = 4; +const LEADERBOARD_ELITE_POINTS = 5; +const LEADERBOARD_FISHING_POINTS = 6; + +/* + GUILD COOLDOWN TIMES +*/ +const GUILD_COOLDOWN_EDIT_TAX_RATES = 60000; +const GUILD_COOLDOWN_EDIT_TAG = 60000; +const GUILD_COOLDOWN_EDIT_NAME = 60000; +const GUILD_COOLDOWN_EDIT_DESCRIPTION = 60000; + +const COLLECTIBLE_CURRENCIES_BY_ID = { + 1: "gold", + 2: "emerald", + 3: "draconicScales", + 4: "corsaireCoins" +}; + +const COLLECTIBLE_CURRENCIES_BY_NAME = { + gold: 1, + emerald: 2, + draconicScales: 3, + corsaireCoins: 4 +}; + +const ENTITY_TYPES_BY_ID = { + 1: ENTITY_TYPE_PLAYER, + 2: ENTITY_TYPE_NPC, + 3: ENTITY_TYPE_MONSTER, + 4: ENTITY_TYPE_TOWER, + 5: ENTITY_TYPE_COLLECTABLE +}; + +const TIMING_EVENTS_BY_ID = { + 1: "attackWithCannon", + 2: "attackWithHarpooner", + 3: "repair", + 4: "disconnect", + 5: "disconnect", + 6: "isChangeMapAvailable", + 7: "move", + 8: "respawn", + 9: "overtimeDecreaseCurrentHitpoint", + 10: "overtimeIncreaseCurrentHitpoint", + 11: "die", + 12: "overtimeDecreaseCurrentHitpoint", + 13: "overtimeDecreaseCurrentHitpoint", + 14: "respawn", + 15: "changeMap", + 16: "abortAttack", + 17: "effectRemove", + 97: "attackWithCannon", + 98: "attackWithCannon", + 99: "attackWithCannon", + 100: "attackWithCannon", + 101: "attackWithCannon", + 102: "attackWithCannon", + 103: "attackWithCannon", + 104: "attackWithCannon", + 105: "attackWithCannon", + 106: "attackWithCannon", + 197: "attackWithHarpooner", + 198: "attackWithHarpooner", + 199: "attackWithHarpooner", + 200: "attackWithHarpooner", + 201: "attackWithHarpooner", + 202: "attackWithHarpooner", + 299: "changeSpeed", + 300: "changeSpeed", + 999: "refreshVisibilities" +}; + +const EVENTS_BY_TIMING_EVENTS_ID = { + 1: EVENT_ATTACKING, + 2: EVENT_ATTACKING +}; + +const EFFECTS_TERMINATE_MOVEMENT = [1]; + +module.exports.MAXIMUM = { + MAX_SAFE_INTEGER, MAX_LEVEL_ELITE, MAX_GUILD_TAX_RATE_GOLD, MAX_GUILD_TAX_RATE_EMERALD +}; + +module.exports.TYPES = { + ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_MONSTER, ENTITY_TYPE_TOWER, ENTITY_TYPE_COLLECTABLE +}; + +module.exports.UNATTACKABLE_ENTITY_TYPES = UNATTACKABLE_ENTITY_TYPES; + +module.exports.EVENTS = { + EVENT_DISCONNECT, EVENT_DISCONNECT_CANCEL, EVENT_DISCONNECT_IDLE, EVENT_RESPAWN, EVENT_MAP_CHANGED, EVENT_PROPERTY_CHANGED, EVENT_ATTACK_STARTED, EVENT_ATTACKING, EVENT_DIE, EVENT_SPEED_UPDATE, + EVENT_ATTACK_ABORTED_BY_USER, EVENT_STOP_MOVEMENT, EVENT_CANCEL_STATE_CHANGE_TIMEOUT, + EVENT_ABLE_TO_JUMP, EVENT_UNABLE_TO_JUMP, EVENT_LEVEL_IS_LOW, EVENT_UPDATE_VISIBILITY, EVENT_DESTINATION_REACHED, + EVENT_DESTINATION_NEW, EVENT_SEEK_TARGET, EVENT_LEVEL_UP, EVENT_ADD_EVENTS, EVENT_READY, EVENT_DISTRIBUTE_REWARD, + EVENT_DAMAGE, EVENT_ITEM_USED, EVENT_CANNONS_USED, EVENT_HARPOONERS_USED, EVENT_QUEST_DONE, EVENT_EFFECT_ADD, EVENT_DESINATION_NEW, EVENT_ATTACK_BAR, EVENT_PATH_READY, EVENT_SPAWN_ADMIRAL, + EVENT_HEALTH_CHANGED, EVENT_HEALTH_CHANGED_AUTHORLESS, EVENT_GUILD_ISLAND_CAPTURE, EVENT_GUILD_ISLAND_TOWER_BUILT, EVENT_GUILD_ISLAND_TOWER_HEALTH_CHANGED +}; + +module.exports.BEHAVIOURS = { + BEHAVIOUR_AGGRESSIVE, BEHAVIOUR_STOP_IF_ATTACKED +}; + +module.exports.MAP_MODES = { + MAP_MODE_PVE, MAP_MODE_PVP +}; + +module.exports.LOGOUT_TIMES = { + LOGOUT_TIME_BASIC, LOGOUT_TIME_PREMIUM, LOGOUT_TIME_IDLE +}; + +module.exports.BASE = { + BASE_REPAIR_AMOUNT_MINIMUM, BASE_REPAIR_AMOUNT_MAXIMUM, BASE_SPEED_PLAYER_REPAIR, BASE_SPEED_PLAYER +}; + +module.exports.TASK_TYPES = { + TASK_TYPE_DEAL_DAMAGE, TASK_TYPE_DESTROY +}; + +module.exports.TIMING_EVENTS = { + TIMING_EVENT_CANNON_ATTACK, TIMING_EVENT_HARPOONER_ATTACK, TIMING_EVENT_REPAIR, TIMING_EVENT_DISCONNECT, TIMING_EVENT_DISCONNECT_IDLE, TIMING_EVENT_MAP_CHANGE, TIMING_EVENT_MOVE, TIMING_EVENT_RESPAWN, + TIMING_EVENT_OVERTIME_DECREASE_HITPOINT, TIMING_EVENT_OVERTIME_INCREASE_HITPOINT, TIMING_EVENT_OVERTIME_DECREASE_HITPOINT_2 +}; + +module.exports.CATEGORIES = { + CATEGORY_ECONOMY, CATEGORY_ITEM, CATEGORY_AMMUNITION, CATEGORY_HARPOON, CATEGORY_DESIGN, CATEGORY_STATUS, CATEGORY_CANNON, CATEGORY_HARPOONER, CATEGORY_SAIL, CATEGORY_PIRATE, CATEGORY_RANKING_POINTS +}; + +module.exports.AFFECTS = { + AFFECT_CANNON_DAMAGE, AFFECT_HARPOONER_DAMAGE, AFFECT_PROTECTION, AFFECT_SAILING_SPEED, AFFECT_HITPOINT +}; + +module.exports.GUILD_PERMISSIONS = { + GUILD_PERMISSION_KICK, GUILD_PERMISSION_ACCEPT_JOIN_REQUEST, GUILD_PERMISSION_EDIT_IDENTITY, GUILD_PERMISSION_EDIT_TAX, GUILD_PERMISSION_EDIT_ISLAND +}; + +module.exports.GUILD_COOLDOWNS = { + GUILD_COOLDOWN_EDIT_TAX_RATES, GUILD_COOLDOWN_EDIT_TAG, GUILD_COOLDOWN_EDIT_NAME, GUILD_COOLDOWN_EDIT_DESCRIPTION +}; + +module.exports.COLLECTIBLE_CURRENCIES_BY_ID = COLLECTIBLE_CURRENCIES_BY_ID; + +module.exports.COLLECTIBLE_CURRENCIES_BY_NAME = COLLECTIBLE_CURRENCIES_BY_NAME; + +module.exports.ENTITY_TYPES_BY_ID = ENTITY_TYPES_BY_ID; + +module.exports.TIMING_EVENTS_BY_ID = TIMING_EVENTS_BY_ID; + +module.exports.EVENTS_BY_TIMING_EVENTS_ID = EVENTS_BY_TIMING_EVENTS_ID; + +module.exports.EFFECTS_TERMINATE_MOVEMENT = EFFECTS_TERMINATE_MOVEMENT; diff --git a/Data/designs.js b/Data/designs.js new file mode 100644 index 0000000..d89c80b --- /dev/null +++ b/Data/designs.js @@ -0,0 +1,62 @@ +const DESIGNS = { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true, + 11: true, + 12: true, + 13: true, + 14: true, + 15: true, + 16: true, + 17: true, + 18: true, + 19: true, + 20: true, + 21: true, + 22: true, + 23: true, + 24: true, + 25: true, + 26: true, + 27: true, + 28: true, + 29: true, + 30: true, + 31: true, + 32: true, + 33: true, + 34: true, + 35: true, + 36: true, + 37: true, + 38: true, + 39: true, + 40: true, + 41: true, + 42: true, + 43: true, + 44: true, + 45: true, + 46: true, + 47: true, + 48: true, + 49: true, + 50: true, + 51: true, + 52: true, + 53: true, + 54: true, + 55: true, + 56: true, + 57: true, + 58: true +}; + +module.exports = DESIGNS; \ No newline at end of file diff --git a/Data/effects.js b/Data/effects.js new file mode 100644 index 0000000..87cdea6 --- /dev/null +++ b/Data/effects.js @@ -0,0 +1,18 @@ +const effects = { + 1: [1], + 2: [2], + 3: [3], + 4: [4], + 5: [5], + 6: [6], + 7: [7], + 8: [8] +}; + +/* + Keys are the effectID's. Arrays bounded to them contain itemID's. + This is used to activate the items bounded to the effects. + It looks redundant: arrays are used so multiple items can be bounded to one effect. +*/ + +module.exports = effects; \ No newline at end of file diff --git a/Data/eliteRewards.js b/Data/eliteRewards.js new file mode 100644 index 0000000..88030f5 --- /dev/null +++ b/Data/eliteRewards.js @@ -0,0 +1,151 @@ +const Reward = require("../Game/Reward"); + +const eliteRewards = [ + 0, + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 6: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 7: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 8: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 9: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 10: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 11: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 12: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 13: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 14: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 15: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 16: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 17: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 18: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 19: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 20: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 21: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 22: { amountMinimum: 1, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 5: { + 23: { amountMinimum: 1, chance: 100 } + } + } + }) +]; + +module.exports = eliteRewards; \ No newline at end of file diff --git a/Data/entityGroups.js b/Data/entityGroups.js new file mode 100644 index 0000000..7fd6eb1 --- /dev/null +++ b/Data/entityGroups.js @@ -0,0 +1,77 @@ +const entityGroup = { + id: 1, + mapIDs: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + entitySettings: { + 1: { + speed: 600, + respawnTime: 0, + size: 128 + }, + 2: { + speed: 400, + respawnTime: 60, + size: 96 + }, + 3: { + speed: 120, + respawnTime: 30, + size: 64 + } + }, + structure: [ + { + typeID: 2, + entityTypeID: 113, + groupMemberTypeID: 1 + }, + { + typeID: 2, + entityTypeID: 113, + groupMemberTypeID: 1 + }, + { + typeID: 2, + entityTypeID: 114, + groupMemberTypeID: 2 + }, + { + typeID: 2, + entityTypeID: 114, + groupMemberTypeID: 2 + }, + { + typeID: 2, + entityTypeID: 114, + groupMemberTypeID: 2 + }, + { + typeID: 2, + entityTypeID: 115, + groupMemberTypeID: 3 + }, + { + typeID: 2, + entityTypeID: 115, + groupMemberTypeID: 3 + }, + { + typeID: 2, + entityTypeID: 115, + groupMemberTypeID: 3 + }, + { + typeID: 2, + entityTypeID: 115, + groupMemberTypeID: 3 + }, + { + typeID: 2, + entityTypeID: 115, + groupMemberTypeID: 3 + } + ] +}; + +const entityGroups = [entityGroup]; + +module.exports = entityGroups; \ No newline at end of file diff --git a/Data/harpooners.js b/Data/harpooners.js new file mode 100644 index 0000000..2b35182 --- /dev/null +++ b/Data/harpooners.js @@ -0,0 +1,22 @@ +const harpooners = { + 1: { + range: 150 / 4, damage: 1 / 4, reload: 6500 / 4 + }, + 2: { + range: 200 / 4, damage: 3 / 4, reload: 5750 / 4 + }, + 3: { + range: 250 / 4, damage: 5 / 4, reload: 5000 / 4 + }, + 4: { + range: 300 / 4, damage: 8 / 4, reload: 4500 / 4 + }, + 5: { + range: 350 / 4, damage: 11 / 4, reload: 4000 / 4 + }, + 6: { + range: 400 / 4, damage: 10 / 4, reload: 4000 / 4 + } +}; + +module.exports = harpooners; \ No newline at end of file diff --git a/Data/harpoons.js b/Data/harpoons.js new file mode 100644 index 0000000..957bbdf --- /dev/null +++ b/Data/harpoons.js @@ -0,0 +1,12 @@ +const harpoons = { + 1: { damage: 50 }, + 2: { damage: 250 }, + 3: { damage: 500 }, + 4: { damage: 750 }, + 5: { damage: 1250 }, + 6: { damage: 1500 }, + 7: { damage: 2000 }, + 8: { damage: 2500 } +}; + +module.exports = harpoons; \ No newline at end of file diff --git a/Data/items.js b/Data/items.js new file mode 100644 index 0000000..e8f2fa1 --- /dev/null +++ b/Data/items.js @@ -0,0 +1,33 @@ +const constants = require("../Data/constants"); +const { AFFECTS, MAP_MODES, TYPES } = constants; +const { AFFECT_CANNON_DAMAGE, AFFECT_HARPOONER_DAMAGE, AFFECT_PROTECTION, AFFECT_SAILING_SPEED, AFFECT_HITPOINT } = AFFECTS; +const { MAP_MODE_PVE, MAP_MODE_PVP } = MAP_MODES; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_TOWER } = TYPES; + +const itemTable = { + 1: { scale: 1.2, isSelf: true, isDurable: true, affects: [2], range: 0, cooldown: 0, statusID: 1, useOnImpact: false }, + 2: { scale: 1.2, isSelf: true, isDurable: true, affects: [2], range: 0, cooldown: 0, statusID: 2, useOnImpact: false }, + 3: { scale: 1.2, isSelf: true, isDurable: true, affects: [2], range: 0, cooldown: 0, statusID: 3, useOnImpact: false }, + 4: { scale: 1.2, isSelf: true, isDurable: true, affects: [2], range: 0, cooldown: 0, statusID: 4, useOnImpact: false }, + 5: { scale: 0.2, isSelf: true, isDurable: true, affects: [AFFECT_PROTECTION], range: 0, cooldown: 0, statusID: 5, useOnImpact: false }, + 6: { scale: 0.2, isSelf: true, isDurable: true, affects: [AFFECT_PROTECTION], range: 0, cooldown: 0, statusID: 6, useOnImpact: false }, + 7: { scale: 0.2, isSelf: true, isDurable: true, affects: [AFFECT_PROTECTION], range: 0, cooldown: 0, statusID: 7, useOnImpact: false }, + 8: { scale: 0.2, isSelf: true, isDurable: true, affects: [AFFECT_PROTECTION], range: 0, cooldown: 0, statusID: 8, useOnImpact: false }, + 9: { scale: -1000, isSelf: false, isDurable: false, affects: [AFFECT_HITPOINT], range: 700, cooldown: 2000, statusID: 0, useOnImpact: true, allowedTargetTypes: { + "PvE": [ENTITY_TYPE_NPC, ENTITY_TYPE_TOWER], + "PvP": [ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_TOWER] + }}, + 10: { scale: -25000, isSelf: false, isDurable: false, affects: [AFFECT_HITPOINT], range: 700, cooldown: 2000, statusID: 0, useOnImpact: true, allowedTargetTypes: { + "PvE": [ENTITY_TYPE_NPC, ENTITY_TYPE_TOWER], + "PvP": [ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_TOWER] + }}, + 11: { scale: 10000, isSelf: true, isDurable: false, affects: [AFFECT_HITPOINT], range: 0, cooldown: 10000, statusID: 0, useOnImpact: true }, + 12: { scale: 15000, isSelf: true, isDurable: false, affects: [AFFECT_HITPOINT], range: 0, cooldown: 10000, statusID: 0, useOnImpact: true }, + 13: { scale: 16000, isSelf: true, isDurable: false, affects: [AFFECT_HITPOINT], range: 0, cooldown: 10000, statusID: 0, useOnImpact: true }, + 14: { scale: 17000, isSelf: true, isDurable: false, affects: [AFFECT_HITPOINT], range: 0, cooldown: 10000, statusID: 0, useOnImpact: true }, + 15: { scale: 35000, isSelf: true, isDurable: false, affects: [AFFECT_HITPOINT], range: 0, cooldown: 10000, statusID: 0, useOnImpact: true }, + 16: { scale: -12, isSelf: true, isDurable: false, affects: [AFFECT_SAILING_SPEED], range: 0, cooldown: 15000, time: 5000, statusID: 16, iid: 299, useOnImpact: true }, + 17: { scale: -25, isSelf: true, isDurable: false, affects: [AFFECT_SAILING_SPEED], range: 0, cooldown: 30000, time: 10000, statusID: 17, iid: 300, useOnImpact: true } +}; + +module.exports = itemTable; \ No newline at end of file diff --git a/Data/leaderboard.js b/Data/leaderboard.js new file mode 100644 index 0000000..355aecb --- /dev/null +++ b/Data/leaderboard.js @@ -0,0 +1,20 @@ +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const leaderboards = []; + +for (let i = 0; i < 7; i++) { + const leaderboard = { + cache: {}, + list: new DoubleLinkedList(), + size: 0, + entriesNEW: {}, + updatePosition(id, amount, isAbsolute) { + if (isAbsolute) this.list.addItem(id, amount); + else this.list.accumulateItem(id, amount); + } + }; + + leaderboards.push(leaderboard); +}; + +module.exports = leaderboards; \ No newline at end of file diff --git a/Data/levelRewards.js b/Data/levelRewards.js new file mode 100644 index 0000000..979870a --- /dev/null +++ b/Data/levelRewards.js @@ -0,0 +1,162 @@ +const Reward = require("../Game/Reward"); + +const levelRewards = [ + 0, + 0, + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 3: { amountMinimum: 42, chance: 100 } + }, + 8: { + 3: { amountMinimum: 42, chance: 100 } + } + } + }), + new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 42, chance: 100 } + }, + 2: { + 5: { amountMinimum: 42, chance: 100 } + }, + 7: { + 1: { amountMinimum: 42, chance: 100 } + }, + 8: { + 1: { amountMinimum: 42, chance: 100 } + } + } + }) +]; + +module.exports = levelRewards; + diff --git a/Data/levels.js b/Data/levels.js new file mode 100644 index 0000000..ebfd4e5 --- /dev/null +++ b/Data/levels.js @@ -0,0 +1,45 @@ +const { MAXIMUM } = require("./constants"); +const { MAX_LEVEL_ELITE } = MAXIMUM; + +const a = 1; +const b = -1; + +const levels = { + pirateLevel(points) { + const c = points * 2 / 250 * -1; + + const discriminant = Math.pow(b, 2) - 4 * a * c; + const x1 = (-b + Math.sqrt(discriminant)) / (2 * a); + const currentLevel = Math.floor(x1); + + return currentLevel; + }, + eliteLevel(points) { + const c = points * 2 / 750 * -1; + const discriminant = Math.pow(b, 2) - 4 * a * c; + const x1 = (-b + Math.sqrt(discriminant)) / (2 * a); + const currentLevel = Math.floor(x1); + + return Math.min(currentLevel, MAX_LEVEL_ELITE); + }, + fishingLevel(points) { + const c = points * 2 / 525 * -1; + + const discriminant = Math.pow(b, 2) - 4 * a * c; + const x1 = (-b + Math.sqrt(discriminant)) / (2 * a); + const currentLevel = Math.floor(x1); + + return currentLevel; + }, + guildLevel(points) { + const c = points * 2 / 20 * -1; + + const discriminant = Math.pow(b, 2) - 4 * a * c; + const x1 = (-b + Math.sqrt(discriminant)) / (2 * a); + const currentLevel = Math.floor(x1); + + return currentLevel; + } +}; + +module.exports = levels; \ No newline at end of file diff --git a/Data/loginRewards.js b/Data/loginRewards.js new file mode 100644 index 0000000..2549940 --- /dev/null +++ b/Data/loginRewards.js @@ -0,0 +1,31 @@ +const loginRewards = { + 7: { + 1: { + 3: { + 5: { amountMinimum: 1500, chance: 100 } + }, + 7: { + 3: { amountMinimum: 2, chance: 100 } + }, + 8: { + 3: { amountMinimum: 2, chance: 100 } + } + }, + 2: { + 3: { + 5: { amountMinimum: 1500, chance: 100 } + }, + 7: { + 3: { amountMinimum: 2, chance: 100 } + }, + 8: { + 3: { amountMinimum: 2, chance: 100 } + } + }, + 3: { + + } + } +}; + +module.exports = loginRewards; \ No newline at end of file diff --git a/Data/mapMatrix.js b/Data/mapMatrix.js new file mode 100644 index 0000000..cc823ca --- /dev/null +++ b/Data/mapMatrix.js @@ -0,0 +1,9 @@ +const maps = [ + [1, 6, 11, 16], + [2, 7, 12, 17], + [3, 8, 13, 18], + [4, 9, 14, 19], + [5, 10, 15, 20] +]; + +module.exports = maps; \ No newline at end of file diff --git a/Data/maps.js b/Data/maps.js new file mode 100644 index 0000000..0b56c7c --- /dev/null +++ b/Data/maps.js @@ -0,0 +1,436 @@ +const constans = require("./constants"); +const entityGroups = require("./entityGroups"); + +const { MAP_MODES } = constans; + +const { MAP_MODE_PVE, MAP_MODE_PVP } = MAP_MODES; + +const MAP_NEIGHBOUR_DIRECTION_LEFT = 1; +const MAP_NEIGHBOUR_DIRECTION_RIGHT = 2; +const MAP_NEIGHBOUR_DIRECTION_TOP = 3; +const MAP_NEIGHBOUR_DIRECTION_BOTTOM = 4; + +const map_1 = { + name: "1/1", + level: 1, + id: 1, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(1)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 5, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 2, align: "y", threshold: 96 } + ], + collision: {} +}; + +const map_2 = { + name: "1/2", + level: 1, + id: 2, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(2)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 1, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 6, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 3, align: "y", threshold: 96 } + ] +} + +const map_3 = { + name: "2/1", + level: 10, + id: 3, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(3)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 7, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 2, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 4, align: "y", threshold: 96 } + ] +} + +const map_4 = { + name: "2/2", + level: 10, + id: 4, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(4)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 8, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 3, align: "y", threshold: 96 } + ] +} + +const map_5 = { + name: "3/1", + level: 25, + id: 5, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(5)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 1, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 9, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 6, align: "y", threshold: 96 } + ] +} + +const map_6 = { + name: "3/2", + level: 25, + id: 6, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(6)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 2, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 10, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 5, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 7, align: "y", threshold: 96 } + ] +} + +const map_7 = { + name: "4/1", + level: 50, + id: 7, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(7)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 3, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 11, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 6, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 8, align: "y", threshold: 96 } + ] +} + +const map_8 = { + name: "4/2", + level: 50, + id: 8, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(8)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 4, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 12, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 7, align: "y", threshold: 96 }, + ] +} + +const map_9 = { + name: "5/1", + level: 75, + id: 9, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(9)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 5, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 13, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 10, align: "y", threshold: 96 } + ] +} + +const map_10 = { + name: "5/2", + level: 75, + id: 10, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(10)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 6, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 14, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 9, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 11, align: "y", threshold: 96 } + ] +} + +const map_11 = { + name: "6/1", + level: 100, + id: 11, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(11)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 7, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 15, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 10, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 12, align: "y", threshold: 96 } + ] +} + +const map_12 = { + name: "6/2", + level: 1, + id: 12, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(12)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 8, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 16, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 11, align: "y", threshold: 96 } + ] +} + +const map_13 = { + name: "7/1", + level: 1, + id: 13, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(13)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 9, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 17, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 14, align: "y", threshold: 96 } + ] +} + +const map_14 = { + name: "7/2", + level: 1, + id: 14, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(14)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 10, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 18, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 13, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 15, align: "y", threshold: 96 } + ] +} + +const map_15 = { + name: "8/1", + level: 1, + id: 15, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(15)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 11, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 19, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 14, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 16, align: "y", threshold: 96 } + ] +} + +const map_16 = { + name: "8/2", + level: 1, + id: 16, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(16)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 12, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_RIGHT, id: 20, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 15, align: "y", threshold: 96 } + ] +} + +const map_17 = { + name: "9/1", + level: 1, + id: 17, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(17)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 13, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 18, align: "y", threshold: 96 } + ] +} + +const map_18 = { + name: "9/2", + level: 1, + id: 18, + type: MAP_MODE_PVE, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(18)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 14, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 17, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 19, align: "y", threshold: 96 } + ] +} + +const map_19 = { + name: "10/1", + level: 1, + id: 19, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(19)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 15, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 18, align: "y", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_BOTTOM, id: 20, align: "y", threshold: 96 } + ] +} + +const map_20 = { + name: "10/2", + level: 1, + id: 20, + type: MAP_MODE_PVP, + size: { + width: 6000, + height: 4500, + chunk: { + width: 1200, + height: 900 + } + }, + entityGroups: entityGroups.filter(e => e.mapIDs.includes(20)), + neighbours: [ + { directionIndex: MAP_NEIGHBOUR_DIRECTION_LEFT, id: 16, align: "x", threshold: 96 }, + { directionIndex: MAP_NEIGHBOUR_DIRECTION_TOP, id: 19, align: "y", threshold: 96 } + ] +} + +module.exports = [map_1, map_2, map_3, map_4, map_5, map_6, map_7, map_8, map_9, map_10, map_11, map_12, map_13, map_14, map_15, map_16, map_17, map_18, map_19, map_20]; \ No newline at end of file diff --git a/Data/overtime.js b/Data/overtime.js new file mode 100644 index 0000000..8f2ef25 --- /dev/null +++ b/Data/overtime.js @@ -0,0 +1,7 @@ +const OVERTIME_HEALTH_EFFECTS = { + 1: { amount: 0.15, ticks: 10, time: 1000 }, + 2: { amount: 0.15, ticks: 5, time: 500 }, + 3: { amount: 0.15, ticks: 10, time: 100 } +}; + +module.exports = OVERTIME_HEALTH_EFFECTS; \ No newline at end of file diff --git a/Data/packetRelevancies.js b/Data/packetRelevancies.js new file mode 100644 index 0000000..40e8ac9 --- /dev/null +++ b/Data/packetRelevancies.js @@ -0,0 +1,18 @@ +const Utility = require("../Game/Utility"); + +const packetRelevancies = { + 0: function (player, packet) { + return player.inDistance[packet.authorID] || player.id === packet.authorID; + }, + 1: function (player, packet) { + return Utility.IsInDistance(packet.position, player.position, player.sight + 60); + }, + 19: function (player) { + return true; + }, + 59: function (player, packet) { + return player.isAlive && (player.inDistance[packet.authorID] || player.id === packet.authorID); + } +}; + +module.exports = packetRelevancies; \ No newline at end of file diff --git a/Data/packets.js b/Data/packets.js new file mode 100644 index 0000000..af7ba56 --- /dev/null +++ b/Data/packets.js @@ -0,0 +1,71 @@ +const ReadyMessage = require("../Messages/ReadyMessage"); +const QuestMessage = require("../Messages/QuestMessage"); +const SpyMessage = require("../Messages/SpyMessage"); +const CauldronMessage = require("../Messages/CauldronMessage"); +const MovementMessage = require("../Messages/MovementMessage"); +const CloseMessage = require("../Messages/CloseMessage"); +const RepairMessage = require("../Messages/RepairMessage"); +const AttackStartMessage = require("../Messages/AttackStartMessage"); +const AttackAbortMessage = require("../Messages/AttackAbortMessage"); +const RespawnMessage = require("../Messages/RespawnMessage"); +const EquipmentChangeMessage = require("../Messages/EquipmentChangeMessage"); +const MapChangeMessage = require("../Messages/MapChangeMessage"); +const AmmunitionChangeMessage = require("../Messages/AmmunitionChangeMessage"); +const DesignChangeMessage = require("../Messages/DesignChangeMessage"); +const ItemUseMessage = require("../Messages/ItemUseMessage"); +const ItemBuyMessage = require("../Messages/ItemBuyMessage"); +const LeaderboardPageMessage = require("../Messages/LeaderboardPageMessage"); +const GuildMemberLeaveMessage = require("../Messages/GuildMemberLeaveMessage"); +const GuildMemberJoinMessage = require("../Messages/GuildMemberJoinMessage"); +const GuildJoinRequestSubmitMessage = require("../Messages/GuildJoinRequestSubmitMessage"); +const GuildDataMessage = require("../Messages/GuildDataMessage"); +const GuildDonateMessage = require("../Messages/GuildDonateMessage"); +const GuildDetailsTaxEditMessage = require("../Messages/GuildDetailsTaxEditMessage"); +const GuildDetailsIdentityEditMessage = require("../Messages/GuildDetailsIdentityEditMessage"); +const GuildIslandTowerRepairMessage = require("../Messages/GuildIslandTowerRepairMessage"); +const GuildIslandTowerBuildMessage = require("../Messages/GuildIslandTowerBuildMessage"); +const LeaderboardDataMessage = require("../Messages/LeaderboardDataMessage"); +const GuildSearchMessage = require("../Messages/GuildSearchMessage"); +const GuildJoinRequestRevokeMessage = require("../Messages/GuildJoinRequestRevokeMessage"); +const GuildCreateMessage = require("../Messages/GuildCreateMessage"); + +const packetTable = { + 1: { handler: ReadyMessage, expectedSize: 1, isAvailable: true }, + 2: { handler: QuestMessage, expectedSize: 3, isAvailable: true }, + 3: { handler: SpyMessage, expectedSize: 5, isAvailable: true }, + 4: { handler: CauldronMessage, expectedSize: 3, isAvailable: true }, + 5: { handler: MovementMessage, expectedSize: 5, isAvailable: true }, + 6: { handler: CloseMessage, expectedSize: 1, isAvailable: true }, + 7: { handler: RepairMessage, expectedSize: 1, isAvailable: true }, + 8: { handler: AttackStartMessage, expectedSize: 6, isAvailable: true }, + 9: { handler: AttackAbortMessage, expectedSize: 6, isAvailable: true }, + 10: { handler: RespawnMessage, expectedSize: 1, isAvailable: true }, + 11: { handler: EquipmentChangeMessage, expectedSize: 5, isAvailable: true }, + 12: { handler: MapChangeMessage, expectedSize: 1, isAvailable: true }, + 13: { handler: AmmunitionChangeMessage, expectedSize: 3, isAvailable: true }, + 14: { handler: DesignChangeMessage, expectedSize: 3, isAvailable: true }, + 15: { handler: ItemUseMessage, expectedSize: 7, isAvailable: true }, + 16: { handler: ItemBuyMessage, expectedSize: 7, isAvailable: true }, + 17: { handler: LeaderboardPageMessage, expectedSize: 4, isAvailable: true }, + 18: { handler: GuildMemberLeaveMessage, expectedSize: 5, isAvailable: true }, + 19: { handler: GuildMemberJoinMessage, expectedSize: 5, isAvailable: true }, + 20: { handler: GuildJoinRequestSubmitMessage, expectedSize: 0, isAvailable: true }, + 21: { handler: GuildDataMessage, expectedSize: 1, isAvailable: true }, + 22: { handler: GuildDonateMessage, expectedSize: 6, isAvailable: true }, + 23: { handler: GuildDetailsTaxEditMessage, expectedSize: 3, isAvailable: true }, + 24: { handler: GuildDetailsIdentityEditMessage, expectedSize: 0, isAvailable: true }, + 25: { handler: GuildIslandTowerRepairMessage, expectedSize: 6, isAvailable: true }, + 26: { handler: GuildIslandTowerBuildMessage, expectedSize: 7, isAvailable: true }, + 27: { handler: LeaderboardDataMessage, expectedSize: 1, isAvailable: true }, + 28: { handler: GuildSearchMessage, expectedSize: 0, isAvailable: true }, + 29: { handler: GuildJoinRequestRevokeMessage, expectedSize: 7, isAvailable: true }, + 30: { handler: GuildCreateMessage, expectedSize: 0, isAvailable: true } +}; + +module.exports = packetTable; + +/* + handler: the function that runs when packet is received + expectedSize: size of the packet + isAvailable: whether the server accepts this packet +*/ \ No newline at end of file diff --git a/Data/payment.js b/Data/payment.js new file mode 100644 index 0000000..d913295 --- /dev/null +++ b/Data/payment.js @@ -0,0 +1,8 @@ +const entries = { + 1: { name: "Corsaire Coins (200)", unit_amount: 1000, amount: 200 }, + 2: { name: "Corsaire Coins (450)", unit_amount: 2000, amount: 450 }, + 3: { name: "Corsaire Coins (1000)", unit_amount: 3000, amount: 1000 }, + 4: { name: "Corsaire Coins (2000)", unit_amount: 4000, amount: 2000 } +}; + +module.exports = entries; \ No newline at end of file diff --git a/Data/queries.js b/Data/queries.js new file mode 100644 index 0000000..5e0a4ae --- /dev/null +++ b/Data/queries.js @@ -0,0 +1,30 @@ +module.exports.QUERY_FETCH_RESOURCES = "SELECT categoryID, itemID AS id, amount FROM resources WHERE playerID = ?"; +module.exports.QUERY_FETCH_PLAYER_DATA = "SELECT * FROM playerdata WHERE playerID = ?"; +module.exports.QUERY_FETCH_PLAYER_INFORMATION = "SELECT extensions.cannonSlots FROM extensions WHERE playerID = ?"; +module.exports.QUERY_FETCH_GUILD_INFORMATION = "SELECT guilds.id, guilds.tag FROM guilds JOIN guildmembers ON guilds.id = guildmembers.guildID WHERE guildmembers.playerID = ?"; +module.exports.QUERY_INSERT_DEFAULT_PLAYER = "INSERT INTO playerdata (playerID, x, y, mapID) VALUES (?, ?, ?, ?)"; +module.exports.QUERY_FETCH_EQUIPMENTS = "SELECT itemID AS id, categoryID, amountEquipped FROM equipments WHERE playerID = ?"; +module.exports.QUERY_FETCH_NPC_SPAWN = "SELECT npc.id, npc.parentEntityTypeId, npc.name, npc.speed, npc.minHealth, npc.maxHealth, spawn.amount, spawn.respawnTime, reward.categoryId, reward.itemId, reward.min, reward.max, reward.chance, reward.isLastShot FROM spawn INNER JOIN npc ON spawn.entityTypeId = npc.id LEFT JOIN reward ON spawn.id = reward.rewardId WHERE mapId IN (?, 0) AND typeId = ?"; +module.exports.QUERY_FETCH_MONSTER_SPAWN = "SELECT monster.id, monster.parentEntityTypeId, monster.name, monster.minHealth, monster.maxHealth, spawn.amount, spawn.respawnTime, reward.categoryId, reward.itemId, reward.min, reward.max, reward.chance, reward.isLastShot FROM spawn INNER JOIN monster ON spawn.entityTypeId = monster.id LEFT JOIN reward ON spawn.id = reward.rewardId WHERE mapId IN (?, 0) AND typeId = ?"; +module.exports.QUERY_FETCH_COLLECTABLE_SPAWN = "SELECT collectable.id, collectable.name, spawn.amount, spawn.respawnTime, reward.categoryId, reward.itemId, reward.min, reward.max, reward.chance FROM spawn INNER JOIN collectable ON spawn.entityTypeId = collectable.id LEFT JOIN reward ON spawn.id = reward.rewardId WHERE mapId = ? AND typeId = ?"; +module.exports.QUERY_FETCH_ACTIVE_QUESTS = "SELECT quests.questID, completedAmount, CAST(state AS UNSIGNED) AS state, questprogression.amount, questprogression.taskIndex, questprogression.taskType FROM quests LEFT JOIN questprogression ON quests.questID = questprogression.questID WHERE quests.playerID = ?"; +module.exports.QUERY_FETCH_STATUS = "SELECT statusID FROM status WHERE playerID = ? AND isActive = ?"; +module.exports.QUERY_FETCH_GUILD_REQUESTS = "SELECT guilds.id, guildrequests.playerID, guildrequests.message FROM guildrequests INNER JOIN guilds ON guildrequests.guildID = guilds.id"; +module.exports.QUERY_FETCH_GUILD_MEMBERS = "SELECT guilds.id, BINARY(guilds.name) AS name, BINARY(guilds.tag) AS tag, BINARY(guilds.description) AS description, guilds.createdBy, guilds.taxRateGold / 100 AS taxRateGold, guilds.taxRateEmerald / 100 AS taxRateEmerald, guilds.taxRatesEditedAt, guilds.tagEditedAt, guilds.nameEditedAt, guilds.descriptionEditedAt, guilds.descriptionEditedAt, guildmembers.playerID, guildmembers.permission FROM guildmembers INNER JOIN guilds ON guildmembers.guildID = guilds.id"; +module.exports.QUERY_FETCH_GUILD_ISLANDS = "SELECT * FROM guildislands"; +module.exports.QUERY_FETCH_GUILD_ISLAND_DAMAGES = "SELECT * FROM guildislanddamages"; +module.exports.QUERY_FETCH_GUILD_DIPLOMACIES = "SELECT guildID, targetGuildID, CONV(HEX(color), 16, 10) + 0 AS color FROM guilddiplomacy JOIN guilds ON guilddiplomacy.guildID = guilds.id"; +module.exports.QUERY_FETCH_GUILD_RESOURCES = "SELECT guildID, categoryID, itemID AS id, amount FROM guildresources"; +module.exports.QUERY_FETCH_GUILD_TOWERS = "SELECT gi.mapID, gt.id, gt.guildIslandID, gt.entityTypeID, gt.currentHitpoints, gt.positionX, gt.positionY FROM guildtowers gt INNER JOIN guildislands gi ON gt.guildIslandID = gi.id WHERE gi.mapID = ?"; +module.exports.QUERY_UPDATE_GUILD_ISLAND_TOWER_DEAD = "UPDATE guildtowers SET currentHitpoints = ? WHERE id = ?"; +module.exports.QUERY_UPDATE_GUILD_ISLAND_TOWER_UPGRADE = "UPDATE guildtowers SET entityTypeID = ?, currentHitpoints = ? WHERE id = ?"; +module.exports.QUERY_UPDATE_GUILD_ISLAND_TOWER_DEFAULT = "UPDATE guildtowers SET currentHitpoints = ? WHERE guildIslandID = ?"; +module.exports.QUERY_DELETE_GUILD_ISLAND_DAMAGES = "DELETE FROM guildislanddamages WHERE guildIslandID = ?"; +module.exports.QUERY_UPDATE_GUILD_ISLAND_AUTHOR = "UPDATE guildislands SET guildID = ? WHERE id = ?"; +module.exports.QUERY_UPDATE_GUILD_ISLAND_DAMAGES = "INSERT INTO guildislanddamages (damage, guildIslandID, guildID) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE damage = damage + ?"; +module.exports.QUERY_UPDATE_GUILD_RESOURCE_SINGLE = "INSERT INTO guildresources (guildID, categoryID, itemID, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?"; +module.exports.QUERY_UPDATE_PLAYER_RESOURCE_SINGLE = "INSERT INTO resources (playerID, categoryID, itemID, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?"; +module.exports.QUERY_UPDATE_GUILD_TAX_RATES = "UPDATE guilds SET taxRateGold = ?, taxRateEmerald = ? WHERE id = ?"; +module.exports.QUERY_DELETE_GUILD_MEMBER = "DELETE FROM guildmembers WHERE playerID = ? AND isLeader = ?"; +module.exports.QUERY_FETCH_GUILD_OFFLINE_MEMBERS = "SELECT accounts.id, username, itemID, amount FROM resources INNER JOIN accounts on resources.playerID = accounts.id WHERE categoryID = 6 AND itemID = 1 AND playerID IN (?)"; +module.exports.QUERY_FETCH_SHOP_ENTRIES = "SELECT * FROM shop"; \ No newline at end of file diff --git a/Data/quests.js b/Data/quests.js new file mode 100644 index 0000000..f2c2976 --- /dev/null +++ b/Data/quests.js @@ -0,0 +1,97 @@ +const quest = { + id: 1, + maximumComplete: 1, + minimumLevel: 1, + maximumLevel: 99, + task: { + 1: [ + { typeID: 2, entityTypeID: 1, requiredAmount: 200, amount: 0 }, + { typeID: 3, entityTypeID: 1, requiredAmount: 5000, amount: 0 }, + + ], + 2: [ + { typeID: 5, entityTypeID: 1, requiredAmount: 3, amount: 0 }, + { typeID: 3, entityTypeID: 3, requiredAmount: 3, amount: 0 } + ] + }, + reward: { + 3: { + 5: { amountMinimum: 69, chance: 100 } + }, + 5: { + 18: { amountMinimum: 1, chance: 100 } + }, + 6: { + 1: { amountMinimum: 100, chance: 100 } + }, + 7: { + 1: { amountMinimum: 3, chance: 100 } + }, + 8: { + 1: { amountMinimum: 3, chance: 100 } + } + } +}; + +const quest2 = { + id: 2, + maximumComplete: 3, + minimumLevel: 1, + maximumLevel: 2, + task: { + 1: [ + { typeID: 2, entityTypeID: 3, requiredAmount: 2000, amount: 0 }, + { typeID: 2, entityTypeID: 5, requiredAmount: 1500, amount: 0 }, + ], + 2: [ + { typeID: 5, entityTypeID: 1, requiredAmount: 10, amount: 0 }, + { typeID: 3, entityTypeID: 3, requiredAmount: 3, amount: 0 } + ] + }, + reward: { + 5: { + 6: { amountMinimum: 1, chance: 100 } + } + } +}; + +const quest3 = { + id: 3, + maximumComplete: 2, + minimumLevel: 1, + maximumLevel: 99, + task: { + 2: [ + { typeID: 5, entityTypeID: 1, requiredAmount: 10, amount: 0 } + ] + }, + reward: { + 3: { + 1: { amountMinimum: 1000, chance: 100 }, + 2: { amountMinimum: 2000, chance: 100 }, + 5: { amountMinimum: 2700, chance: 100 }, + }, + 5: { + 35: { amountMinimum: 1, chance: 100 } + } + } +}; + +const quest4 = { + id: 4, + maximumComplete: 1, + minimumLevel: 4, + maximumLevel: 4, + task: { + 2: [ + { typeID: 5, entityTypeID: 1, requiredAmount: 3, amount: 0 } + ] + }, + reward: { + 5: { + 34: { amountMinimum: 1, chance: 100 } + } + } +}; + +module.exports = [quest, quest2, quest3, quest4]; \ No newline at end of file diff --git a/Data/schemeTower.js b/Data/schemeTower.js new file mode 100644 index 0000000..f09e897 --- /dev/null +++ b/Data/schemeTower.js @@ -0,0 +1,49 @@ +const schemeTower = { + 1: { + maximumHitpoints: 5000, + }, + 2: { + maximumHitpoints: 15000, + }, + 3: { + maximumHitpoints: 25000, + }, + 4: { + maximumHitpoints: 35000, + }, + 5: { + maximumHitpoints: 45000, + }, + 6: { + maximumHitpoints: 55000, + }, + 7: { + maximumHitpoints: 65000, + }, + 8: { + maximumHitpoints: 75000, + }, + 9: { + maximumHitpoints: 85000, + }, + 10: { + maximumHitpoints: 95000, + }, + 11: { + maximumHitpoints: 105000, + }, + 12: { + maximumHitpoints: 115000, + }, + 13: { + maximumHitpoints: 125000, + }, + 14: { + maximumHitpoints: 135000, + }, + 15: { + maximumHitpoints: 145000, + } +}; + +module.exports = schemeTower; \ No newline at end of file diff --git a/Data/shop.js b/Data/shop.js new file mode 100644 index 0000000..4d755b6 --- /dev/null +++ b/Data/shop.js @@ -0,0 +1,123 @@ +const constants = require("./constants"); +const { CATEGORIES } = constants; +const { CATEGORY_ITEM, CATEGORY_AMMUNITION, CATEGORY_HARPOON, CATEGORY_DESIGN, CATEGORY_CANNON, CATEGORY_HARPOONER } = CATEGORIES; + +const SHOP = []; + +const SHOP_ENTRIES_BUY = { + +}; + +module.exports = { + SHOP, + SHOP_ENTRIES_BUY +}; +/* +const SHOP = { + [CATEGORY_ITEM]: { + + }, + [CATEGORY_AMMUNITION]: { + 1: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 1500, + maximumGlobalAmount: 2500, + shopCategoryID: 4, + isAvailable: () => true + }, + 2: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 3000, + maximumGlobalAmount: 1000, + shopCategoryID: 4, + isAvailable: () => true + }, + }, + [CATEGORY_HARPOON]: { + 1: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 2000, + shopCategoryID: 6, + isAvailable: () => true + }, + 2: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 2000, + shopCategoryID: 6, + isAvailable: () => true + }, + 3: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 2000, + shopCategoryID: 6, + isAvailable: () => true + }, + 4: { + currency: 1, + unitPrice: 21, + maximumPlayerAmount: 2000, + shopCategoryID: 6, + isAvailable: () => true + } + }, + [CATEGORY_DESIGN]: { + 2: { + currency: 1, + unitPrice: 200_000, + maximumPlayerAmount: 1, + shopCategoryID: 2, + isAvailable: (player) => !player.inventory.hasDesign(2) + }, + 3: { + currency: 1, + unitPrice: 400_000, + maximumPlayerAmount: 1, + shopCategoryID: 2, + isAvailable: (player) => !player.inventory.hasDesign(3) + }, + 4: { + currency: 1, + unitPrice: 600_000, + maximumPlayerAmount: 1, + shopCategoryID: 2, + isAvailable: (player) => !player.inventory.hasDesign(4) + }, + 5: { + currency: 2, + unitPrice: 60_000, + maximumPlayerAmount: 1, + shopCategoryID: 2, + isAvailable: (player) => !player.inventory.hasDesign(5) + } + }, + [CATEGORY_CANNON]: { + 1: { + currency: 1, + unitPrice: 1_000_000, + maximumPlayerAmount: 10, + shopCategoryID: 3, + isAvailable: () => true + }, + 2: { + currency: 1, + unitPrice: 1_000_000, + maximumPlayerAmount: 10, + shopCategoryID: 3, + isAvailable: () => true + }, + }, + [CATEGORY_HARPOONER]: { + 1: { + currency: 1, + unitPrice: 1_000_000, + maximumPlayerAmount: 10, + shopCategoryID: 5, + isAvailable: () => true + } + } +};*/ \ No newline at end of file diff --git a/Data/startRewards.js b/Data/startRewards.js new file mode 100644 index 0000000..c92ccfa --- /dev/null +++ b/Data/startRewards.js @@ -0,0 +1,12 @@ +const startRewards = [ + { categoryID: 1, id: 1, amount: 100_000 }, + { categoryID: 1, id: 2, amount: 1_000 }, + { categoryID: 7, id: 1, amount: 20 }, + { categoryID: 8, id: 1, amount: 10 }, + { categoryID: 3, id: 1, amount: 2_000 }, + { categoryID: 4, id: 1, amount: 1_000 }, + // { categoryID: 7, id: 10, amount: 50 }, + // { categoryID: 3, id: 3, amount: 99999999 }, +]; + +module.exports = startRewards; \ No newline at end of file diff --git a/Data/stripe.js b/Data/stripe.js new file mode 100644 index 0000000..ea6e655 --- /dev/null +++ b/Data/stripe.js @@ -0,0 +1,16 @@ +const STRIPE_IP_COLLECTION = { + "3.18.12.63": true, + "3.130.192.231": true, + "13.235.14.237": true, + "13.235.122.149": true, + "18.211.135.69": true, + "35.154.171.200": true, + "52.15.183.38": true, + "54.88.130.119": true, + "54.88.130.237": true, + "54.187.174.169": true, + "54.187.205.235": true, + "54.187.216.72": true +}; + +module.exports = STRIPE_IP_COLLECTION; \ No newline at end of file diff --git a/Events/close.js b/Events/close.js new file mode 100644 index 0000000..cb7c847 --- /dev/null +++ b/Events/close.js @@ -0,0 +1,42 @@ +const Game = require("../Game/Game"); +const { players } = Game; + +const extractBehaviourFeatures = require("../Utility/extractBehaviourFeatures"); + +// const NORMAL_CLOSE_CODES = [0, 1, 2]; + +module.exports = function (ws, code, message) { + if (code === 1337) return; + + const player = players[ws.accountID]; + if (!player) { + ws.isInterrupted = true; + delete socket.sockets[ws.id]; + + return; + }; + + const playerActions = global.actions[player.id]; + const features = extractBehaviourFeatures(playerActions); + + delete global.actions[player.id]; + + if (features) { + features.playerID = player.id; + features.timestampStart = new Date(ws.connectedAt); + + global.StatisticsWorker.postMessage({ + id: 2, + data: features + }); + }; + + player.inDistance = {}; + player.isAvailable = false; + player.hasUnexpectedlyLeft = true; + + const individualPackets = player.map.networkPacketsInvidiual[player.id]; + if (individualPackets) individualPackets.length = 0; + console.info("close event", performance.now()); + delete socket.sockets[ws.id]; +}; \ No newline at end of file diff --git a/Events/drain.js b/Events/drain.js new file mode 100644 index 0000000..aafcbda --- /dev/null +++ b/Events/drain.js @@ -0,0 +1,3 @@ +module.exports = function (ws) { + console.info("Drain?"); +}; \ No newline at end of file diff --git a/Events/message.js b/Events/message.js new file mode 100644 index 0000000..8d77367 --- /dev/null +++ b/Events/message.js @@ -0,0 +1,94 @@ +const packetTable = require("../Data/packets"); +const { players } = require("../Game/Game"); + +const actions = []; +global.actions = actions; + +const { MongoClient } = require("mongodb"); +let db = null; + +async function connect() { + const client = new MongoClient("mongodb://localhost:27017"); + await client.connect({ + maxPoolSize: 100, + minPoolSize: 20, + maxConnecting: 20, + waitQueueTimeoutMS: 0, + socketTimeoutMS: 30000, + connectTimeoutMS: 10000, + retryWrites: false, + directConnection: true + }); + + return client.db('test'); +}; + +(async function() { + db = await connect(); + global.mongodb = db; + + return; + setInterval(async () => { + const t = []; + for (let i = 0; i < 32; i++) { + t.push({ + playerID: 1, + timestamp: new Date(), + opcode: Math.floor(Math.random() * 120) + }); + }; + + const bulkOps = t.map(doc => ({ + insertOne: doc + })); + + const n = performance.now(); + await db.collection("Actions").insertMany(t, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + /* await db.collection('Actions').bulkWrite(bulkOps, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + });*/ + }, 500); +})(); + +module.exports = async function (ws, message, isBinary) { + const player = players[ws.accountID]; + if (!player) return; + + const isLimitReached = ws.limiter.update(); + if (isLimitReached) return ws.close(); + + if (player.isInactive) return; // this shouldn't happen, though + + const view = new DataView(message); + const packetID = view.getUint8(0); + const packet = packetTable[packetID]; + + if (packetID !== 1 && !player.isAvailable) return; + if (!packet) return; + if (packet.expectedSize && packet.expectedSize !== view.byteLength) return console.info("expectedSize", view.byteLength, packet.expectedSize); + if (!packet.isAvailable) return; + + player.lastInteractionTimestamp = performance.now(); + + const metadata = packet.handler(ws, view, player) ?? []; + + if (packetID > 1) { + const action = { + ts: new Date(), + pid: player.id, + opcode: packetID, + metadata + }; + + const relevantContext = global.actionContext.getElement(player.id).save(2000); + relevantContext.push(action); + console.info("asd", relevantContext.length) + }; +}; \ No newline at end of file diff --git a/Events/open.js b/Events/open.js new file mode 100644 index 0000000..63441f6 --- /dev/null +++ b/Events/open.js @@ -0,0 +1,239 @@ +const Limiter = require("../Game/PacketLimiter"); +const EquipmentLimiter = require("../Game/EquipmentLimiter"); +const Inventory = require("../Game/Inventory"); +const Player = require("../Game/Player"); +const Game = require("../Game/Game"); + +const playerHandlers = require("../Handlers/Player"); + +const EnvironmentSetupPacket = require("../Packets/EnvironmentSetup"); + +const rewardCallback = require("../Callbacks/reward"); +const attackCallback = require("../Callbacks/attack"); + +const baseStarterRewards = require("../Data/startRewards"); + +const { maps, players, guilds } = Game; + +const queries = require("../Data/queries"); +const { QUERY_FETCH_PLAYER_DATA, QUERY_FETCH_PLAYER_INFORMATION, QUERY_FETCH_GUILD_INFORMATION, QUERY_INSERT_DEFAULT_PLAYER, QUERY_FETCH_ACTIVE_QUESTS, QUERY_FETCH_STATUS, QUERY_FETCH_RESOURCES, QUERY_FETCH_EQUIPMENTS } = queries; + +const playerEvents = Object.keys(playerHandlers); + +const PlayerBehaviour = require("../Game/PlayerBehaviour"); +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +global.actionContext = new DoubleLinkedList(); + +module.exports = async function (ws) { + console.info("Before open for", ws.username, Game.state, ws.isAdmin); + + if (Game.state === 0 && !ws.isAdmin) return ws.end(1337); + + ws.connectedAt = Date.now(); + let abruptDisconnectTest = performance.now(); + + const ID = ws.accountID; + const sessionID = Date.now().toString(36) + Math.random().toString(36).substring(2) + "!" + ID; + + socket.sockets[sessionID] = ws; + + ws.id = sessionID; + ws.limiter = new Limiter({ + countThreshold: 10, + timeThreshold: 1000 + }); + + const alreadyConnectedPlayer = players[ID]; + if (alreadyConnectedPlayer) { + const player = alreadyConnectedPlayer; + + const alreadyConnectedSoket = socket.sockets[player.sessionID]; + if (alreadyConnectedSoket) alreadyConnectedSoket.end(1); + + player.sessionID = sessionID; + ws.subscribe(player.map.id.toString()); + + player.map.removePlayer(player.id); + player.map.addPlayer(player); + + player.isUnderDisconnect = false; + + player.cancelDisconnect(); + + const packet = EnvironmentSetupPacket(player.map.id); + return ws.send(packet, true, true); + }; + + const playerBehaviour = new PlayerBehaviour({ + size: 16, + buffer: global.playerBehaviourContext, + id: ID + }); + + global.actionContext.addItem(ID, playerBehaviour); + + const generalQuery = execute(QUERY_FETCH_PLAYER_DATA, [ID]); + const informationQuery = execute(QUERY_FETCH_PLAYER_INFORMATION, [ID]); + const guildQuery = execute(QUERY_FETCH_GUILD_INFORMATION, [ID]); + const questQuery = execute(QUERY_FETCH_ACTIVE_QUESTS, [ID]); + const statusQuery = execute(QUERY_FETCH_STATUS, [ID, true]); + const resourcesQuery = execute(QUERY_FETCH_RESOURCES, [ID]); + const equipmentsQuery = execute(QUERY_FETCH_EQUIPMENTS, [ID]); + + const res = await Promise.all([generalQuery, informationQuery, guildQuery, questQuery, statusQuery, resourcesQuery, equipmentsQuery]); + const hasError = res.some(q => q.error !== undefined); + + if (hasError) return; + if (ws.isInterrupted) return; + + let isAlive = false; + let map = null; + let hitpoints = { + current: 0, + maximum: 0 + }; + let position = {}; + let attemptToDoubleLog = false; + let maximumCannonRange = 0; + let maximumHarpoonerRange = 0; + let textureID = 0; + let sight = 0; + let speed = 0; + let globalRank = 0; + let levelRank = 0; + + const playerFromDB = res[0].data[0]; + const guildFromDB = res[2].data[0]; + const questFromDB = res[3].data; + const statusFromDB = res[4].data; + const resourcesFromDB = res[5].data; + const equipmentsFromDB = res[6].data; + + const statusPoints = []; + + const guildID = guildFromDB ? guildFromDB.id : 0; + const guild = guilds[guildID]; + + const selectedAmmunition = playerFromDB ? playerFromDB.selectedAmmunitionID : 0; + const selectedHarpoon = playerFromDB ? playerFromDB.selectedHarpoonID : 0; + + const resourceSharedLimiter = { + cannon: new EquipmentLimiter({ + maximum: 50 + }), + harpooner: new EquipmentLimiter({ + maximum: 25 + }), + sail: new EquipmentLimiter({ + maximum: 5 + }), + pirate: new EquipmentLimiter({ + maximum: 250 + }) + }; + + const inventory = new Inventory({ + resourceSharedLimiter + }); + + resourcesFromDB.forEach(r => { + switch (r.categoryID) { + case 6: + statusPoints.push(r); + break; + + case 7: + case 8: + const equipment = equipmentsFromDB.find(e => e.id === r.id && e.categoryID === r.categoryID); + r.amountEquipped = equipment ? equipment.amountEquipped : 0; + break; + }; + + inventory.refreshSingle(r); + }); + + inventory.setSelectedAmmunition(selectedAmmunition); + inventory.setSelectedHarpoon(selectedHarpoon); + + if (!attemptToDoubleLog) { + const playerHasData = playerFromDB ? true : false; + if (!playerHasData) { + const startingMap = maps.find(m => m.id === 1); + const { x, y } = startingMap.grid.randomNode(10); + + textureID = 1; + sight = 450; + speed = 250; + globalRank = 1; + levelRank = 1; + isAlive = true; + map = startingMap; + position = { + x, + y + }; + hitpoints = { + current: 10000, + maximum: 50000 + }; + + baseStarterRewards.forEach(r => inventory.refreshSingle(r)); + + await execute(QUERY_INSERT_DEFAULT_PLAYER, [ID, x, y, 1]); + } else { + textureID = playerFromDB.designID; + sight = playerFromDB.sight; + speed = playerFromDB.speed; + globalRank = playerFromDB.globalRank; + levelRank = playerFromDB.levelRank; + isAlive = !!hitpoints; + map = maps.find(m => m.id === playerFromDB.mapID); + + position = { + x: playerFromDB.x, + y: playerFromDB.y + }; + + hitpoints = { + current: playerFromDB.currentHitpoints, + maximum: playerFromDB.maximumHitpoints + }; + }; + }; + + const player = new Player({ + id: ID, + sessionID: ws.id, + name: ws.username, + textureID, + map, + guild, + positionX: position.x, + positionY: position.y, + isAlive, + hitpoints, + maximumCannonRange, + maximumHarpoonerRange, + sight, + speed, + globalRank, + levelRank, + inventory, + statusPoints, + activeStatusEffects: statusFromDB, + quests: questFromDB, + rewardCallback, + attackCallback + }); + + player.setDesign(textureID); + + ws.subscribe(player.map.id.toString()); + + playerEvents.forEach(e => player.on(e, playerHandlers[e])); + players[ID] = player; + + const packet = EnvironmentSetupPacket(player.map.id); + ws.send(packet, true, true); +}; diff --git a/Events/upgrade.js b/Events/upgrade.js new file mode 100644 index 0000000..5743c93 --- /dev/null +++ b/Events/upgrade.js @@ -0,0 +1,64 @@ +const crypto = require("crypto"); + +const redis = require("../Miscellaneous/redis"); + +const Game = require("../Game/Game"); + +const HTTP_BAD_REQUEST = "403"; + +function badRequest(res) { + res.cork(() => res.isAborted ? null : res + .writeStatus(HTTP_BAD_REQUEST) + .end()); +}; + +module.exports = async function (res, req, context) { + const cookieHeader = req.getHeader("cookie"); + const cookie = cookieHeader.split("ps=")[1]; + + res.onAborted(() => { + res.isAborted = true; + console.info("ABORTED!!!"); + }); + + if (!cookie) { + console.info("upgrade 1") + return badRequest(res); + }; + + const secWebSocketKey = req.getHeader("sec-websocket-key"); + const secWebSocketProtocol = req.getHeader("sec-websocket-protocol"); + const secWebSocketExtensions = req.getHeader("sec-websocket-extensions"); + + const userAgent = req.getHeader("user-agent"); + const ip = req.getHeader("x-forwarded-for"); + + const formattedCookie = cookie.split(".")[0].slice(4); + const cookieInDB = await redis.get(`sess:${formattedCookie}`); + if (!cookieInDB) { + console.info("upgrade 2") + + return badRequest(res); + }; + + const parsedCookie = JSON.parse(cookieInDB); + if (!parsedCookie.accountID) { + console.info("upgrade 3") + + return badRequest(res); + }; + + const hash = crypto.createHash("md5") + .update(userAgent) + .update(ip) + .digest("hex"); + + // if (hash !== parsedCookie.fingerprint) return badRequest(res); + const { accountID, username, isAdmin } = parsedCookie; + console.info("Before upgrading connection for", username); + res.cork(() => res.upgrade({ accountID, username, cookie, isAdmin }, + secWebSocketKey, + secWebSocketProtocol, + secWebSocketExtensions, + context)); +}; diff --git a/Game/AdmiralCounter.js b/Game/AdmiralCounter.js new file mode 100644 index 0000000..a68d1fa --- /dev/null +++ b/Game/AdmiralCounter.js @@ -0,0 +1,39 @@ +class AdmiralCounter { + constructor(configuration) { + this.entities = Object.entries(configuration); + this.admiralCounters = {}; + } + + increaseCounter(ids) { + const { typeID, parentEntityTypeID } = ids; + const id = `${typeID}_${parentEntityTypeID}`; + + if (!this.admiralCounters[id]) this.admiralCounters[id] = { childKilledCount: 1, admiralAliveCount: 0 }; + else this.admiralCounters[id].childKilledCount++; + + const thresholds = this.entities.find(e => parseInt(e[0]) === typeID); + if (!thresholds) return; + + const threshold = thresholds[1][parentEntityTypeID]; + if (!threshold) return; + + const counter = this.admiralCounters[id]; + if (counter.childKilledCount === threshold.childToBeKilledCount) { + counter.childKilledCount = 0; + + if (counter.admiralAliveCount < threshold.maximumAdmiralCount) { + counter.admiralAliveCount++; + return true; + }; + }; + + return false; + } + + descreaseAdmiralCount(id) { + const counter = this.admiralCounters[id]; + if (counter) counter.admiralAliveCount--; + } +}; + +module.exports = AdmiralCounter; \ No newline at end of file diff --git a/Game/Cannon.js b/Game/Cannon.js new file mode 100644 index 0000000..b03ffa1 --- /dev/null +++ b/Game/Cannon.js @@ -0,0 +1,71 @@ +const EventEmitter = require("events"); + +const constants = require("../Data/constants"); +const { CATEGORIES } = constants; +const { CATEGORY_CANNON } = CATEGORIES; + +const eventsMapped = { + 1: 97, + 2: 98, + 3: 99, + 4: 100, + 5: 101, + 6: 102, + 7: 103, + 8: 104, + 9: 105, + 10: 106 +}; + +class Cannon extends EventEmitter { + constructor(info) { + super(); + + this.id = info.id; + this.categoryID = CATEGORY_CANNON; + this.eventID = eventsMapped[this.id]; + this.damage = info.damage; + this.range = info.range; + this.reload = info.reload; + this.scatter = info.scatter; + this.amount = info.amount; + this.amountEquipped = info.amountEquipped || 0; + this.lastShotTickTimestamp = 0; + this.resourceSharedLimiter = info.resourceSharedLimiter;; + }; + + equip(quantity) { + if (this.amountEquipped + quantity > this.amount) return + + const isTypeQunatityExhausted = this.resourceSharedLimiter.count(quantity); + if (!isTypeQunatityExhausted) return + + + this.amountEquipped += quantity; + + return true; + } + + unequip(quantity) { + if (this.amountEquipped - quantity < 0) return + + const isTypeQunatityExhausted = this.resourceSharedLimiter.count(-quantity); + if (!isTypeQunatityExhausted) return + + + this.amountEquipped -= quantity; + + return true; + } + + getProperties() { + return { + typeID: 1, + id: this.id, + amount: this.amount, + amountEquipped: this.amountEquipped + }; + } +}; + +module.exports = Cannon; \ No newline at end of file diff --git a/Game/Collectable.js b/Game/Collectable.js new file mode 100644 index 0000000..a5b0ae9 --- /dev/null +++ b/Game/Collectable.js @@ -0,0 +1,70 @@ +const Entity = require("./Entity"); + +const Reward = require("./Reward"); + +const constants = require("../Data/constants"); + +const { TYPES, TIMING_EVENTS } = constants; +const { ENTITY_TYPE_COLLECTABLE } = TYPES; +const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS; + +class Collectable extends Entity { + constructor(info) { + super({ + type: ENTITY_TYPE_COLLECTABLE, + id: info.id, + map: info.map, + isAlive: true, + position: info.position + }); + + this.respawnTime = 10000; + this.entityTypeID = info.entityTypeID; + this.dieCallback = info.dieCallback; + this.rewardCallback = info.rewardCallback; + this.reward = new Reward({ + distributionType: 2, + baseReward: info.rewardEntries.reduce((result, entry) => { + if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance + }; + else result[entry.categoryId] = { + [entry.itemId]: { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance + } + }; + + return result; + }, {}) + }); + + this.map.collectables.set(this.id, this); + } + + die(deathEvent) { + super.die(); + + const die = { + typeID: this.typeID, + entityTypeID: this.entityTypeID, + id: this.id, + authorID: deathEvent.authorID, + targetEntityTypeID: this.entityTypeID + }; + + this.dieCallback(die); + + this.setSingleEvent(TIMING_EVENT_RESPAWN, this.respawnTime); + } + + getProperties() { + const { position, type, typeID, entityTypeID, id } = this; + return { position, type, typeID, entityTypeID, id }; + } +}; + +module.exports = Collectable; \ No newline at end of file diff --git a/Game/Entity.js b/Game/Entity.js new file mode 100644 index 0000000..924cc94 --- /dev/null +++ b/Game/Entity.js @@ -0,0 +1,106 @@ +const EventScheduler = require("./EventScheduler"); + +const constants = require("../Data/constants"); +const { TYPES, TIMING_EVENTS } = constants; +const { ENTITY_TYPE_PLAYER } = TYPES; +const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS; + +class Entity extends EventScheduler { + constructor(info) { + const id = info.type === ENTITY_TYPE_PLAYER ? info.id : Entity.composeID(info.type - 1, info.id); + super(info.type, id, info.map.id); + + this.isAlive = info.isAlive ?? true; + this.chunk = null; + this.type = null; + this.map = info.map; + this.typeID = info.type; + this.id = id; + + const { map } = this; + + if (info.position) { + const cell = info.map.grid.getCell(info.position.x, info.position.y); + const coordinate = info.map.grid.getCoords(cell.xw, cell.xh) + + this.position = coordinate; + } else this.position = map.grid.randomNode(10); + + if (info.type !== ENTITY_TYPE_PLAYER) { + const currentChunk = this.getCurrentChunk(); + map[currentChunk].addItem(id, this); + + this.chunk = currentChunk; + }; + }; + + static composeID(entityType, entityID) { + return ((entityType & 0xFF) << 24) | (entityID & 0xFFFFFF); + } + + static getType(compositeID) { + return (compositeID >> 24) & 0xFF; + } + + static getID(compositeID) { + return compositeID & 0xFFFFFF; + } + + getCurrentChunk() { + return `chunk_${Math.floor(this.position.x / (10 * 120))}-${Math.floor(this.position.y / (10 * 90))}`; + } + + removeFromChunk() { + if (!this.chunk) return; + + this.map[this.chunk].removeItem(this.id); + + this.chunk = null; + } + + updateChunk() { + const { chunk, map, id } = this; + + const currentChunk = this.getCurrentChunk(); + if (chunk !== currentChunk) { + this.removeFromChunk(); + + map[currentChunk].addItem(id, this); + this.chunk = currentChunk; + + this.chunkIndex = [Math.floor(this.position.x / (10 * 120)), Math.floor(this.position.y / (10 * 90))]; + + return true; + }; + + return false; + } + + die() { + if (!this.isAlive) return; + + this.isAlive = false; + + this.removeFromChunk(); + + const players = this.map.getLivingEntities([ENTITY_TYPE_PLAYER]); + players.forEach(p => delete p.inDistance[this.id]); // this is not efficient, should update + } + + respawn(respawn = {}) { + if (this.isAlive) return; + + this.isAlive = true; + this.position = respawn.position ? respawn.position : this.map.grid.randomNode(10); + + this.updateChunk(); + + this.removeEvent(TIMING_EVENT_RESPAWN); + } + + markActive() { + this.map[`chunkTest_${Math.floor(this.position.x / (10 * 120))}-${Math.floor(this.position.y / (10 * 90))}`].addItem(this.id, this); + } +}; + +module.exports = Entity; \ No newline at end of file diff --git a/Game/EntityGroup.js b/Game/EntityGroup.js new file mode 100644 index 0000000..a9ecfa9 --- /dev/null +++ b/Game/EntityGroup.js @@ -0,0 +1,96 @@ +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +class EntityGroup { + constructor(info) { + this.id = info.id; + this.mapIDs = Array.isArray(info.mapIDs) ? info.mapIDs : []; + this.structure = info.structure; + this.entitySettings = info.entitySettings; + this.members = new DoubleLinkedList(); + this.leaders = new DoubleLinkedList(); + this.isSummonPossible = true; + this.predefinedArea = info.predefinedArea ? info.predefinedArea : { + x: 1000, + y: 1000, + width: 750, + height: 750 + }; + }; + + setAreaPosition(position) { + this.predefinedArea.x = position.x; + this.predefinedArea.y = position.y; + } + + getEntitySetting(groupMemberTypeID) { + const entitySetting = this.entitySettings[groupMemberTypeID]; + + return entitySetting ? entitySetting : { + speed: 600, + respawnTime: 0 + }; + } + + cancelRespawn() { + if (!this.isSummonPossible) return; + + this.members.each((key, value) => { + value.cancelEvent(8); + + return true; + }); + + this.isSummonPossible = false; + } + + default() { + this.members.clearEach(() => null); + this.leaders.clearEach(() => null); + + this.isSummonPossible = true; + } + + isDefeated() { + let deadEntitiesCount = 0; + + this.members.each((key, value) => { + if (!value.isAlive) deadEntitiesCount++; + + return true; + }); + + this.leaders.each((key, value) => { + if (!value.isAlive) deadEntitiesCount++; + + return true; + }); + + return this.structure.length === deadEntitiesCount || (this.members.isEmpty() && this.leaders.isEmpty()); + } + + addMember(entity) { + this.members.addItem(entity.id, entity); + } + + addLeader(entity) { + this.leaders.addItem(entity.id, entity); + } + + isLeader(id) { + return this.leaders.hasElement(id); + } + + isLeaderAlive() { + let isAlive = false; + + this.leaders.each(function (key, value) { + isAlive = value.isAlive; + + return !value.isAlive; + }); + + return isAlive; + } +}; + +module.exports = EntityGroup; \ No newline at end of file diff --git a/Game/EquipmentLimiter.js b/Game/EquipmentLimiter.js new file mode 100644 index 0000000..1652cef --- /dev/null +++ b/Game/EquipmentLimiter.js @@ -0,0 +1,20 @@ +class EquipmentLimiter { + constructor(info) { + this.quantity = { + current: 0, + maximum: info.maximum + }; + }; + + count(quantity) { + + if (!quantity) return false; + if (this.quantity.current + quantity > this.quantity.maximum || this.quantity.current + quantity < 0) return false; + + this.quantity.current += quantity; + + return true; + } +}; + +module.exports = EquipmentLimiter; \ No newline at end of file diff --git a/Game/EventScheduler.js b/Game/EventScheduler.js new file mode 100644 index 0000000..6b3bb79 --- /dev/null +++ b/Game/EventScheduler.js @@ -0,0 +1,154 @@ +const EventEmitter = require("events"); + +const Game = require("./Game"); + +class EventScheduler extends EventEmitter { + constructor(typeID, id, mapID) { + super(); + + this.entity = { + typeID, + id + }; + + this.events = {}; + this.mapID = mapID; + }; + + setLocalization(mapID) { + this.mapID = mapID; + } + + getLocalization = () => this.mapID; + + setReusableSingleEvent(id, time, data) { + if (!this.events[id] || this.events[id][0].isOver) { + const previousTick = this.getEventTick(id); + const relativeTo = previousTick ? previousTick : performance.now(); + const tickAt = relativeTo + (time > 0 ? time : 0); + + const event = { + id, + entity: this.entity, + isCancelled: false, + index: 0, + tickAt, + data, + getLocalization: this.getLocalization, + isSingle: true, + isOver: false + }; + + Game.events.add(tickAt, event); + this.events[id] = [event]; + } else { + this.events[id][0].isCancelled = false; + }; + } + + setSingleEvent(id, time, data) { + const tickAt = performance.now() + time; + + const event = { + id, + entity: this.entity, + isCancelled: false, + index: 0, + tickAt, + data, + getLocalization: this.getLocalization + }; + + if (!this.events[id]) { + Game.events.add(tickAt, event); + this.events[id] = [event]; + }; + } + + setEventTest2(id, time, data) { + const previousTick = this.getEventTick(id); + const relativeTo = previousTick ? previousTick : performance.now(); + const tickAt = relativeTo + time; + + const event = { + id, + entity: this.entity, + isCancelled: false, + index: this.events[id] ? this.events[id].length - 1 : 0, + tickAt, + data, + getLocalization: this.getLocalization + }; + + Game.events.add(tickAt, event); + this.events[id] = [event]; + } + + setEvent(id, time, data) { + const previousTick = this.getEventTick(id); + const relativeTo = previousTick ? previousTick : performance.now(); + const tickAt = relativeTo + time; + + const event = { + id, + entity: this.entity, + isCancelled: false, + index: this.events[id] ? this.events[id].length - 1 : 0, + tickAt, + data, + getLocalization: this.getLocalization + }; + + Game.events.add(tickAt, event); + + if (this.events[id]) this.events[id].push(event); + else this.events[id] = [event]; + } + + setIndependentEvent(id, time, data) { + const tickAt = performance.now() + time; + + const event = { + id, + entity: this.entity, + isCancelled: false, + index: this.events[id] ? this.events[id].length : 0, + tickAt, + data, + getLocalization: this.getLocalization + }; + + Game.events.add(tickAt, event); + + if (this.events[id]) this.events[id].push(event); + else this.events[id] = [event]; + } + + getEventTick(id, index) { + const event = this.events[id]; + if (!event) return 0; + + if (!index) index = event.length - 1; + + const element = event[index]; + return element && !element.isOver && element.tickAt; + } + + removeEvent(id) { + delete this.events[id]; + } + + cancelEvent(id, index) { + if (this.events[id]) { + if (index == null) { + for (let i = 0; i < this.events[id].length; i++) this.events[id][i].isCancelled = true; + } else if (index === -1) { + for (let i = 0; i < this.events[id].length - 1; i++) this.events[id][i].isCancelled = true; + } else { + this.events[id][index].isCancelled = true; + }; + }; + } +}; + +module.exports = EventScheduler; \ No newline at end of file diff --git a/Game/Game.js b/Game/Game.js new file mode 100644 index 0000000..ff76751 --- /dev/null +++ b/Game/Game.js @@ -0,0 +1,43 @@ +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); +const PQ = require("../Utility/Q"); + +const maps = []; +for (let i = 0; i < 20; i++) maps.push(require(`../Maps/Map_${i + 1}`)); + +const game = { + players: {}, + packets: new DoubleLinkedList(), + guilds: {}, + guildIslands: [], + events: new PQ(), + eventsFixed: new PQ(), + maps, + getTopic: function (topicName) { + const topic = this.packets.getElement(topicName); + if (topic) return topic; + + return this.packets.addItem(topicName, []); + }, + addPlayer: function (player) { + this.players[player.id] = player; + }, + /* testDestroyAll: function() { + this.maps.forEach(map => map.npcs.forEach(npc => npc)) + },*/ + disconnectAll: function() { + this.state = 0; + + this.maps.forEach(map => map.players.forEach(player => player.disconnect(() => { + socket.sockets[player.sessionID].end(2); + delete this.players[player.id]; + delete socket.sockets[player.sessionID]; + }, true))) + + // socket.us_listen_socket_close(this.listeningServer); + // this.listeningServer = null; + }, + listeningServer: null, + state: 1 +}; + +module.exports = game; diff --git a/Game/Grid.js b/Game/Grid.js new file mode 100644 index 0000000..9c8fe27 --- /dev/null +++ b/Game/Grid.js @@ -0,0 +1,125 @@ +class Grid { + constructor(config) { + this.width = config.width; + this.height = config.height; + this.collision = config.collision ?? {}; + } + + getCoords(tileX, tileY) { + const x = 0 + tileX * 24 + !(tileY % 2) * (24 / 2) + const y = 0 + tileY * (18 / 2); + + return { + x, + y + }; + } + + getCell(roundedX, roundedY) { + const x = roundedX - 0; + const y = roundedY - 0; + const isEven = ((Math.floor(y / 9) * 9 - 9) / 18) % 1 !== 0; + const xw = isEven ? Math.floor(x / 24) : Math.floor((x + 12) / 24); + const xh = Math.floor(y / (18 / 2)); + + return { + xw, + xh + }; + } + + isValidNode(node) { + const blockedTile = this.collision[`${node.xw}-${node.xh}`]; + return (!blockedTile && node.xw > 0 && node.xh > 0 && node.xw < this.width / 24 && node.xh < (this.height * 2) / 18); + } + + randomNodeNear(nearToX, nearToY, radius) { + let r = radius * Math.sqrt(Math.random()); + let a = 2 * Math.PI * Math.random(); + + let nearX = Math.round(r * Math.cos(a) + nearToX); + let nearY = Math.round(r * Math.sin(a) + nearToY); + + let triedNode = this.getCell(nearX, nearY); + + const { xw, xh } = triedNode; + let { x, y } = this.getCoords(xw, xh); + + while (!this.isValidNode(triedNode) || (x === nearToX && y === nearToY) || Math.pow(x - nearToX, 2) + Math.pow(y - nearToY, 2) < Math.pow(500, 2)) { + r = radius * Math.sqrt(Math.random()); + a = 2 * Math.PI * Math.random(); + nearX = Math.round(r * Math.cos(a) + nearToX); + nearY = Math.round(r * Math.sin(a) + nearToY); + + triedNode = this.getCell(nearX, nearY); + + const { xw, xh } = triedNode; + const coords = this.getCoords(xw, xh); + x = coords.x; + y = coords.y; + }; + + return { + x, + y + }; + } + + randomNodeInArea(area) { + let xw = 0; + let xh = 0; + + const minimumWidth = area.x - area.width / 2; + const maximumWidth = area.x + area.width / 2; + const minimumHeight = area.y - area.height / 2; + const maximumHeight = area.y + area.height / 2; + + do { + xw = Math.floor((Math.random() * (maximumWidth - minimumWidth + 1) + minimumWidth) / 24); + xh = Math.floor((Math.random() * (maximumHeight - minimumHeight + 1) + minimumHeight) / (18 / 2)); + } while (!this.isValidNode({ xw, xh })); + + return this.getCoords(xw, xh); + } + + randomNode(percentage) { + let xw = 0; + let xh = 0; + + if (!percentage) percentage = 0; + percentage /= 200; + + const minimumWidth = 0 + this.width * percentage; + const maximumWidth = this.width * (1 - percentage); + const minimumHeight = 0 + this.height * percentage; + const maximumHeight = this.height * (1 - percentage); + + do { + xw = Math.floor((Math.random() * (maximumWidth - minimumWidth + 1) + minimumWidth) / 24); + xh = Math.floor((Math.random() * (maximumHeight - minimumHeight + 1) + minimumHeight) / (18 / 2)); + } while (!this.isValidNode({ xw, xh })); + + return this.getCoords(xw, xh); + } + + getNodeNeighbours(node) { + const { xw, xh } = node; + let rightdown, leftdown, rightup, leftup; + + if (node.xh % 2 !== 0) { + rightdown = { xw, xh: xh + 1 }; + leftdown = { xw: xw - 1, xh: xh + 1 }; + rightup = { xw, xh: xh - 1 }; + leftup = { xw: xw - 1, xh: xh - 1 }; + } else { + rightdown = { xw: xw + 1, xh: xh + 1 }; + leftdown = { xw: xw, xh: xh + 1 }; + rightup = { xw: xw + 1, xh: xh - 1 }; + leftup = { xw: xw, xh: xh - 1 }; + }; + + return [rightup, leftup, leftdown, rightdown]; + } +}; + +module.exports = Grid; \ No newline at end of file diff --git a/Game/Grids.js b/Game/Grids.js new file mode 100644 index 0000000..fa215a9 --- /dev/null +++ b/Game/Grids.js @@ -0,0 +1,15 @@ +const Grid = require("./Grid"); +const data = require("../Data/maps"); +const grids = []; + +data.forEach(d => { + const grid = new Grid({ + width: d.size.width, + height: d.size.height, + collision: d.collision + }); + + grids.push(grid); +}) + +module.exports = grids; \ No newline at end of file diff --git a/Game/Guild.js b/Game/Guild.js new file mode 100644 index 0000000..bd5d333 --- /dev/null +++ b/Game/Guild.js @@ -0,0 +1,143 @@ +const InventoryList = require("./InventoryList"); + +const LEVELS = require("../Data/levels"); + +const GUILD_MAXIMUM_REQUESTS_SIZE = 100; + +const ECONOMY = [1, 2]; + +const globalRequests = {}; + +class Guild { + constructor(info) { + this.id = info.id; + this.topic = `g${info.id}`; + this.name = info.name; + this.tag = info.tag; + this.description = info.description; + this.ownerID = info.ownerID; + this.bank = new GuildBank(); + this.taxRates = info.taxRates; + this.experiencePoints = info.experiencePoints ?? 0; + this.level = LEVELS.guildLevel(this.experiencePoints) ?? 1; + this.size = 0; + this.sizeMaximum = 30; + this.members = []; + this.requests = []; + this.diplomacy = {}; + this.timestamps = info.timestamps; + + this.bank.depositTax({ + categoryID: 1, + id: 1, + amount: 0 + }); + + this.bank.depositTax({ + categoryID: 1, + id: 2, + amount: 0 + }); + }; + + levelUpdate(amount) { + this.experiencePoints += amount; + + if (this.level >= 25) return -1; + + const newLevel = LEVELS.guildLevel(this.experiencePoints); + if (newLevel !== this.level) { + if (newLevel > 25) this.level = 25; + else this.level = newLevel; + + return this.level; + }; + + return -1; + } + + isLeader(playerID) { + return this.ownerID === playerID; + } + + hasRequested(playerID) { + const isRequestPresent = this.requests.some(r => r.playerID === playerID); + + return isRequestPresent; + } + + setPermission(playerID, permission) { + const member = this.members.find(m => m.id === playerID); + if (!member) return; + + member.permission = permission; + } + + addRequest(playerID, message) { + //if (this.requestSize >= GUILD_MAXIMUM_REQUESTS_SIZE) return; + if (globalRequests[playerID] >= 8) return; + console.info(globalRequests[playerID], "request count for player", playerID); + + if (globalRequests[playerID]) globalRequests[playerID]++; + else globalRequests[playerID] = 1; + + const request = { + playerID, + message + }; + + this.requests.push(request); + + return true; + } + + removeRequest(playerID) { + if (globalRequests[playerID]) globalRequests[playerID]--; + + this.requests = this.requests.filter(r => r.playerID !== playerID); + } + + isMember(playerID) { + return this.members.find(member => member.id === playerID) ? true : false; + } + + getMember(playerID) { + return this.members.find(member => member.id === playerID); + } + + addMember(member) { + if (this.size >= this.sizeMaximum) return; + if (this.isMember(member.id)) return; + + this.removeRequest(member.id); + this.members.push(member); + + delete globalRequests[member.id]; + + this.size++; + + return true; + } + + removeMember(playerID) { + this.size--; + this.members = this.members.filter(member => member.id !== playerID); + } +}; + +class GuildBank { + constructor(info) { + this.list = new InventoryList(); + this.deposits = []; + }; + + depositTax(resource) { + if (!ECONOMY.includes(resource.id)) return; + + this.list.accumulateItem(resource.id, resource); + } +}; + +module.exports = Guild; + + diff --git a/Game/GuildIsland.js b/Game/GuildIsland.js new file mode 100644 index 0000000..3d1fb08 --- /dev/null +++ b/Game/GuildIsland.js @@ -0,0 +1,69 @@ +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const constants = require("../Data/constants"); + +const { TIMING_EVENTS } = constants; +const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS; + +const Game = require("./Game"); +const { guilds } = Game; + +class GuildIsland { + constructor(info) { + this.id = info.id; + this.locationMapID = info.locationMapID; + this.index = info.index; + this.position = info.position; + this.guild = info.guild; + this.towers = []; + this.totalDamageReceived = new DoubleLinkedList(); + }; + + updateReceivedDamage(guildID, damage) { + this.totalDamageReceived.accumulateItem(guildID, damage); + } + + getTower(id) { + const tower = this.towers.find(t => t.id === id); + return tower; + } + + addTower(tower) { + this.towers.push(tower); + } + + updateOwnership() { + const towersAlive = this.towers.filter(t => t.hp.current); + if (towersAlive.length) return null; + + let maximumDamage = 0; + let newGuildID = 0; + + this.totalDamageReceived.clearEach((guildID, damage) => { + if (damage > maximumDamage) { + maximumDamage = damage; + newGuildID = guildID; + }; + }); + + this.towers.forEach(tower => tower.setEvent(TIMING_EVENT_RESPAWN, 10000)); + + const topicOld = this.guild.topic; + + const guildOld = guilds[this.guild.id]; + const guildNew = newGuildID ? guilds[newGuildID] : { tag: new ArrayBuffer(0), id: 0 }; + + const topicNew = guildNew.topic; + + this.guild = guildNew; + + return { + guildOld, + guildNew, + topicOld, + topicNew + }; + } +}; + +module.exports = GuildIsland; diff --git a/Game/Harpooner.js b/Game/Harpooner.js new file mode 100644 index 0000000..bc4d4d6 --- /dev/null +++ b/Game/Harpooner.js @@ -0,0 +1,63 @@ +const EventEmitter = require("events"); + +const constants = require("../Data/constants"); +const { CATEGORIES } = constants; +const { CATEGORY_HARPOONER } = CATEGORIES; + +const eventsMapped = { + 1: 197, + 2: 198, + 3: 199, + 4: 200, + 5: 201, + 6: 202 +}; + +class Harpooner extends EventEmitter { + constructor(info) { + super(); + + this.id = info.id; + this.categoryID = CATEGORY_HARPOONER; + this.eventID = eventsMapped[this.id]; + this.damage = info.damage; + this.range = info.range; + this.reload = info.reload; + this.amount = info.amount; + this.amountEquipped = info.amountEquipped || 0; + this.resourceSharedLimiter = info.resourceSharedLimiter; + } + + equip(quantity) { + if (this.amountEquipped + quantity > this.amount) return + + const isTypeQunatityExhausted = this.resourceSharedLimiter.count(quantity); + if (!isTypeQunatityExhausted) return + + this.amountEquipped += quantity; + + return true; + } + + unequip(quantity) { + if (this.amountEquipped - quantity < 0) return + + const isTypeQunatityExhausted = this.resourceSharedLimiter.count(-quantity); + if (!isTypeQunatityExhausted) return + + this.amountEquipped -= quantity; + + return true; + } + + getProperties() { + return { + typeID: 2, + id: this.id, + amount: this.amount, + amountEquipped: this.amountEquipped + }; + } +}; + +module.exports = Harpooner; \ No newline at end of file diff --git a/Game/Inventory.js b/Game/Inventory.js new file mode 100644 index 0000000..64656eb --- /dev/null +++ b/Game/Inventory.js @@ -0,0 +1,303 @@ +const Item = require("./Item"); +const Cannon = require("./Cannon"); +const Harpooner = require("./Harpooner"); +const InventoryList = require("../Game/InventoryList"); + +const schemeCannons = require("../Data/cannons"); +const schemeHarpooners = require("../Data/harpooners"); +const ammunitions = require("../Data/ammunitions"); +const harpoons = require("../Data/harpoons"); +const itemTable = require("../Data/items"); +const constants = require("../Data/constants"); + +const { CATEGORIES, AFFECTS } = constants; +const { CATEGORY_ITEM, CATEGORY_AMMUNITION, CATEGORY_HARPOON, CATEGORY_DESIGN, CATEGORY_ECONOMY, CATEGORY_STATUS, CATEGORY_CANNON, CATEGORY_HARPOONER } = CATEGORIES; +const { AFFECT_CANNON_DAMAGE, AFFECT_HARPOONER_DAMAGE, AFFECT_PROTECTION, AFFECT_SAILING_SPEED, AFFECT_HITPOINT } = AFFECTS; + +const AFFECT_TABLE = { + 1: [1, 2, 3, 4], + [AFFECT_PROTECTION]: [5, 6, 7, 8] +}; + +class Inventory { + constructor(info) { + this.selectedAmmunition = 0; + this.selectedHarpoon = 0; + + this.resourceSharedLimiter = info.resourceSharedLimiter; + + this[CATEGORY_AMMUNITION] = new InventoryList(); + this[CATEGORY_HARPOON] = new InventoryList(); + this[CATEGORY_ITEM] = new InventoryList(); + this[CATEGORY_STATUS] = new InventoryList(); + this[CATEGORY_ECONOMY] = new InventoryList(); + this[CATEGORY_DESIGN] = new InventoryList(); + this[CATEGORY_CANNON] = new InventoryList(); + this[CATEGORY_HARPOONER] = new InventoryList(); + + this[CATEGORY_DESIGN].addItem(1, { + categoryID: CATEGORY_DESIGN, + id: 1, + amount: 1 + }); // default design + } + + refreshSingle(element) { + switch (element.categoryID) { + case CATEGORY_ECONOMY: + + this[CATEGORY_ECONOMY].accumulateItem(element.id, element); + break; + case CATEGORY_ITEM: + const inventoryItem = this[CATEGORY_ITEM].getElement(element.id); + if (!inventoryItem) return this.addItem(element); + + inventoryItem.amount += element.amount; + break; + case CATEGORY_AMMUNITION: + if (!element.amount) return; + + this[CATEGORY_AMMUNITION].accumulateItem(element.id, element); + + if (!this.selectedAmmunition) this.setSelectedAmmunition(element.id); + break; + case CATEGORY_HARPOON: + if (!element.amount) return; + + this[CATEGORY_HARPOON].accumulateItem(element.id, element); + + if (!this.selectedHarpoon) this.setSelectedHarpoon(element.id); + break; + case CATEGORY_DESIGN: + this[CATEGORY_DESIGN].accumulateItem(element.id, element); + break; + case CATEGORY_STATUS: + this[CATEGORY_STATUS].accumulateItem(element.id, element); + break; + case CATEGORY_CANNON: + let cannon = this[CATEGORY_CANNON].getElement(element.id); + if (!cannon) { + const schemeCannon = schemeCannons[element.id]; + if (!schemeCannon) return; + + cannon = new Cannon({ + id: element.id, + damage: schemeCannon.damage, + range: schemeCannon.range, + reload: schemeCannon.reload, + scatter: schemeCannon.scatter, + amount: element.amount, + amountEquipped: element.amountEquipped, + resourceSharedLimiter: this.resourceSharedLimiter.cannon + }); + + this[CATEGORY_CANNON].accumulateItem(cannon.id, cannon); + } else this[CATEGORY_CANNON].accumulateItem(cannon.id, element); + break; + case CATEGORY_HARPOONER: + let harpooner = this[CATEGORY_HARPOONER].getElement(element.id); + if (!harpooner) { + const schemeHarpooner = schemeHarpooners[element.id]; + if (!schemeHarpooner) return; + + harpooner = new Harpooner({ + id: element.id, + damage: schemeHarpooner.damage, + range: schemeHarpooner.range, + reload: schemeHarpooner.reload, + scatter: schemeHarpooner.scatter, + amount: element.amount, + amountEquipped: element.amountEquipped, + resourceSharedLimiter: this.resourceSharedLimiter.harpooner + }); + + this[CATEGORY_HARPOONER].accumulateItem(harpooner.id, harpooner); + } else this[CATEGORY_HARPOONER].accumulateItem(element.id, element); + + break; + }; + } + + use(categoryID, id, amount) { + const category = this[categoryID]; + if (!category) return + + const item = category[id]; + if (!item || !item.amount) return + + const itemScheme = itemTable[id]; + if (!itemScheme && categoryID === CATEGORY_ITEM) return + + let removedCount = -1; + + if (itemScheme.isDurable) item.isActive = true; + else { + if (categoryID === CATEGORY_ITEM) item.nextUseAt = performance.now() + itemScheme.cooldown; + removedCount = itemScheme.useOnImpact || categoryID !== CATEGORY_ITEM ? Math.min(amount, item.amount) : 0; + + item.amount -= removedCount; + item.usedAmount = removedCount; + }; + + return item; + } + + disable(id) { + const item = this[CATEGORY_ITEM].getElement(id); + if (!item) return; + + item.isActive = false; + } + + alterAmount(categoryID, id, amount) { + const category = this[categoryID]; + if (!category) return + + const element = category.getElement(id); + if (!element || !element.amount) return + + const usedAmount = Math.min(amount, element.amount); + element.amount -= usedAmount; + + return usedAmount; + } + + setSelectedAmmunition(id) { + if (!id) return this.selectedAmmunition = 0; + + if (!ammunitions[id]) return; + + const ammunition = this[CATEGORY_AMMUNITION].hasElement(id); + if (!ammunition) return this.selectedAmmunition = 0; + + console.info(ammunition) + + return this.selectedAmmunition = id; + } + + getSelectedAmmunition() { + const selectedAmmunition = this[CATEGORY_AMMUNITION].getElement(this.selectedAmmunition); + if (!selectedAmmunition) return; + + return selectedAmmunition; + } + + setSelectedHarpoon(id) { + if (!id) return this.selectedHarpoon = 0; + + if (!harpoons[id]) return; + + const harpoon = this[CATEGORY_HARPOON].hasElement(id); + if (!harpoon) return this.selectedHarpoon = 0; + + return this.selectedHarpoon = id; + } + + getSeletedHarpoon() { + const selectedHarpoon = this[CATEGORY_HARPOON].getElement(this.selectedHarpoon); + if (!selectedHarpoon) return; + + return selectedHarpoon; + } + + addItem(entry) { + const itemScheme = itemTable[entry.id]; + if (!itemScheme) return; + if (!entry.amount) return; + + const { time, scale, affects } = itemScheme; + + const item = new Item({ + id: entry.id, + amount: entry.amount, + time, + scale, + affects + }); + + this[CATEGORY_ITEM].addItem(item.id, item); + } + + getItemsByAffect(affect) { + const items = Object.values(this[CATEGORY_ITEM]); + return items.filter(item => item.affects.includes(affect)); + } + + useAvailableItemsByAffect(affect, cb) { + AFFECT_TABLE[affect] + .forEach(itemID => { + const item = this[CATEGORY_ITEM].getElement(itemID); + if (item && item.isActive && item.amount) { + const use = item.use(); + cb(use.scale, itemID, use.statusID, use.isStatusActive); + }; + }); + } + + getPosessions() { + return { + [CATEGORY_AMMUNITION]: this[CATEGORY_AMMUNITION], + [CATEGORY_HARPOON]: this[CATEGORY_HARPOON], + [CATEGORY_DESIGN]: this[CATEGORY_DESIGN] + }; + } + + getDesignIDs() { + const designIDs = []; + this[CATEGORY_DESIGN].each(key => designIDs.push(key)); + + return designIDs; + } + + getEconomy() { + const economy = []; + this[CATEGORY_ECONOMY].each((key, value) => economy.push(value)); + + return economy; + } + + getAmmunitions() { + const ammunitions = []; + this[CATEGORY_AMMUNITION].each((key, value) => ammunitions.push(value)); + + return ammunitions; + } + + getHarpoons() { + const harpoons = []; + this[CATEGORY_HARPOON].each((key, value) => harpoons.push(value)); + + return harpoons; + } + + getItems() { + const items = []; + this[CATEGORY_ITEM].each((key, value) => items.push(value)); + + return items; + } + + getCannons() { + const cannons = []; + this[CATEGORY_CANNON].each((key, value) => cannons.push(value)); + + return cannons; + } + + getHarpooners() { + const harpooners = []; + this[CATEGORY_HARPOONER].each((key, value) => harpooners.push(value)); + + return harpooners; + } + + hasDesign(id) { + return this[CATEGORY_DESIGN].hasElement(id); + } + + getItem(id) { + return this[CATEGORY_ITEM].getElement(id); + } +}; + +module.exports = Inventory; \ No newline at end of file diff --git a/Game/InventoryList.js b/Game/InventoryList.js new file mode 100644 index 0000000..173bf40 --- /dev/null +++ b/Game/InventoryList.js @@ -0,0 +1,41 @@ +const LinkedList = require("../Utility/LinkedList"); + +class LinkedListNode { + constructor(key, value) { + this.key = key; + this.value = value; + this.next = null; + }; +}; + +class InventoryList extends LinkedList { + constructor() { + super(); + }; + + accumulateItem(key, value) { + const entry = this.map.get(key); + const updatedValue = entry ? entry.value.amount + value.amount : value; + + if (entry) entry.value.amount = updatedValue; + else this.addItem(key, updatedValue); /// eeeh + } + + addItem(key, value) { + if (!key || !value || !value.categoryID || !value.id || isNaN(value.amount)) return console.info("addItem", key, value); + + const temp = new LinkedListNode(key, value); + + if (!this.head) { + this.head = temp; + this.tail = temp; + } else { + this.tail.next = temp; + this.tail = temp; + }; + + this.map.set(key, temp); + } +}; + +module.exports = InventoryList; \ No newline at end of file diff --git a/Game/Item.js b/Game/Item.js new file mode 100644 index 0000000..c6a9740 --- /dev/null +++ b/Game/Item.js @@ -0,0 +1,61 @@ +const itemTable = require("../Data/items"); +const constants = require("../Data/constants"); + +const { CATEGORIES } = constants; +const { CATEGORY_ITEM } = CATEGORIES; + +class Item { + constructor(config) { + this.categoryID = CATEGORY_ITEM; + this.id = config.id; + this.amount = config.amount; + this.affects = config.affects; + this.scale = config.scale; + this.isDurable = config.isDurable; + this.time = config.time; + this.isActive = false; + } + + use(amount) { + const itemScheme = itemTable[this.id]; + if (!itemScheme) return; + + if (!this.amount) return global.logger.send({ event: "itemUsage", reason: "item amount is 0" }); + + if (!amount) amount = 1; + + const removedAmount = Math.min(amount, this.amount); + this.amount -= removedAmount; + + if (!this.amount && itemScheme.isDurable) this.isActive = false; + + return { + scale: this.scale, + statusID: itemScheme.statusID, + isStatusActive: this.amount && itemScheme.isDurable + }; + } + + activate(amount) { + const itemScheme = itemTable[this.id]; + if (!itemScheme) return; + if (!this.amount) return; + + if (!amount) amount = 1; + + const removedAmount = itemScheme.useOnImpact ? Math.min(amount, this.amount) : 0; + + if (itemScheme.isDurable) this.isActive = true; + else { + this.nextUseAt = performance.now() + itemScheme.cooldown; + this.amount -= removedAmount; + }; + + return { + ...this, + removedAmount + }; + } +}; + +module.exports = Item; \ No newline at end of file diff --git a/Game/Map.js b/Game/Map.js new file mode 100644 index 0000000..d597ba0 --- /dev/null +++ b/Game/Map.js @@ -0,0 +1,511 @@ +const EventEmitter = require("events"); + +const Utility = require("./Utility"); +const Reward = require("./Reward"); +const EntityGroup = require("./EntityGroup"); + +const mapMatrix = require("../Data/mapMatrix"); +const constants = require("../Data/constants"); + +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const grids = require("./Grids"); + +const { TYPES, EVENTS } = constants; +const { ENTITY_TYPE_PLAYER } = TYPES; +const { EVENT_SPAWN_ADMIRAL } = EVENTS; + +const COORDINATE_REGIONS = ["A", "B", "C"]; +const COORDINATE_SECTIONS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; + +function getType(compositeID) { + return (compositeID >> 24) & 0xFF; +}; + +class GameMap extends EventEmitter { + constructor(config) { + super(); + + this.w = config.size.width; + this.h = config.size.height; + this.cw = config.size.chunk.width; + this.ch = config.size.chunk.height; + this.chunkRows = config.size.height / config.size.chunk.height; + this.chunkColumns = config.size.width / config.size.chunk.width; + this.name = config.name; + this.type = config.type; + this.id = config.id; + this.neighbours = config.neighbours; + this.collision = config.collision ?? {}; + this.level = config.level; + this.spawnedEntitiesTotalCount = 0; + this.availableIdentifiers = []; + this.players = new Map(); + this.npcs = new Map(); + this.monsters = new Map(); + this.towers = new Map(); + this.collectables = new Map(); + this.entities = [this.players, this.npcs, this.monsters, this.towers, this.collectables]; + this.entityGroups = Array.isArray(config.entityGroups) ? config.entityGroups.map(e => new EntityGroup(e)) : []; + this.chunksData = []; + this.networkPacketsGlobal = []; + this.networkPacketsLocal = []; + this.networkPacketsInvidiual = {}; + this.testPackets = []; + this.eventBuffer = {}; + this.eventAuthors = []; + + this.lookingForTarget = []; + + this.dirtyChunks = []; + + this.grid = grids[this.id - 1]; + + for (let i = 0; i < this.chunkColumns; i++) { + for (let j = 0; j < this.chunkRows; j++) { + const m = new DoubleLinkedList(); + + this[`chunkTest_${i}-${j}`] = m; + this[`chunk_${i}-${j}`] = new DoubleLinkedList(); + + this.dirtyChunks.push(m); + + this.chunksData.push({ + x: i * config.size.chunk.width, + y: j * config.size.chunk.height, + name: `chunk_${i}-${j}`, + nameTest: `chunkTest_${i}-${j}` + }); + }; + }; + } + + registerIndividualNetworkPacket(identifier, packets, metadata = []) { + if (!(packets instanceof Array)) packets = [packets]; + if (!(metadata instanceof Array)) metadata = [metadata]; + + if (!this.networkPacketsInvidiual[identifier]) this.networkPacketsInvidiual[identifier] = []; + + packets.forEach(packet => this.networkPacketsInvidiual[identifier].push({ + index: this.networkPacketsLocal.length, + packet, + metadata + })); + } + + registerGlobalNetworkPacket(identifier, packets) { + if (!(packets instanceof Array)) packets = [packets]; + const entity = this.getEntityFromSubgroup(identifier); + + packets.forEach(p => this.networkPacketsGlobal.push({ + position: entity.position, + authorID: entity.id, + packet: p + })); + } + + registerNetworkPacket(identifier, packets, headers = []) { + if (!(packets instanceof Array)) packets = [packets]; + + const entity = this.getEntityFromSubgroup(identifier); + + if (headers[0] === 59) packets.forEach((p, i) => this.testPackets.push({ position: entity.position, authorID: entity.id, packet: p, header: headers[i] ? headers[i] : 59 })); + else packets.forEach((p, i) => this.networkPacketsLocal.push({ position: entity.position, authorID: entity.id, packet: p, header: headers[i] ? headers[i] : 0 })); + } + + getNeighbourMapsInRadius(radius) { + const maps = []; + + for (let i = 0; i < mapMatrix.length; i++) { + const mapRow = mapMatrix[i]; + + for (let j = 0; j < mapRow.length; j++) { + if (this.id === mapRow[j]) { + const startFromColumn = Math.max(0, j - radius); + const startFromRow = Math.max(0, i - radius); + const rawColumnBoundary = (j + radius) + 1; + const rawRowBoundary = (i + radius) + 1; + + const columnBoundary = rawColumnBoundary > mapRow.length - 1 ? mapRow.length : rawColumnBoundary; + const rowBoundary = rawRowBoundary > mapMatrix.length - 1 ? mapMatrix.length : rawRowBoundary; + + for (let l = startFromRow; l < rowBoundary; l++) + for (let k = startFromColumn; k < columnBoundary; k++) + maps.push(mapMatrix[l][k]); + break; + }; + }; + }; + + return maps; + } + + spawnEntityGroups(cb, NPC, attackCallback, dieCallback) { + for (let i = 0, length = this.entityGroups.length; i < length; i++) { + const group = this.entityGroups[i]; + if (!group.isDefeated()) continue; + + group.default(); + + const randomPosition = this.grid.randomNode(20); + group.setAreaPosition(randomPosition); + + for (let i = 0, length = group.structure.length; i < length; i++) { + const { typeID, entityTypeID, groupMemberTypeID } = group.structure[i]; + + const entityTypeSetting = group.getEntitySetting(groupMemberTypeID); + + const preSelectedDestinationNode = this.grid.randomNodeInArea(group.predefinedArea); + const spawnData = { + id: this.spawnedEntitiesTotalCount, + speed: entityTypeSetting.speed, + respawnTime: entityTypeSetting.respawnTime, + size: entityTypeSetting.size, + entityTypeID, + parentEntityTypeID: 0, + maximumHitpoint: 4500, + currentHitpoint: 4500, + map: this, + group, + position: preSelectedDestinationNode, + attackCallback, + dieCallback + }; + + const spawn = this.registerNonPlayerCharacter(spawnData, NPC); + + if (groupMemberTypeID === 1) group.addLeader(spawn.npc); + else group.addMember(spawn.npc); + + cb(this, spawn); + }; + }; + } + + setupNonPlayerCharacterSpawn(spawn, cb, globalDieCallback, globalRewardCallback, globalAttackCallback, NPC) { + spawn.forEach(data => { + const { id, parentEntityTypeId, speed, minHealth, maxHealth, amount, respawnTime, rewardEntries } = data; + if (!parentEntityTypeId) return; + + const admiralNPC = spawn.find(parent => parent.id === parentEntityTypeId); + + const reward = new Reward({ + distributionType: 1, + baseReward: rewardEntries.reduce((result, entry) => { + if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance, + lastShot: entry.isLastShot + }; + else result[entry.categoryId] = { + [entry.itemId]: { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance, + lastShot: entry.isLastShot + } + }; + + return result; + }, {}) + }); + + for (let i = 0; i < amount; i++) { + const spareID = this.availableIdentifiers.pop(); + const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); + + const npcSpawnData = { + id: spareID ? spareID : this.spawnedEntitiesTotalCount, + speed, + respawnTime, + reward, + entityTypeID: id, + parentEntityTypeID: parentEntityTypeId, + maximumHitpoint: hitpoint, + currentHitpoint: hitpoint, + map: this, + behaviour: { + stopIfAttacked: true, + aggressive: false + }, + attackCallback: globalAttackCallback, + rewardCallback: globalRewardCallback + }; + + const newSpawn = this.registerNonPlayerCharacter(npcSpawnData, NPC); + newSpawn.npc.dieCallback = data => { + const isAdmiralReady = globalDieCallback.call(newSpawn.npc, data); + if (isAdmiralReady && admiralNPC) { + const { id, speed, minHealth, maxHealth, rewardEntries } = admiralNPC; + + const reward = new Reward({ + distributionType: 1, + baseReward: rewardEntries.reduce((result, entry) => { + if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance, + lastShot: entry.isLastShot + }; + else result[entry.categoryId] = { + [entry.itemId]: { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance, + lastShot: entry.isLastShot + } + }; + + return result; + }, {}) + }); + + const spareID = this.availableIdentifiers.pop(); + const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); + + const admiralSpawnData = { + id: spareID ? spareID : this.spawnedEntitiesTotalCount, + isAdmiral: true, + speed, + reward, + size: 192, + entityTypeID: id, + position: newSpawn.npc.position, + maximumHitpoint: hitpoint, + currentHitpoint: hitpoint, + map: this, + behaviour: { + stopIfAttacked: true, + aggressive: false + }, + dieCallback: globalDieCallback, + attackCallback: globalAttackCallback, + rewardCallback: globalRewardCallback + }; + + const emittingTo = this.getNeighbourMapsInRadius(1); + const notificationData = { + mapID: this.id, + typeID: newSpawn.npc.typeID, + entityTypeID: newSpawn.npc.entityTypeID + }; + + this.emit(EVENT_SPAWN_ADMIRAL, notificationData, emittingTo); + + const admiral = this.registerNonPlayerCharacter(admiralSpawnData, NPC); + cb(this, admiral); + }; + }; + + cb(this, newSpawn); + }; + }); + } + + registerNonPlayerCharacter(data, NPC) { + const npc = new NPC(data); + + if (!this.availableIdentifiers.length) this.spawnedEntitiesTotalCount++; + + const npcSpawnProperties = npc.getProperties(); + + return { npc, npcSpawnProperties }; + } + + setupMonsterSpawn(spawn, cb, globalDieCallback, globalRewardCallback, globalAttackCallback, Monster) { + spawn.forEach(data => { + const { id, parentEntityTypeId, minHealth, maxHealth, amount, respawnTime, rewardEntries } = data; + if (!parentEntityTypeId) return; + + const bossMonster = spawn.find(b => b.id === parentEntityTypeId); + + const reward = new Reward({ + distributionType: 1, + baseReward: rewardEntries.reduce((result, entry) => { + if (result[entry.categoryId]) result[entry.categoryId][entry.itemId] = { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance + }; + else result[entry.categoryId] = { + [entry.itemId]: { + amountMinimum: entry.min, + amountMaximum: entry.max, + chance: entry.chance + } + }; + + return result; + }, {}) + }); + + for (let i = 0; i < amount; i++) { + const spareID = this.availableIdentifiers.pop(); + const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); + + const monsterData = { + id: spareID ? spareID : this.spawnedEntitiesTotalCount, + entityTypeID: id, + reward, + respawnTime, + parentEntityTypeID: parentEntityTypeId, + maximumHitpoint: hitpoint, + currentHitpoint: hitpoint, + map: this, + rewardCallback: globalRewardCallback, + attackCallback: globalAttackCallback + }; + + const newSpawn = this.registerMonster(monsterData, Monster); + newSpawn.monster.dieCallback = data => { + const isAdmiralReady = globalDieCallback.call(newSpawn.monster, data); + + if (isAdmiralReady && bossMonster) { + const { id, minHealth, maxHealth, reward } = bossMonster; + const spareID = this.availableIdentifiers.pop(); + const hitpoint = Math.floor(Math.random() * (maxHealth - minHealth + 1) + minHealth); + + const emittingTo = this.getNeighbourMapsInRadius(1); + const notificationData = { mapID: this.id, typeID: newSpawn.monster.typeID, entityTypeID: newSpawn.monster.entityTypeID }; + this.emit(EVENT_SPAWN_ADMIRAL, notificationData, emittingTo); + + const bossSpawnData = { + id: spareID ? spareID : this.spawnedEntitiesTotalCount, + isAdmiral: true, + reward, + entityTypeID: id, + position: newSpawn.monster.position, + maximumHitpoint: hitpoint, + currentHitpoint: hitpoint, + map: this, + dieCallback: globalDieCallback, + attackCallback: globalAttackCallback, + rewardCallback: globalRewardCallback + }; + + const boss = this.registerMonster(bossSpawnData, Monster); + cb(boss); + }; + }; + + cb(newSpawn); + }; + }); + } + + registerMonster(data, Monster) { + const monster = new Monster(data); + + if (!this.availableIdentifiers.length) this.spawnedEntitiesTotalCount++; + + const monsterSpawnProperties = monster.getProperties(); + + return { monster, monsterSpawnProperties }; + } + + registerCollectable(spawn, cb, globalDieCallback, globalRewardCallback, Collectable) { + spawn.forEach(data => { + const { id, amount, respawnTime, reward } = data; + const newCollectables = []; + + for (let i = 0; i < amount; i++) { + const collectable = new Collectable({ + id: this.spawnedEntitiesTotalCount++, + entityTypeID: id, + respawnTime, + map: this, + rewardEntries: reward, + dieCallback: globalDieCallback, + rewardCallback: globalRewardCallback + }); + + const collectableSpawnProperties = collectable.getProperties(); + newCollectables.push({ collectable, collectableSpawnProperties }); + }; + + cb(newCollectables); + }); + } + + removeNPC(id) { + this.npcs.delete(id); + this.availableIdentifiers.push(id); + } + + removeMonster(id) { + this.monsters.delete(id); + this.availableIdentifiers.push(id); + } + + getNearbyPlayersGameID(objectToCheck) { + const nearbyPlayers = []; + + this.players.forEach(player => { + if (!player.hasUnexpectedlyLeft && + player.isAlive && + Math.pow(objectToCheck.position.x - player.position.x, 2) + Math.pow(objectToCheck.position.y - player.position.y, 2) < Math.pow(player.sight, 2)) + nearbyPlayers.push(player.id); + }); + + return nearbyPlayers; + } + + getLivingEntities(types) { + if (!types.length) return; + + const aliveEntities = []; + + for (let i = 0; i < types.length; i++) { + const type = types[i]; + const entityGroup = this.entities[type - 1]; + entityGroup.forEach(entity => entity.isAlive ? aliveEntities.push(entity) : null); + }; + + return aliveEntities; + } + + getAliveEntityFromSubgroup(type, id) { + const subgroup = this.entities[type - 1]; + if (!subgroup) return null; + + const entity = subgroup.get(+id); + return entity && entity.isAlive ? entity : null; + } + + getEntityFromSubgroup(id) { + const type = getType(id); + + return this.entities[type] ? this.entities[type].get(id) : null; + } + + removePlayer = id => this.players.delete(id); + + addPlayer = player => this.players.set(player.id, player); + + switchPlayer(player, map) { + this.removePlayer(player.id); + + map.players.set(player.id, player); + player.map = map; + } + + getGameCoordinates(spiedEntity) { + const vertical = Math.round(spiedEntity.position.y / 100) - 1; + const verticalRest = vertical % COORDINATE_SECTIONS.length; + const index = Math.floor(Math.max(vertical, 0) / COORDINATE_SECTIONS.length); + + const letters = `${COORDINATE_REGIONS[index]}${COORDINATE_SECTIONS[verticalRest]}`; + const numbers = Math.floor(spiedEntity.position.x / 100) < 10 ? `0${Math.floor(spiedEntity.position.x / 100)}` : Math.floor(spiedEntity.position.x / 100); + + return { + name: spiedEntity.name, + id: spiedEntity.id, + map: spiedEntity.map.name, + x: numbers, + y: letters + }; + } +}; + +module.exports = GameMap; \ No newline at end of file diff --git a/Game/Maps.js b/Game/Maps.js new file mode 100644 index 0000000..6b63a0a --- /dev/null +++ b/Game/Maps.js @@ -0,0 +1,7 @@ +const maps = []; +for (let i = 0; i < 19; i++) { + const map = require(`../Maps/Map_${i + 1}`); + maps.push(map); +}; + +module.exports = maps; \ No newline at end of file diff --git a/Game/Monster.js b/Game/Monster.js new file mode 100644 index 0000000..ca9f125 --- /dev/null +++ b/Game/Monster.js @@ -0,0 +1,147 @@ +const Entity = require("./Entity"); + +const constants = require("../Data/constants"); +const { TYPES, EVENTS, TIMING_EVENTS, MAXIMUM } = constants; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_MONSTER } = TYPES; +const { TIMING_EVENT_RESPAWN } = TIMING_EVENTS; +const { EVENT_DIE } = EVENTS; +const { MAX_SAFE_INTEGER } = MAXIMUM; + +class Monster extends Entity { + constructor(info) { + super({ + type: ENTITY_TYPE_MONSTER, + id: info.id, + map: info.map, + isAlive: true, + position: info.position + }); + + this.hp = { + maximum: info.maximumHitpoint, + current: info.currentHitpoint + }; + + this.entityTypeID = info.entityTypeID; + this.parentEntityTypeID = info.parentEntityTypeID ?? 0; + this.respawnTime = info.respawnTime * 1000; + this.isAdmiral = info.isAdmiral ?? false; + this.attackers = []; + this.totalDamageReceived = 0; + this.damageReceived = {}; + this.dieCallback = info.dieCallback; + this.attackCallback = info.attackCallback; + this.rewardCallback = info.rewardCallback; + this.reward = info.reward; + + const { map, id } = this; + + map.monsters.set(id, this); + + const nearbyPlayersID = map.getNearbyPlayersGameID(this); + for (let i = 0; i < nearbyPlayersID.length; i++) { + const player = map.getEntityFromSubgroup(nearbyPlayersID[i]); + player.inDistance[id] = 0x01; + }; + } + + increaseCurrentHitpoint(amount) { + this.hp.current = Math.min(this.hp.maximum, this.hp.current + amount); + return this.hp.current; + } + + decreaseCurrentHitpoint(amount, author) { + const { authorID } = author; + + amount = Math.abs(amount); + + const damage = Math.min(this.hp.current, amount); + + if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage; + else this.damageReceived[authorID] = damage; + + this.totalDamageReceived += damage; + this.hp.current -= damage; + + this.attackCallback({ + typeID: this.typeID, + entityTypeID: this.entityTypeID, + authorID, + damage + }); + + const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER; + if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER; + + if (this.hp.current === 0 || isOverSafeLimit) { + const die = { + typeID: this.typeID, + lastShotTypeID: 1, + lastShotID: author.authorID, + targetID: this.id, + targetEntityTypeID: this.entityTypeID + }; + + this.die(die); + // this.setSingleEvent(11, 0, die); + }; + + return { + normalizedAmount: damage, + currentHitpoint: this.hp.current + }; + } + + isAtMaximumHitpoints() { + return this.hp.current === this.hp.maximum; + } + + die(die) { + super.die(); + + this.hp.current = 0; + + this.attackers.forEach(data => { + const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry); + attackerEntity.wipeAttack(); + }); + + this.attackers.length = 0; + + this.emit(EVENT_DIE, die, this.id); + + const admiralProperties = { + typeID: this.typeID, + entityTypeID: this.entityTypeID, + parentEntityTypeID: this.parentEntityTypeID, + id: this.id, + isAdmiral: this.isAdmiral, + authorID: die.lastShotID + }; + + this.dieCallback(admiralProperties); + + this.removeEvent(11); + + this.damageReceived = {}; + + if (!this.isAdmiral) this.setEvent(TIMING_EVENT_RESPAWN, this.respawnTime); + } + + respawn() { + if (this.isAlive) return; + + this.hp.current = this.hp.maximum; + + super.respawn(); + + this.removeEvent(TIMING_EVENT_RESPAWN); + } + + getProperties() { + const { position, id, hp, type, typeID, isAdmiral, entityTypeID } = this; + return { position, id, hp, type, typeID, isAdmiral, entityTypeID }; + } +}; + +module.exports = Monster; \ No newline at end of file diff --git a/Game/NPC.js b/Game/NPC.js new file mode 100644 index 0000000..24c3d22 --- /dev/null +++ b/Game/NPC.js @@ -0,0 +1,763 @@ +const Entity = require("./Entity"); +const Utility = require("./Utility"); +const Cannon = require("./Cannon"); + +const ammunitions = require("../Data/ammunitions"); +const cannonsToEquip = require("../Data/cannonsSchemeNPC"); +const cannons = require("../Data/cannons"); +const OVERTIME_HEALTH_EFFECTS = require("../Data/overtime"); + +const constants = require("../Data/constants"); + +const { EVENTS, TIMING_EVENTS, TYPES, EFFECTS_TERMINATE_MOVEMENT, EVENTS_BY_TIMING_EVENTS_ID, AFFECTS, MAXIMUM } = constants; +const { EVENT_DESTINATION_REACHED, EVENT_DESTINATION_NEW, EVENT_SPEED_UPDATE, EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_ATTACKING, EVENT_EFFECT_ADD, EVENT_ATTACK_BAR, EVENT_HEALTH_CHANGED } = EVENTS; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC } = TYPES; +const { TIMING_EVENT_MOVE, TIMING_EVENT_RESPAWN } = TIMING_EVENTS; +const { AFFECT_CANNON_DAMAGE } = AFFECTS; +const { MAX_SAFE_INTEGER } = MAXIMUM; + +const EVENTS_REMOVE_DEATH_NPC = [9, 11, 17]; + +class NPC extends Entity { + constructor(info) { + super({ + type: ENTITY_TYPE_NPC, + id: info.id, + map: info.map, + isAlive: true, + position: info.position + }); + + this.hp = { + maximum: info.maximumHitpoint, + current: info.currentHitpoint + }; + + this.entityTypeID = info.entityTypeID ?? 0; + this.parentEntityTypeID = info.parentEntityTypeID ?? 0; + this.respawnTime = info.respawnTime * 1000; + this.isAdmiral = info.isAdmiral ?? false; + this.speed = info.speed; + this.size = info.size ?? 128; + this.cannonRange = 0; + this.i = 0; + this.path = []; + this.attackers = []; + this.isSpeedLowered = false; + this.damageReceived = {}; + // this.behaviour = info.behaviour; + this.totalDamageReceived = 0; + this.lastShotTickTimestamp = 0; + this.activeOvertimeHealthEffects = {}; + this.rewardCallback = info.rewardCallback; + this.attackCallback = info.attackCallback; + this.dieCallback = info.dieCallback; + this.reward = info.reward; + this.activeEffects = {}; + this.cannonTypesOutOfRange = []; + this.searchedTarget = null; + this.group = info.group; + this.inventory = { + cannons: [], + selectedAmmunition: 1, + equipCannons() { + const cannonsEquippedScheme = cannonsToEquip[info.entityTypeID]; + if (!cannonsEquippedScheme) return; + + for (let i = 0, length = cannonsEquippedScheme.length; i < length; i++) { + const [id, amount] = cannonsEquippedScheme[i]; + const cannonScheme = cannons[id]; + + if (this.cannonRange < cannonScheme.range) this.cannonRange = cannonScheme.range; + + const cannon = new Cannon({ + id, + damage: cannonScheme.damage, + range: cannonScheme.range, + reload: cannonScheme.reload, + scatter: 0, + amount, + amountEquipped: amount + }); + + this.cannons.push(cannon); + }; + }, + getCannons() { + return this.cannons; + } + }; + + this.inventory.equipCannons(); + + const cannonsStatistics = this.getCannonsStatistics(); + this.cannonRange = cannonsStatistics.maximumCannonRange; + this.cannonTypesInUse = cannonsStatistics.distinctCannonTypes; + + this.behaviour = { + stopIfAttacked: !!this.parentEntityTypeID, + attackIfAttacked: true, + isAggressive: !this.parentEntityTypeID, + isChaser: false, + stopAttackIfAbandoned: true, + selectTargetType: 1 + }; + + this.behaviourBits = Object.values(this.behaviour).reduce((prev, cur, i) => cur ? prev + Math.pow(2, i) : prev + 0, 0); + + if (this.behaviour.isChaser) { + this.chasingBehaviour = { + range: 750, + recalculateAccumulator: 0, + position: { + x: 0, + y: 0 + }, + target: null + }; + }; + + const { map, id } = this; + + map.npcs.set(id, this); + + const nearbyPlayersID = map.getNearbyPlayersGameID(this); + for (let i = 0; i < nearbyPlayersID.length; i++) { + const player = map.getEntityFromSubgroup(nearbyPlayersID[i]); + player.inDistance[id] = true; + }; + } + + trackOvertimeHealthEffect(id) { + const effect = this.activeOvertimeHealthEffects[id]; + if (effect) effect.count++; + else this.activeOvertimeHealthEffects[id] = { count: 0 }; + + return effect ? effect.count : 0; + } + + overtimeDecreaseCurrentHitpoint(event) { + if (!event) return; + + const { id, authorTypeID, authorID, damage, iid } = event; + const { amount, ticks, time } = OVERTIME_HEALTH_EFFECTS[id]; + + const decrease = this.decreaseCurrentHitpoint(damage * amount, { + authorTypeID, + authorID + }); + + if (!decrease) return; + + const currentCount = this.trackOvertimeHealthEffect(id); + if (currentCount < ticks && decrease.currentHitpoint) this.setIndependentEvent(iid, time, event); + else delete this.activeOvertimeHealthEffects[id]; + + const data = { + targetTypeID: this.typeID, + targetID: this.id, + amount: decrease.normalizedAmount, + type: 0, + isCritical: false + }; + + this.emit(EVENT_HEALTH_CHANGED, data, this.id); + } + + effectAdd(effectID) { + this.activeEffects[effectID] = true; + + this.emit(EVENT_EFFECT_ADD, { + typeID: this.typeID, + id: this.id, + effectID + }); + } + + effectRemove(effectID) { + this.activeEffects[effectID] = false; + + this.emit(EVENT_EFFECT_ADD, { + typeID: this.typeID, + id: this.id, + effectID + }); + + this.removeEvent(17); + + switch (effectID) { + case 1: + this.determineMovementState(); + break; + }; + } + + isAtMaximumHitpoints() { + return this.hp.current === this.hp.maximum; + } + + increaseCurrentHitpoint(amount) { + amount = Math.abs(amount); + + const normalizedAmount = this.hp.current + amount > this.hp.maximum ? amount - (this.hp.current + amount - this.hp.maximum) : amount; + + this.hp.current += normalizedAmount; + + if (this.isSpeedLowered && !this.isHPBelowPercentageThreshold(35)) { + this.changeSpeed(-this.speed / 2); + this.isSpeedLowered = false; + }; + + return { + normalizedAmount, + currentHitpoint: this.hp.current + }; + } + + decreaseCurrentHitpoint(amount, author) { + if (this.hp.current === 0) return; + + const { authorID } = author; + + amount = Math.abs(amount); + + const damage = Math.min(this.hp.current, amount); + + if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage; + else this.damageReceived[authorID] = damage + + this.totalDamageReceived += damage; + this.hp.current -= damage; + + if (!this.isSpeedLowered && this.isHPBelowPercentageThreshold(35)) { + this.changeSpeed(this.speed); + this.isSpeedLowered = true; + }; + + this.attackCallback({ + typeID: this.typeID, + entityTypeID: this.entityTypeID, + authorID, + damage + }); + + const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER; + if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER; + + if (this.hp.current === 0 || isOverSafeLimit) { + const die = { + typeID: this.typeID, + lastShotTypeID: 1, + lastShotID: author.authorID, + targetID: this.id, + targetEntityTypeID: this.entityTypeID + }; + + this.die(die); + //this.setSingleEvent(11, 0, die); + }; + + return { + normalizedAmount: damage, + currentHitpoint: this.hp.current + }; + } + + isHPBelowPercentageThreshold(percentageThreshold) { + return this.hp.maximum / 100 * percentageThreshold > this.hp.current; + } + + changeSpeed(speed) { + this.speed += speed; + + const eSpeed = { + typeID: this.typeID, + id: this.id, + speed: this.speed + }; + + this.emit(EVENT_SPEED_UPDATE, eSpeed, this.id); + } + + die(eventDeath) { + if (!this.isAlive) return; + + this.emit(EVENT_DIE, eventDeath, this.id); + + super.die(); + + this.hp.current = 0; + + this.attackers.forEach(data => { + const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry); + attackerEntity.wipeAttack(); + }); + + this.attackers.length = 0; + + if (this.group) + this.emit("miniMapDotRemoveTest", { + id: this.id + }); + + const admiralProperties = { + typeID: this.typeID, + entityTypeID: this.entityTypeID, + parentEntityTypeID: this.parentEntityTypeID, + id: this.id, + isAdmiral: this.isAdmiral, + authorID: eventDeath.lastShotID + }; + + this.stopMovement(); + this.abortAttack(); + this.untrackChasedTarget(); + + this.dieCallback(admiralProperties); + + this.damageReceived = {}; + this.activeEffects = {}; + + EVENTS_REMOVE_DEATH_NPC.forEach(e => this.removeEvent(e)); + + if (this.group && !this.group.isLeaderAlive()) this.group.cancelRespawn(); + + if ((!this.isAdmiral && !this.group) || (this.group && !this.group.isLeader(this.id) && this.group.isLeaderAlive())) this.setEvent(TIMING_EVENT_RESPAWN, this.respawnTime); + } + + respawn() { + if (this.isAlive) return; + + this.hp.current = this.hp.maximum; + this.isSpeedLowered = false; + this.speed /= 2; + + super.respawn({ + position: this.group ? this.map.grid.randomNodeInArea(this.group.predefinedArea) : null + }); + + this.determineMovementState(); + + this.removeEvent(TIMING_EVENT_RESPAWN); + } + + haltMovement() { + this.isDirty = this.isDirty || this.isMoving && this.behaviour.stopIfAttacked; + } + + stopMovement() { + this.isDirty = false; + this.isMoving = false; + this.path = []; + this.i = 0; + this.removeEvent(TIMING_EVENT_MOVE); + } + + startMovement(path) { + if (!Array.isArray(path)) return; + if (!path.length) return; + + this.path = path; + + this.isMoving = true; + + const [x, y] = this.path[this.i]; + this.emit(EVENT_DESTINATION_NEW, { + id: this.id, + typeID: this.typeID, + mapID: this.map.id, + x, + y + }); + + this.setEvent(TIMING_EVENT_MOVE, this.speed); + } + + getCannonsStatistics() { + const cannons = this.inventory.getCannons(); + + let maximumCannonRange = 0; + + const distinctCannonTypes = cannons.reduce((acc, cur) => { + const alreadyAddedType = acc.find(c => c.id === cur.id); + if (!alreadyAddedType && cur.amountEquipped) { + if (cur.range > maximumCannonRange) maximumCannonRange = cur.range; + + acc.push({ + id: cur.id, + reload: cur.reload, + range: cur.range, + eventID: cur.eventID + }); + }; + + return acc; + }, []); + + return { + distinctCannonTypes, + maximumCannonRange + }; + } + + calculateCannonDamage(id) { + const { inventory } = this; + const ammunitionScheme = ammunitions[inventory.selectedAmmunition]; + let scaleSum = 1; + let damage = 0; + let k = 0; + + /*inventory.useAvailableItemsByAffect(AFFECT_CANNON_DAMAGE, (scale, itemID, statusID, isStatusActive) => { + scaleSum += scale; + + if (!isStatusActive) this.disableStatusEffect(statusID); + });*/ + + const cannons = this.inventory.getCannons(); + for (let i = 0, length = cannons.length; i < length; i++) { + const cannonType = cannons[i]; + + for (let j = 0, length = cannonType.amountEquipped; j < length; j++) { + const minimum = ((cannonType.damage * ((100 + cannonType.scatter) / 100) + 1) * scaleSum) - 1; + const maximum = cannonType.damage * scaleSum; + + if (id === cannonType.id) { + damage += Math.ceil((Math.random() * (maximum - minimum) + minimum) * ammunitionScheme.damage); + k += cannonType.amountEquipped; + }; + }; + }; + + return { + usedCannonCount: k, + value: damage, + critical: false, + usedAmmunition: ammunitionScheme + }; + } + + initializeAttackWithCannon(target) { + if (!target) return; + + if (this.isAttacking()) return false; + + const isInShootingRange = Utility.IsInDistance(this.position, target.position, this.cannonRange); + if (!isInShootingRange) return false; + if (target.id === this.id) return false; + if (!target.isAlive) return false; + if (this.hp.current <= 0) return false; + if (!this.behaviour.attackIfAttacked) return false; + if (!this.inventory.getCannons().some(c => c.amountEquipped)) return false; + + + if (target.typeID === ENTITY_TYPE_PLAYER) target.cancelDisconnect(); + + this.haltMovement(); + + this.target = target; + this.searchedTarget = null; + + const attackerInformation = { + attackerEntry: this.id + }; + + target.attackers.push(attackerInformation); + + const eventAttackBar = { + authorID: this.id, + authorTypeID: this.typeID, + isTarget: false, + source: 1 + }; + + target.emit(EVENT_ATTACK_BAR, eventAttackBar); + + this.cannonTypesInUse.forEach(c => this.setReusableSingleEvent(c.eventID, 0, { id: c.id })); + } + + flush(timingEvent) { + const currentFlush = this.map.eventBuffer[this.id]; + if (!currentFlush) return + + this.emit("attacking", currentFlush.data, currentFlush.author); + + delete this.map.eventBuffer[this.id]; + } + + attackWithCannon(event) { + if (!this.isAlive) return; + + const { target } = this; + + const cannonType = this.cannonTypesInUse.find(c => c.id === event.id); + + const recheckShootingRange = Utility.IsInDistance(this.position, target.position, cannonType.range); + if (!recheckShootingRange) { + this.cannonTypesOutOfRange.push({ + cannonID: cannonType.id, + lastFiredAtTimestamp: performance.now(), + event + }); + + if (this.cannonRange === cannonType.range) { + this.abortAttack(); + } else { + this.setReusableSingleEvent(cannonType.eventID, cannonType.reload, event); + }; + + return true; + }; + + if (this.behaviour.stopAttackIfAbandoned && !this.behaviour.isAggressive && !this.attackers.length) return this.abortAttack(); + + if (target.isRepairing()) target.cancelRepair(); + + const damage = this.calculateCannonDamage(cannonType.id); + + this.lastShotTickTimestamp = performance.now(); + + const healthUpdate = target.decreaseCurrentHitpoint(damage.value, { + authorTypeID: this.typeID, + authorID: this.id + }); + + // this.emit("attacking", attacking, this.id); + + const flushBuffer = this.map.eventBuffer[this.id]; + if (flushBuffer) flushBuffer.data.amount += healthUpdate.normalizedAmount; + else { + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + amount: healthUpdate.normalizedAmount, + isCritical: damage.critical + }; + + this.map.eventBuffer[this.id] = { + data: attacking, + author: target.id, + target: this.id + }; + + this.map.eventAuthors.push(this.id); + + /*this.flushBuffer[EVENT_ATTACKING] = { + data: attacking, + author: target.id + };*/ + }; + + //this.flush(1); + + if (healthUpdate.currentHitpoint <= 0) return; + + this.setReusableSingleEvent(cannonType.eventID, cannonType.reload, event); + + return true; + } + + wipeAttack() { + const { target } = this; + if (!target) return + + this.target = null; + + this.untrackChasedTarget(); + this.stopMovement(); + this.determineMovementState(); + + this.cannonTypesInUse.forEach(c => this.cancelEvent(c.eventID)); + } + + abortAttack() { + const { target } = this; + if (!target) return; + + if (this.behaviour.isChaser) { + this.chasingBehaviour.target = this.target; + this.chasingBehaviour.position = Object.assign({}, this.target.position); + }; + + this.target.attackers = target.attackers.filter(data => data.attackerEntry !== this.id); + this.target = null; + + this.cannonTypesInUse.forEach(c => this.cancelEvent(c.eventID)); + + this.determineMovementState(); + + this.markActive(); + + this.map.lookingForTarget.push(this); + } + + determineMovementState(preSelectedDestinationNode) { + if (!this.isAlive) return; + if (this.isDirty) return; + if (this.isMoving) return; + if (this.behaviour.stopIfAttacked && this.isAttacking()) return; + if (!this.behaviour.stopIfAttacked && this.isMoving) return; + if (this.behaviour.stopIfAttacked && this.attackers.length) return; + if (EFFECTS_TERMINATE_MOVEMENT.some(effect => this.activeEffects[effect])) return; + + if (!this.isDirty && this.chasingBehaviour && this.chasingBehaviour.target + && this.chasingBehaviour.position.x === this.position.x && this.chasingBehaviour.position.y === this.position.y) { + this.chasingBehaviour.position = { + x: this.chasingBehaviour.target.position.x, + y: this.chasingBehaviour.target.position.y + }; + }; + + let destination = null; + if (this.group) destination = this.map.grid.randomNodeInArea(this.group.predefinedArea); + else if (preSelectedDestinationNode) destination = preSelectedDestinationNode; + else if (this.chasingBehaviour && this.chasingBehaviour.target) destination = this.chasingBehaviour.position; + else destination = null; + + const predefinedArea = this.group ? this.group.predefinedArea : null; + this.emit(EVENT_DESTINATION_REACHED, { + destination, + predefinedArea + }); + } + + seekEnemy(searchedTarget) { + if (!searchedTarget) return; + if (!this.isAlive) return; + if (this.isAttacking()) return; + if (!this.behaviour.isAggressive) return; + + if (!this.searchedTarget) return this.searchedTarget = searchedTarget; + + let isBehaviourSatisfied = false; + + switch (this.behaviour.selectTargetType) { + case 1: + isBehaviourSatisfied = searchedTarget.hp.current - this.searchedTarget.hp.current <= 0; + break; + case 2: + isBehaviourSatisfied = this.searchedTarget.hp.current - searchedTarget.hp.current <= 0; + break; + case 3: + isBehaviourSatisfied = searchedTarget.hp.maximum - this.searchedTarget.hp.maximum < 0; + break; + case 4: + isBehaviourSatisfied = this.searchedTarget.hp.maximum - searchedTarget.hp.maximum < 0; + break; + case 5: + isBehaviourSatisfied = searchedTarget.speed - this.searchedTarget.speed < 0; + break; + case 6: + isBehaviourSatisfied = this.searchedTarget.speed - searchedTarget.speed < 0; + break; + case 7: + isBehaviourSatisfied = searchedTarget.sight - this.searchedTarget.sight < 0; + break; + case 8: + isBehaviourSatisfied = this.searchedTarget.sight - searchedTarget.sight < 0; + break; + }; + + const isInShootingRange = Utility.IsInDistance(this.position, searchedTarget.position, this.cannonRange); + const isSelectedInShootingRange = Utility.IsInDistance(this.position, this.searchedTarget.position, this.cannonRange); + if ((isBehaviourSatisfied && isInShootingRange) || !isSelectedInShootingRange) return this.searchedTarget = searchedTarget; + } + + trackChasedTarget() { + if (!this.chasingBehaviour || !this.chasingBehaviour.target) return; + + const isInRange = Utility.IsInDistance(this.position, this.chasingBehaviour.target.position, this.chasingBehaviour.range); + if (!isInRange) { + this.untrackChasedTarget(); + + return this.isDirty = true; + }; + + const chasedPosition = this.chasingBehaviour.target.position; + if (chasedPosition.x === this.chasingBehaviour.position.x && chasedPosition.y === this.chasingBehaviour.position.y) return; + + const threshold = Math.ceil((1000 - this.speed) / 150); + if (++this.chasingBehaviour.recalculateAccumulator === threshold) { + const isChasedTargetInShootingRange = Utility.IsInDistance(this.chasingBehaviour.position, chasedPosition, this.range); + if (!isChasedTargetInShootingRange) { + this.chasingBehaviour.position = { + x: chasedPosition.x, + y: chasedPosition.y + }; + + this.isDirty = true; + }; + + this.chasingBehaviour.recalculateAccumulator = 0; + }; + } + + untrackChasedTarget() { + if (!this.chasingBehaviour || !this.chasingBehaviour.target) return; + + this.chasingBehaviour.target = null; + this.chasingBehaviour.position.x = 0; + this.chasingBehaviour.position.y = 0; + this.chasingBehaviour.recalculateAccumulator = 0; + } + + move() { + const { position } = this; + const [currentX, currentY] = this.path[this.i]; + + const dx = currentX - position.x; + const dy = currentY - position.y; + + position.x += dx; + position.y += dy; + + if (this.group) { + this.emit("miniMapMove", { + id: this.id, + position: this.position + }); + }; + + this.updateChunk(); + + this.markActive(); + + if (this.searchedTarget) { + const target = this.map.getEntityFromSubgroup(this.searchedTarget.id); + if (target) this.initializeAttackWithCannon(target); + else this.searchedTarget = null; + }; + + if (this.i === this.path.length - 1 || this.isDirty) { + this.untrackChasedTarget(); + this.stopMovement(); + this.determineMovementState(); + } else { + const nextNode = this.path[++this.i]; + const [x, y] = nextNode; + + this.trackChasedTarget(); + + this.setEvent(TIMING_EVENT_MOVE, this.speed); + + this.emit(EVENT_DESTINATION_NEW, { + id: this.id, + typeID: this.typeID, + x, + y + }); + }; + } + + isAttacking = () => this.target ? true : false; + + getProperties() { + const { hp, position, id, speed, size, typeID, entityTypeID, path, behaviourBits, isMoving, isAdmiral, isSpeedLowered } = this; + const activeEffectsBits = Object.values(this.activeEffects).reduce((prev, cur, i) => cur ? prev + Math.pow(2, i) : prev + 0, 0); + + return { hp, position, id, speed, size, typeID, entityTypeID, path: path.length ? path[this.i] : [], behaviourBits, activeEffectsBits, isMoving, isAdmiral, isSpeedLowered }; + } +}; + +module.exports = NPC; \ No newline at end of file diff --git a/Game/PacketLimiter.js b/Game/PacketLimiter.js new file mode 100644 index 0000000..26b48a3 --- /dev/null +++ b/Game/PacketLimiter.js @@ -0,0 +1,21 @@ +class Limiter { + constructor(info) { + this.countThreshold = info.countThreshold; + this.timeThreshold = info.timeThreshold; + this.nextTime = 0; + this.time = 0; + this.count = 0; + }; + + update() { + if (performance.now() - this.time < this.timeThreshold) this.count++; + else { + this.time = performance.now(); + this.count = 1; + }; + + return this.count > this.countThreshold; + } +}; + +module.exports = Limiter; \ No newline at end of file diff --git a/Game/Player.js b/Game/Player.js new file mode 100644 index 0000000..30420a7 --- /dev/null +++ b/Game/Player.js @@ -0,0 +1,1536 @@ +const Entity = require("./Entity"); +const Quests = require("./QuestHandler"); +const Utility = require("./Utility"); +const DoubleLinkedList = require("../Utility/DoubleLinkedList"); + +const DESIGNS = require("../Data/designs"); +const AMMUNITIONS = require("../Data/ammunitions"); +const HARPOONS = require("../Data/harpoons"); +const EFFECTS = require("../Data/effects"); +const OVERTIME_HEALTH_EFFECTS = require("../Data/overtime"); +const LEVELS = require("../Data/levels"); +const REWARDS_LEVEL_PIRATE = require("../Data/levelRewards"); +const REWARDS_LEVEL_ELITE = require("../Data/eliteRewards"); + +const globalDieCallback = require("../Callbacks/die"); + +const Game = require("./Game"); +const { maps } = Game; + +const constants = require("../Data/constants"); + +const { EVENTS, TYPES, UNATTACKABLE_ENTITY_TYPES, MAP_MODES, LOGOUT_TIMES, TIMING_EVENTS, EVENTS_BY_TIMING_EVENTS_ID, BASE, AFFECTS, MAXIMUM } = constants; +const { EVENT_LEVEL_IS_LOW, EVENT_UPDATE_VISIBILITY, EVENT_DESTINATION_NEW, EVENT_ABLE_TO_JUMP, EVENT_UNABLE_TO_JUMP, EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_RESPAWN, EVENT_ATTACKING, EVENT_DISCONNECT, EVENT_DISCONNECT_CANCEL, EVENT_LEVEL_UP, EVENT_MAP_CHANGED, EVENT_PROPERTY_CHANGED, EVENT_SPEED_UPDATE, EVENT_ATTACK_ABORTED_BY_USER, EVENT_DAMAGE, EVENT_ITEM_USED, EVENT_CANNONS_USED, EVENT_HARPOONERS_USED, EVENT_QUEST_DONE, EVENT_EFFECT_ADD, EVENT_ATTACK_BAR, EVENT_PATH_READY, EVENT_HEALTH_CHANGED_AUTHORLESS } = EVENTS; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_MONSTER, ENTITY_TYPE_TOWER, ENTITY_TYPE_COLLECTABLE } = TYPES; +const { MAP_MODE_PVE, MAP_MODE_PVP } = MAP_MODES; +const { LOGOUT_TIME_PREMIUM } = LOGOUT_TIMES; +const { TIMING_EVENT_CANNON_ATTACK, TIMING_EVENT_HARPOONER_ATTACK, TIMING_EVENT_REPAIR, TIMING_EVENT_MAP_CHANGE, TIMING_EVENT_DISCONNECT, TIMING_EVENT_DISCONNECT_IDLE, TIMING_EVENT_MOVE, TIMING_EVENT_OVERTIME_DECREASE_HITPOINT, TIMING_EVENT_OVERTIME_DECREASE_HITPOINT_2, TIMING_EVENT_OVERTIME_INCREASE_HITPOINT } = TIMING_EVENTS; +const { BASE_REPAIR_AMOUNT_MINIMUM, BASE_REPAIR_AMOUNT_MAXIMUM, BASE_SPEED_PLAYER_REPAIR, BASE_SPEED_PLAYER } = BASE; +const { AFFECT_CANNON_DAMAGE, AFFECT_PROTECTION } = AFFECTS; +const { MAX_LEVEL_ELITE } = MAXIMUM; + +const statusLevels = ["pirateLevel", "eliteLevel", "fishingLevel"]; + +class Player extends Entity { + constructor(info) { + super({ + type: ENTITY_TYPE_PLAYER, + id: info.id, + map: info.map, + isAlive: !!info.hitpoints.current, + isAdmiral: false, + position: { + x: info.positionX, + y: info.positionY + } + }); + + this.hp = info.hitpoints; + const isSpeedLowered = this.isHPBelowPercentageThreshold(35); + + this.previousDestination = { + xw: -1, + xh: -1 + }; + + this.id = info.id; + this.sessionID = info.sessionID; + this.globalRank = info.globalRank; + this.levelRank = info.levelRank; + this.guild = info.guild; + this.moved = 0; + this.hasUnexpectedlyLeft = false; + this.textureID = info.textureID; + this.name = info.name || "?"; + this.speed = isSpeedLowered ? BASE_SPEED_PLAYER + BASE_SPEED_PLAYER / 2 : BASE_SPEED_PLAYER; + this.repairSpeed = 1500; + info.sight = 792; + this.sight = (Math.floor(info.sight / 12) + 0) * 12; + this.maximumCannonRange = 0//info.maximumCannonRange; + this.maximumHarpoonerRange = 0//info.maximumHarpoonerRange; + this.isMoving = false; + this.isPositionChanged = false; + this.isDirty = false; + this.lastHarpoonShotTickTimestamp = 0; + this.lastRepairTickTimestamp = 0; + this.isSpeedLowered = isSpeedLowered; + this.target = null; + this.attackers = []; + this.path = []; + this.scheduledPath = []; + this.isNewPathReady = false; + this.isLastNodeFinished = false; + this.cannonTypesInUse = []; + this.cannonTypesOutOfRange = []; + this.inDistance = {}; + this.lastFoundNeighbour = 0; + this.activeOvertimeHealthEffects = {}; + this.activeEffects = {}; + this.rewardCallback = info.rewardCallback; + this.attackCallback = info.attackCallback; + this.flushBuffer = {}; + this.neighbouringChunks = new Map(); + + this.lastInteractionTimestamp = 0; + this.isInactive = false; + + this.topics = new DoubleLinkedList(); + + this.rankingPoints = [0, 0, 0, 0].map((e, i) => ({ + id: i + 1, + amount: e + })); + + const points = [0, 0, 0, 0].map((e, i) => ({ + id: i + 1, + amount: e + })); + + for (let i = 0, length = info.statusPoints.length; i < length; i++) { + const point = info.statusPoints[i]; + points[point.id - 1] = { + id: point.id, + amount: point.amount + }; + }; + + this.statusPoints = points; + + this.pirateLevel = LEVELS.pirateLevel(points[0].amount); + this.eliteLevel = LEVELS.eliteLevel(points[1].amount); + this.fishingLevel = LEVELS.fishingLevel(points[2].amount); + + this.inventory = info.inventory; + + if (this.inventory.hasDesign(5)) { + const eliteLevelMaximum = Math.min(this.eliteLevel, MAX_LEVEL_ELITE); + this.inventory.resourceSharedLimiter.cannon.quantity.maximum += eliteLevelMaximum * 2; + }; + + for (let i = 0, equimpents = this.inventory.getCannons(), maximumRange = 0, length = equimpents.length; i < length; i++) { + const equimpent = equimpents[i]; + + if (equimpent.range > maximumRange && equimpent.amountEquipped) this.maximumCannonRange = equimpent.range; + + equimpent.resourceSharedLimiter = this.inventory.resourceSharedLimiter.cannon; + equimpent.resourceSharedLimiter.count(equimpent.amountEquipped); + }; + + for (let i = 0, equimpents = this.inventory.getHarpooners(), maximumRange = 0, length = equimpents.length; i < length; i++) { + const equimpent = equimpents[i]; + + if (equimpent.range > maximumRange && equimpent.amountEquipped) this.maximumHarpoonerRange = equimpent.range; + + equimpent.resourceSharedLimiter = this.inventory.resourceSharedLimiter.harpooner; + equimpent.resourceSharedLimiter.count(equimpent.amountEquipped); + }; + + this.quests = new Quests(info.quests, completedQuest => { + this.emit(EVENT_QUEST_DONE, { + questID: completedQuest.id, + isSuccessful: true + }); + + if (completedQuest.reward) completedQuest.reward.single(2, this.inventory.getPosessions(), rewards => this.rewardCallback({ isRegular: true, authorID: this.id, source: {}, rewards })); + }); + + const rawActiveStatusEffects = info.activeStatusEffects.map(s => s.statusID); + this.activeStatusEffects = this.activateStatusEffects(rawActiveStatusEffects); + } + + markAsDirty() { + + } + + ready() { + this.map.addPlayer(this); + + this.updateChunk(); + // this.updateRelevantChunks(); + + this.isAvailable = true; + this.lastFoundNeighbour = 0; + this.targetMap = null; + + this.markActive(); + this.refreshVisibilities(true); + + /*this.setEvent(TIMING_EVENT_OVERTIME_DECREASE_HITPOINT, 1000, { + id: 1, + authorTypeID: this.typeID, + authorID: this.id, + damage: 2000, + iid: 9 + });*/ + + //this.setEvent(5, 12000) + + + // this.setEvent(TIMING_EVENT_OVERTIME_DECREASE_HITPOINT_2, 1000, { id: 2, authorTypeID: 1, authorID: 7, iid: 12 }); + // this.setEvent(13, 1000, { id: 3, authorTypeID: 1, authorID: 7, iid: 13 }); + } + + activateStatusEffects(presentStatusEffects) { + const activeStatusEffects = []; + + presentStatusEffects.forEach(status => EFFECTS[status].forEach(itemID => { + const item = this.inventory.getItem(itemID); + if (!item) return; + + item.activate(); + activeStatusEffects.push(status); + })); + + return activeStatusEffects; + } + + flush(timingEvent) { + const event = EVENTS_BY_TIMING_EVENTS_ID[timingEvent]; + const currentFlush = this.flushBuffer[event]; + if (!currentFlush) return; + + this.emit(event, currentFlush.data, currentFlush.author); + + this.flushBuffer = {}; + } + + effectAdd(effectID) { + this.activeEffects[effectID] = true; + + this.emit(EVENT_EFFECT_ADD, { + effectID, + typeID: this.typeID, + id: this.id + }); + } + + effectRemove(effectID) { + this.activeEffects[effectID] = false; + + this.emit(EVENT_EFFECT_ADD, { + effectID, + typeID: this.typeID, + id: this.id + }); + + this.removeEvent(17); + } + + disableStatusEffect(statusID) { + const index = this.activeStatusEffects.findIndex(id => id === statusID); + if (index !== -1) this.activeStatusEffects.splice(index, 1); + } + + updateRankingPoints(rankID, amount) { + const rankingPointIndex = rankID - 1; + const current = this.rankingPoints[rankingPointIndex]; + if (!current) return; + + current.amount += amount; + } + + updateLevels(levelID, amount) { + const levelIndex = levelID - 1; + const current = this.statusPoints[levelIndex]; + if (!current) return; + + const levelType = statusLevels[levelIndex]; + current.amount = Math.max(0, current.amount + amount); + + const levelFormula = LEVELS[levelType]; + if (!levelFormula) return; + + const level = levelFormula(current.amount); + if (this[levelType] !== level) { + const levelBefore = this[levelType]; + + this[levelType] = level; + + if (levelIndex === 0) { + this.quests.refresh(level); + + for (let i = levelBefore + 1; i < level + 1; i++) { + const currentLevelRewards = REWARDS_LEVEL_PIRATE[i]; + if (currentLevelRewards) currentLevelRewards.single(0, this.inventory.getPosessions(), rewards => this.rewardCallback({ isRegular: true, authorID: this.id, source: {}, rewards })); + }; + }; + + if (levelIndex === 1 && this.inventory.hasDesign(5)) { + for (let i = levelBefore + 1; i < level + 1; i++) { + const currentEliteLevelRewards = REWARDS_LEVEL_ELITE[i]; + if (currentEliteLevelRewards) { + currentEliteLevelRewards.single(0, this.inventory.getPosessions(), rewards => this.rewardCallback({ isRegular: true, authorID: this.id, source: {}, rewards })); + + this.inventory.resourceSharedLimiter.cannon.quantity.maximum += 2; + }; + }; + + const ePropertyChanged = { + typeID: 5, + value: this.inventory.resourceSharedLimiter.cannon.quantity.maximum + }; + + this.emit(EVENT_PROPERTY_CHANGED, ePropertyChanged, this.id); + }; + + const eLevelUp = { + statusLevelID: levelID, + level, + id: this.id + }; + + this.emit(EVENT_LEVEL_UP, eLevelUp); + }; + } + + updateCannonRanges() { + if (!this.target) return; + + this.cannonTypesOutOfRange = this.cannonTypesOutOfRange.reduce((acc, cur) => { + const { cannonID, lastFiredAtTimestamp, event } = cur; + + const cannonType = this.cannonTypesInUse.find(c => c.id === cannonID); + const recheckShootingRange = Utility.IsInDistance(this.position, this.target.position, cannonType.range); + if (!recheckShootingRange) acc.push(cur); + else { + const throttle = Math.max((lastFiredAtTimestamp + cannonType.reload) - performance.now(), 0); + this.setReusableSingleEvent(TIMING_EVENT_CANNON_ATTACK, throttle, event); + }; + + return acc; + }, []); + } + + trackOvertimeHealthEffect(id) { + const effect = this.activeOvertimeHealthEffects[id]; + + if (effect) effect.count++; + else this.activeOvertimeHealthEffects[id] = { count: 0 }; + + return effect ? effect.count : 0; + } + + overtimeDecreaseCurrentHitpoint(event) { + if (!event) return + + const { id, authorTypeID, authorID, damage, iid } = event; + const { amount, ticks, time } = OVERTIME_HEALTH_EFFECTS[id]; + + const decrease = this.decreaseCurrentHitpoint(damage * amount, { + authorTypeID, + authorID + }); + + const currentCount = this.trackOvertimeHealthEffect(id); + if (currentCount < ticks && decrease.currentHitpoint) this.setIndependentEvent(iid, time, event); + else delete this.activeOvertimeHealthEffects[id]; + + const data = { + targetTypeID: this.typeID, + targetID: this.id, + amount: decrease.normalizedAmount, + type: 0, + isCritical: false + }; + + this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, data, this.id); + } + + increaseCurrentHitpoint(amount) { + amount = Math.abs(amount); + + const normalizedAmount = this.hp.current + amount <= this.hp.maximum ? amount : amount - (this.hp.current + amount - this.hp.maximum); + this.hp.current += normalizedAmount; + + if (this.isSpeedLowered && this.isHPAbovePercentageThreshold(35)) { + this.changeSpeed(-BASE_SPEED_PLAYER / 2); + this.isSpeedLowered = false; + }; + + return { + currentHitpoint: this.hp.current, + normalizedAmount + }; + } + + decreaseCurrentHitpoint(amount, author) { + amount = Math.abs(amount); + + let scaleSum = 1; + this.inventory.useAvailableItemsByAffect(AFFECT_PROTECTION, (scale, itemID, statusID, isStatusActive) => { + scaleSum += scale * 100; + + const itemUsage = { + statusID, + itemID, + amountToRemove: 1, + isActive: true + }; + + if (!isStatusActive) this.disableStatusEffect(statusID); + + this.emit(EVENT_ITEM_USED, itemUsage, this.id); + }); + + const reducedBy = Math.floor(amount * ((scaleSum - 1) / 100)); + const damage = amount - reducedBy; + const normalizedAmount = this.hp.current - damage >= 0 ? damage : damage - (damage - this.hp.current); + + this.hp.current -= normalizedAmount; + + if (!this.isSpeedLowered && this.isHPBelowPercentageThreshold(35)) { + this.changeSpeed(BASE_SPEED_PLAYER / 2); + this.isSpeedLowered = true; + }; + + this.cancelRepair(); + this.cancelDisconnect(); + + const { authorTypeID, authorID } = author; + + if (this.hp.current === 0) { + const eventDeath = { + typeID: this.typeID, + lastShotTypeID: authorTypeID, + lastShotID: authorID, + targetID: this.id, + targetEntityTypeID: 0 + }; + + this.setSingleEvent(11, 0, eventDeath); + }; + + return { + currentHitpoint: this.hp.current, + normalizedAmount + }; + } + + isAtMaximumHitpoints() { + return this.hp.current === this.hp.maximum; + } + + setDesign(id) { + if (this.isMoving) return; + if (this.isAttacking()) return; + if (this.isBeingAttacked()) return; + if (!this.isAlive) return; + + const hasDesign = this.inventory.hasDesign(id); + if (this.textureID === id && hasDesign) return; + + if (!hasDesign) return this.textureID = 1; + + const design = DESIGNS[id]; + if (!design) return; + + return this.textureID = id; + } + + die(death) { + if (!this.isAlive) return this.removeEvent(11); + + super.die(); + + this.cancelRepair(); + this.destroyMovement(); + this.abortAttack(); + this.abortAttackers(); + + this.hp.current = 0; + + // this.removeEvent(14); + + this.emit(EVENT_DIE, death, this.id); + + this.removeEvent(11); + this.removeEvent(TIMING_EVENT_OVERTIME_DECREASE_HITPOINT); + this.removeEvent(12); + this.removeEvent(13); + this.removeEvent(17); + + if (death.lastShotTypeID === ENTITY_TYPE_PLAYER) { + const admiralProperties = { + typeID: this.typeID, + entityTypeID: this.entityTypeID, + parentEntityTypeID: 0, + id: this.id, + isAdmiral: false, + authorID: death.lastShotID, + authorTypeID: death.lastShotTypeID + }; + + globalDieCallback(admiralProperties); + }; + + this.lastSinkID = death.lastShotID; + this.activeOvertimeHealthEffects = {}; + this.activeEffects = {}; + this.inDistance = {}; + } + + respawn() { + if (this.isAlive) return; + + this.hp.current = Math.floor(this.hp.maximum / 10); + + super.respawn(); + + //this.updateRelevantChunks(); + this.refreshVisibilities(true); + + if (this.isSpeedLowered && !this.isHPBelowPercentageThreshold(35)) { + this.speed /= 2; + this.isSpeedLowered = false; + }; + + this.emit(EVENT_RESPAWN); + //this.setEvent(TIMING_EVENT_OVERTIME_DECREASE_HITPOINT, 0, { id: 1, authorTypeID: 1, authorID: 7, iid: 9 }); + this.removeEvent(14); + } + + calculateRepairAmount() { + const baseRepairAmounts = [BASE_REPAIR_AMOUNT_MINIMUM, BASE_REPAIR_AMOUNT_MAXIMUM]; + const repairAmounts = [baseRepairAmounts[0], baseRepairAmounts[1]]; + let scaleSum = 1; + + /* this.inventory.useAvailableItemsByAffect(1, scale => { + + scaleSum += scale; + });*/ + + for (let i = 0; i < baseRepairAmounts.length; i++) repairAmounts[i] += baseRepairAmounts[i] * scaleSum; + + return Utility.RandomIntegerBetween(repairAmounts[0], repairAmounts[1]); + } + + calculateRepairSpeed() { + const baseRepairSpeed = BASE_SPEED_PLAYER_REPAIR; + let scaleSum = 1; +/* + this.inventory.useAvailableItemsByAffect(3, scale => { + + scaleSum += scale; + }); +*/ + return baseRepairSpeed / scaleSum; + } + + cancelRepair() { + if (!this.isRepairing()) return; + + this.cancelEvent(TIMING_EVENT_REPAIR); + + this.isDirty = false; + } + + repair(doneIndex) { + const { id } = this; + + this.lastRepairTickTimestamp = performance.now(); + + const repairAmount = this.calculateRepairAmount(); + // const repairSpeed = this.calculateRepairSpeed(); + + const healthUpdate = this.increaseCurrentHitpoint(repairAmount); + if (!healthUpdate.normalizedAmount) return; + + const eventRepair = { + targetTypeID: ENTITY_TYPE_PLAYER, + targetID: id, + amount: healthUpdate.normalizedAmount, + type: 1, + isCritical: false, + }; + + this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, eventRepair, this.id); + + if (healthUpdate.currentHitpoint >= this.hp.maximum) return this.cancelRepair(); + + this.setReusableSingleEvent(TIMING_EVENT_REPAIR, this.repairSpeed, doneIndex); + + return true; + } + + initializeRepair() { + if (this.isRepairing()) return false; + if (this.hp.current >= this.hp.maximum) return false; + if (!this.isAlive) return false; + + this.abortAttack(); + this.cancelDisconnect(); + + this.setReusableSingleEvent(TIMING_EVENT_REPAIR, 0, true); + + return true; + } + + isHPBelowPercentageThreshold(percentageThreshold) { + return this.hp.maximum / 100 * percentageThreshold > this.hp.current; + } + + isHPAbovePercentageThreshold(percentageThreshold) { + return this.hp.maximum / 100 * percentageThreshold < this.hp.current; + } + + changeSpeed(speed) { + this.speed += speed; + + const eventSpeedChange = { + typeID: this.typeID, + id: this.id, + speed: this.speed + }; + + this.emit(EVENT_SPEED_UPDATE, eventSpeedChange, this.id); + } + + disconnect() { + const { map, id } = this; + + this.abortAttackers(); + this.abortAttack(); + this.cancelRepair(); + this.destroyMovement(); + + //if (!this.isInactive) { + const entities = map.getLivingEntities([ENTITY_TYPE_PLAYER]); + entities.forEach(obj => delete obj.inDistance[id]); // this is inefficient + //} else global.ll.trace(this.id, "didn't remove inDistance", new Date()) + + this.emit(EVENT_DISCONNECT, id); + + this.removeEvent(TIMING_EVENT_DISCONNECT); + this.removeEvent(TIMING_EVENT_DISCONNECT_IDLE); + + this.events = {}; + + this.removeFromChunk(); + map.removePlayer(this.id); + } + + regularDisconnect() { + if (this.isBeingAttacked()) return + + this.abortAttackAlert(); + this.cancelRepair(); + + this.removeEvent(15); + this.targetMap = null; + this.tempSolutionFixThis = true; + + this.isDirty = this.isMoving; + + this.setIndependentEvent(TIMING_EVENT_DISCONNECT, LOGOUT_TIME_PREMIUM); + } + + cancelDisconnect() { + if (!this.isDisconnecting()) return; + + if (!this.hasUnexpectedlyLeft) this.emit(EVENT_DISCONNECT_CANCEL); + this.cancelEvent(TIMING_EVENT_DISCONNECT); + + this.isDirty = false; + } + + calculateHarpoonerDamage(id) { + const chosenHarpoon = this.inventory.getSeletedHarpoon(); + if (!chosenHarpoon) return false; + + const harpoonScheme = HARPOONS[this.inventory.selectedHarpoon]; + if (!harpoonScheme) return false; + + let damage = 0; + let scaleSum = 1; + let k = 0; + + const harpooners = this.inventory.getHarpooners(); + for (let i = 0, length = harpooners.length; i < length; i++) { + const harpoonerType = harpooners[i]; + + for (let j = 0, length = harpoonerType.amountEquipped; j < length && k < chosenHarpoon.amount; j++) { + const minimum = (harpoonerType.damage * (/*harpoonerType.scatter*/ 100 / 100) + 1) * scaleSum; + const maximum = harpoonerType.damage * scaleSum; + + if (id === harpoonerType.id) { + damage += Math.ceil((Math.random() * (maximum - minimum) + minimum) * (harpoonScheme.damage / 100)); + k++; + }; + }; + }; + + return { + damage, + usedHarpoonerCount: k, + usedHarpoon: harpoonScheme + }; + } + + getHarpoonersStatistics() { + let maximumHarpoonerRange = 0; + const distinctHarpoonerTypes = this.inventory.getHarpooners().reduce((acc, cur) => { + const isAlreadyAdded = acc.some(c => c.id === cur.id); + if (!isAlreadyAdded && cur.amountEquipped) { + if (cur.range > maximumHarpoonerRange) maximumHarpoonerRange = cur.range; + + acc.push({ + id: cur.id, + reload: cur.reload, + range: cur.range, + eventID: cur.eventID + }); + }; + + return acc; + }, []); + + return { + distinctHarpoonerTypes, + maximumHarpoonerRange + }; + } + + getCannonsStatistics() { + let maximumCannonRange = 0; + const distinctCannonTypes = this.inventory.getCannons().reduce((acc, cur) => { + const alreadyAddedType = acc.find(c => c.id === cur.id); + if (!alreadyAddedType && cur.amountEquipped) { + if (cur.range > maximumCannonRange) maximumCannonRange = cur.range; + acc.push(cur); + }; + + return acc; + }, []); + + return { + distinctCannonTypes, + maximumCannonRange + }; + } + + calculateCannonDamage(id) { + const { inventory } = this; + const selected = inventory.getSelectedAmmunition(); + const ammunitionScheme = AMMUNITIONS[inventory.selectedAmmunition]; + + let scaleSum = 1; + let damage = 0; + let k = 0; + + inventory.useAvailableItemsByAffect(AFFECT_CANNON_DAMAGE, (scale, itemID, statusID, isStatusActive) => { + scaleSum += scale; + + const itemUsage = { + statusID, + itemID, + amountToRemove: 1, + isActive: true + }; + + if (!isStatusActive) this.disableStatusEffect(statusID); + + this.emit(EVENT_ITEM_USED, itemUsage, this.id); + }); + + const cannons = this.inventory.getCannons(); + for (let i = 0, length = cannons.length; i < length; i++) { + const cannonType = cannons[i]; + + for (let j = 0, length = cannonType.amountEquipped; j < length && k < selected.amount; j++) { + const minimum = ((cannonType.damage * ((100 + cannonType.scatter) / 100) + 1) * scaleSum) - 1; + const maximum = cannonType.damage * scaleSum; + + if (id === cannonType.id) { + damage += Math.ceil((Math.random() * (maximum - minimum) + minimum) * ammunitionScheme.damage); + k++; + }; + }; + }; + + return { + usedCannonCount: k, + value: damage, + critical: false, + usedAmmunition: ammunitionScheme + }; + } + + attackWithHarpooner(event) { + const { target } = this; + if (!target) return + + const harpoonerType = this.harpoonerTypesInUse.find(h => h.id === event.id); + + const isStillInShootingRange = Utility.IsInDistance(this.position, target.position, this.maximumHarpoonerRange); + if (!isStillInShootingRange) return this.abortAttackAlert(); + + this.lastHarpoonShotTickTimestamp = performance.now(); + + const attackingAnimation = { + authorSessionID: this.sessionID, + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + entityTypeID: target.entityTypeID + }; + + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + amount: 0, + isCritical: false, + type: 0 + }; + + const damage = this.calculateHarpoonerDamage(event.id); + const healthUpdate = target.decreaseCurrentHitpoint(damage.damage, { + authorSessionID: this.sessionID, + authorID: this.id + }); + + attacking.amount = healthUpdate.normalizedAmount; + attacking.isCritical = false; + + const flushBuffer = this.flushBuffer[EVENT_ATTACKING]; + if (flushBuffer) flushBuffer.data[0].amount += attacking.amount; + else { + const data = [attacking, attackingAnimation]; + this.flushBuffer[EVENT_ATTACKING] = { + data, + author: target.id + }; + }; + + /*if (healthUpdate.currentHitpoint <= 0)*/ this.flush(TIMING_EVENT_HARPOONER_ATTACK); + + // this.updateLevels(3, 25000); + + this.rewardCallback({ + isRegular: true, + authorID: this.id, + source: {}, + rewards: [{ + categoryID: 6, + id: 3, + amount: 500 + }] + }); + + const chosenHarpoon = this.inventory.getSeletedHarpoon(); + chosenHarpoon.amount -= damage.usedHarpoonerCount; + + if (chosenHarpoon.amount <= 0) { + const entries = this.inventory.getHarpoons(); + const entry = entries.find(item => this.inventory.selectedHarpoon !== item.id && item.amount); + this.inventory.setSelectedHarpoon(entry ? entry.id : 0); + + this.abortAttackAlert(); + }; + + this.emit(EVENT_HARPOONERS_USED, { amount: damage.usedHarpoonerCount, type: 1 }, this.id); + + if (target.hp.current <= 0) return; + + this.setReusableSingleEvent(harpoonerType.eventID, harpoonerType.reload, event); + + return true; + } + + initializeAttackWithHarpooner(selectedTarget) { + const harpoonersStatistics = this.getHarpoonersStatistics(); + + if (UNATTACKABLE_ENTITY_TYPES.includes(selectedTarget.typeID)) return; + + if (selectedTarget.typeID !== ENTITY_TYPE_MONSTER) return; + + const isInShootingRange = Utility.IsInDistance(this.position, selectedTarget.position, this.maximumHarpoonerRange); + if (!isInShootingRange) return; + + const chosenHarpoon = this.inventory.getSeletedHarpoon(); + if (!chosenHarpoon || chosenHarpoon.amount <= 0) return; + + if (this.target && selectedTarget.id === this.target.id) return; + if (!selectedTarget.isAlive) return; + if (!this.isAlive) return; + if (this.isRepairing()) return; + + if (this.target && this.target.id !== selectedTarget.id) this.abortAttack(); + + this.cancelDisconnect(); + + this.target = selectedTarget; + + const attackInformation = { + attackerEntry: this.id, + sessionID: this.sessionID + }; + + selectedTarget.attackers.push(attackInformation); + + this.emit(EVENT_ATTACK_STARTED, { + authorID: selectedTarget.id, + authorTypeID: selectedTarget.typeID, + }, this.id); + + this.emit(EVENT_ATTACK_BAR, { + authorID: selectedTarget.id, + authorTypeID: selectedTarget.typeID, + isTarget: true, + source: 1 + }, this.id); + + this.harpoonerTypesInUse = harpoonersStatistics.distinctHarpoonerTypes; + this.maximumHarpoonerRange = harpoonersStatistics.maximumHarpoonerRange; + + harpoonersStatistics.distinctHarpoonerTypes.forEach(h => { + const throttle = (this.lastHarpoonShotTickTimestamp + h.reload) - performance.now(); + this.setReusableSingleEvent(h.eventID, throttle > 0 ? throttle : 0, { id: h.id }); + + }); + } + + attackWithCannon(event) { + const { target } = this; + if (!target) return; + + const ammunitionScheme = AMMUNITIONS[this.inventory.selectedAmmunition]; + if (target.typeID === ENTITY_TYPE_PLAYER && this.map.type === MAP_MODE_PVE && ammunitionScheme.damage > 0) return this.abortAttackAlert(); + + /* + idk if best solution + could check on ammo change request whether player is currenctly changing from heal ammo to dmg ammo + and abort if so + + but also, when they run out of heal ammo, and automatically changes to a dmg one + it would continue attacking if this check wasn't here + */ + + const cannonType = this.cannonTypesInUse.find(c => c.id === event.id); + const recheckShootingRange = Utility.IsInDistance(this.position, target.position, cannonType.range); + if (!recheckShootingRange) { + this.cannonTypesOutOfRange.push({ + cannonID: cannonType.id, + lastFiredAtTimestamp: performance.now(), + event + }); + + if (this.maximumCannonRange === cannonType.range) { + this.abortAttackAlert(); + + if (target.typeID === ENTITY_TYPE_NPC) target.determineMovementState(); + } else { + this.cancelEvent(cannonType.eventID); + }; + + return true; + }; + + cannonType.lastShotTickTimestamp = performance.now(); + + const damage = this.calculateCannonDamage(cannonType.id); + + const healthUpdate = damage.value >= 0 ? + target.decreaseCurrentHitpoint(damage.value, { + authorTypeID: this.typeID, + authorID: this.id, + authorGuildID: this.guild ? this.guild.id : 0 + }) : target.increaseCurrentHitpoint(damage.value); + + if (damage.value < 0 && !healthUpdate.normalizedAmount) return this.abortAttackAlert(); + + const attackingAnimation = { + authorSessionID: this.sessionID, + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + entityTypeID: target.entityTypeID + }; + + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + amount: healthUpdate.normalizedAmount, + isCritical: damage.critical, + type: damage.value <= 0 + }; + + const flushBuffer = this.map.eventBuffer[this.id]; + if (flushBuffer) flushBuffer.data[0].amount += attacking.amount; + else { + const data = [attacking, attackingAnimation]; + this.map.eventBuffer[this.id] = { + data, + author: target.id, + target: this.id + }; + + this.map.eventAuthors.push(this.id); + }; + + this.emit(EVENT_CANNONS_USED, { + amount: damage.usedCannonCount, + type: 0 + }, this.id); + + //this.flush(1); + + const terminateTreshold = Utility.RandomIntegerBetween(1, 100); + if (target.speed && terminateTreshold <= damage.usedAmmunition.chanceToTerminateMovement && !target.activeEffects[1]) { + target.haltMovement(); + + target.effectAdd(1); + target.setSingleEvent(17, 20000, 1); + }; + + const overtimeHealthEffectID = damage.usedAmmunition.overtimeHealthEffectID; + if (overtimeHealthEffectID) { + target.setEvent(13, 1000, { + id: overtimeHealthEffectID, + authorTypeID: this.typeID, + authorID: this.id, + authorGuildID: this.guild.id, + damage: healthUpdate.normalizedAmount, + iid: 13 + }); + }; + + const selected = this.inventory.getSelectedAmmunition(); + selected.amount -= damage.usedCannonCount; + + if (selected.amount <= 0) { + const entries = this.inventory.getAmmunitions(); + const entry = entries.find(item => this.inventory.selectedAmmunition !== item.id && item.amount); + + this.inventory.setSelectedAmmunition(entry ? entry.id : 0); + + this.abortAttackAlert(); + }; + + // this.updateLevels(2, 500); + this.rewardCallback({ + isRegular: true, + authorID: this.id, + source: {}, + rewards: [{ + categoryID: 6, + id: 2, + amount: 500 + }] + }); + + if (target.isAtMaximumHitpoints()) return this.abortAttackAlert(); + if (healthUpdate.currentHitpoint <= 0) return; + + this.setReusableSingleEvent(cannonType.eventID, cannonType.reload, event); + + return true; + } + + initializeAttackWithCannon(selectedTarget) { + const cannonsStatistics = this.getCannonsStatistics(); + const isInShootingRange = Utility.IsInDistance(this.position, selectedTarget.position, cannonsStatistics.maximumCannonRange); + + if (UNATTACKABLE_ENTITY_TYPES.includes(selectedTarget.typeID)) return; + if (!isInShootingRange) return; + if (this.isRepairing()) return; + + const ammunitionScheme = AMMUNITIONS[this.inventory.selectedAmmunition]; + if (selectedTarget.typeID === ENTITY_TYPE_PLAYER && this.map.type === MAP_MODE_PVE && ammunitionScheme.damage > 0) return; + + const selected = this.inventory.getSelectedAmmunition(); + if (!selected || selected.amount <= 0) return; + + if (ammunitionScheme.damage < 0 && selectedTarget.isAtMaximumHitpoints()) return; + + if (this.target && this.target.id === selectedTarget.id) return; + if (selectedTarget.id === this.id) return; + if (!selectedTarget.isAlive) return; + if (!this.isAlive) return; + if (selectedTarget.type === ENTITY_TYPE_TOWER && selectedTarget.guild === this.guild) return; + if (!this.inventory.getCannons().some(c => c.amountEquipped)) return; + + if (this.target && this.target.id !== selectedTarget.id) this.abortAttack(); + + this.cancelDisconnect(); + + const attackInformation = { + attackerEntry: this.id, + sessionID: this.sessionID + }; + + selectedTarget.attackers.push(attackInformation); + + this.target = selectedTarget; + + if (selectedTarget.typeID === ENTITY_TYPE_NPC) { + selectedTarget.haltMovement(); + selectedTarget.initializeAttackWithCannon(this); + } + // + else if (selectedTarget.typeID === ENTITY_TYPE_PLAYER) { + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + isTarget: false, + source: 1 + }; + + selectedTarget.emit(EVENT_ATTACK_BAR, attacking, this.id); + }; + + const attacking = { + authorID: selectedTarget.id, + authorTypeID: selectedTarget.typeID, + isTarget: true + }; + + this.emit(EVENT_ATTACK_STARTED, attacking, this.id); + this.emit(EVENT_ATTACK_BAR, attacking, this.id); + + this.cannonTypesInUse = cannonsStatistics.distinctCannonTypes; + this.maximumCannonRange = cannonsStatistics.maximumCannonRange; + + cannonsStatistics.distinctCannonTypes.forEach(c => this.setReusableSingleEvent(c.eventID, c.reload - (performance.now() - c.lastShotTickTimestamp), { id: c.id })); + } + + wipeAttack() { + this.cancelEquippedExtensions(); + + this.target = null; + } + + abortAttack() { + if (!this.isAttacking()) return; + if (!this.target) return; + + this.cancelEquippedExtensions(); + + const target = { + id: this.target.id, + typeID: this.target.typeID + }; + + this.target.attackers = this.target.attackers.filter(data => data.attackerEntry !== this.id); + + if (this.target.typeID === ENTITY_TYPE_NPC && !this.target.isAttacking()) this.target.determineMovementState(); + // if npc doesn't attack or doesn't have cannons + + this.target = null; + + return target; + } + + abortAttackAlert() { + if (!this.isAttacking()) return; + if (!this.target) return; + + this.cancelEquippedExtensions(); + + const eventAttackAbort = { + authorID: this.target.id, + authorTypeID: this.target.typeID, + targetID: this.id, + targetTypeID: this.typeID + }; + + this.target.attackers = this.target.attackers.filter(data => data.attackerEntry !== this.id); + + if (this.target.typeID === ENTITY_TYPE_NPC && !this.target.isAttacking()) this.target.determineMovementState(); + + this.emit(EVENT_ATTACK_ABORTED_BY_USER, eventAttackAbort); + + this.target = null; + } + + destroyMovement() { + if (!this.isMoving) return; + + this.destination = null; + this.isDirty = false; + this.isMoving = false; + this.path = []; + this.i = 0; + console.info("Destroyed movement on map", this.map.id); + this.removeEvent(TIMING_EVENT_MOVE); + } + + changeMap() { + this.removeEvent(15); + + const { targetMap } = this; + if (!targetMap) return; + + if (targetMap.level > this.pirateLevel) return; + + const { map } = this; + const newMapPosition = map.neighbours.find(m => m.id === targetMap.id); + if (!newMapPosition) return console.info("no newMapPosition"); + + const oldMapID = this.map.id; + + this.moved = 0; + this.scheduledPath.length = 0; + this.isNewPathReady = false; + this.isLastNodeFinished = false; + + this.removeFromChunk(); + this.destroyMovement(); + this.cancelDisconnect(); + this.cancelRepair(); + this.abortAttack(); + this.abortAttackers(); + + map.switchPlayer(this, targetMap); + this.setLocalization(targetMap.id); + + const entitiesWereInDistance = map.getNearbyPlayersGameID(this); + entitiesWereInDistance.forEach(id => { + const player = map.getEntityFromSubgroup(id); + player.propagatePresenceToEntity(this); + }); + + this.inDistance = {}; + + const { position } = this; + this.position[newMapPosition.align] = newMapPosition.align === "x" ? Math.abs(position[newMapPosition.align] - map.w) : Math.abs(position[newMapPosition.align] - map.h); + + const otherAlign = newMapPosition.align === "x" ? "h" : "w"; + const checkOtherAlign = newMapPosition.align === "x" ? position.y : position.x; + if (checkOtherAlign > map[otherAlign]) position[otherAlign] = map[otherAlign]; + + console.info("Position set", this.position, "Map changed to", targetMap.id, performance.now(), this.map.neighbours.map(e => e.id)) + + this.emit(EVENT_MAP_CHANGED, { + oldMap: maps[oldMapID - 1], + newMap: { + mapID: this.map.id, + x: this.position.x, + y: this.position.y + } + }, this.id); + + this.updateChunk(); + this.refreshVisibilities(true); + this.markActive(); + + this.lastFoundNeighbour = 0; + this.targetMap = null; + this.found = false; + + this.removeEvent(TIMING_EVENT_MAP_CHANGE); + + return { + mapID: this.map.id, + x: this.position.x, + y: this.position.y + }; + } + + isChangeMapAvailable() { + const { position, map } = this; + + this.removeEvent(TIMING_EVENT_MAP_CHANGE); + this.notWorking = true; + + const mapBoundaries = [position.x - 96 <= 0, position.x >= map.w - 96, position.y <= 96, position.y >= map.h - 96]; + const foundNeighbourIndex = mapBoundaries.findIndex(e => e) + 1; + if (!foundNeighbourIndex) return; + + const recognizedNeighbourMap = map.neighbours.find(n => n.directionIndex === foundNeighbourIndex); + if (!recognizedNeighbourMap) return; + + const targetMap = maps.find(m => m.id === recognizedNeighbourMap.id); + if (!targetMap) return; + + if (this.isDisconnecting()) return this.tempSolutionFixThis = true; + + if (targetMap.level <= this.pirateLevel) { + if (!this.targetMap) { + const eventAbleToJump = { + isAbleToJump: true, + newMapID: targetMap.id + }; + + this.emit(EVENT_ABLE_TO_JUMP, eventAbleToJump); + }; + + console.info("TargetMap is set to be", targetMap.id, performance.now(), this.map.neighbours.map(e => e.id)); + this.notWorking = false; + this.targetMap = targetMap; + } else this.emit(EVENT_LEVEL_IS_LOW); + } + + move() { + const { position, path, map, i } = this; + const currentStep = path[i]; + const dx = currentStep[0] - position.x; + const dy = currentStep[1] - position.y; + + position.x += dx; + position.y += dy; + + this.nextNode = path[i === path.length - 1 ? i : i + 1]; + + const mapBoundaries = this.targetMap ? + [position.x - 96 <= 0, position.x >= map.w - 96, position.y <= 96, position.y >= map.h - 96] + : + [position.x - 72 <= 0, position.x >= map.w - 72, position.y <= 72, position.y >= map.h - 72]; + + const foundNeighbour = mapBoundaries.findIndex(n => n) + 1; + if (foundNeighbour) { + if (this.lastFoundNeighbour !== foundNeighbour) { + this.lastFoundNeighbour = foundNeighbour; + + const switchableMap = this.map.neighbours.find(n => n.directionIndex === foundNeighbour); + if (switchableMap && !this.targetMap) this.setSingleEvent(TIMING_EVENT_MAP_CHANGE, 2000); + }; + } else { + this.notWorking = false; + this.lastFoundNeighbour = 0; + + if (this.targetMap) { + const eventUnableToJump = { + isAbleToJump: false, + newMapID: 0 + }; + + this.emit(EVENT_ABLE_TO_JUMP, eventUnableToJump); + }; + + this.targetMap = null; + this.tempSolutionFixThis = false; + }; + + if (this.target && this.target.typeID === ENTITY_TYPE_NPC) this.target.initializeAttackWithCannon(this); + + this.updateCannonRanges(); + + this.updateChunk(); + + this.markActive(); + + if (this.intermediateMovementWasStarted) { + this.intermediateMovementWasStarted = false; + this.destroyMovement(); + + return this.emit(EVENT_PATH_READY, []); + }; + + if (i === path.length - 1) { + this.checkForCollectables(); + this.destroyMovement(); + } else { + if (this.isDirty) this.destroyMovement(); + else { + const currentNode = path[++this.i]; + const [x, y] = currentNode; + + this.setEventTest2(TIMING_EVENT_MOVE, this.speed); + + this.emit(EVENT_DESTINATION_NEW, { + id: this.id, + typeID: this.typeID, + x, + y + }); + }; + }; + + this.isPositionChanged = true; + + if (this.target && !this.inDistance[this.target.id]) this.abortAttack(); + } + + initializeMovement(path) { + if (!Array.isArray(path)) return; + if (path.length === 0) return; + if (this.activeEffects[1]) return; + + this.path = path; + this.i = 0; + + this.nextNode = path[this.i]; + this.isMoving = true; + this.isDirty = false; + this.movementStartedAt = performance.now(); + + this.cancelEvent(TIMING_EVENT_MOVE); + this.setEvent(TIMING_EVENT_MOVE, this.speed); + + const [x, y] = this.nextNode; + this.emit(EVENT_DESTINATION_NEW, { + id: this.id, + typeID: this.typeID, + x, + y + }); + } + + refreshVisibilities(isForced) { + const xMin = Math.max(0, Math.floor((this.position.x - this.sight - 60) / this.map.cw)); + const xMax = Math.min(this.map.chunkColumns - 1, Math.floor((this.position.x + this.sight + 60) / this.map.cw)); + const yMin = Math.max(0, Math.floor((this.position.y - this.sight - 60) / this.map.ch)); + const yMax = Math.min(this.map.chunkRows - 1, Math.floor((this.position.y + this.sight + 60) / this.map.ch)); + + for (let x = xMin; x <= xMax; x++) { + for (let y = yMin; y <= yMax; y++) { + const chunk = this.map.chunksData[x * 5 + y]; + const target = !this.isPositionChanged && !isForced ? this.map[chunk.nameTest] : this.map[chunk.name]; + + target.each((_, entity) => { + if (!entity.hasUnexpectedlyLeft) { + if (entity.typeID === ENTITY_TYPE_PLAYER) entity.propagatePresenceToEntity(this); + else this.propagatePresenceToEntity(entity); + }; + + if (entity.typeID === ENTITY_TYPE_NPC || entity.typeID === ENTITY_TYPE_TOWER) entity.seekEnemy(this); + }); + }; + }; + + this.isPositionChanged = false; + } + + propagatePresenceToEntity(entity) { + if (this.hasUnexpectedlyLeft) return; + if (this.typeID === ENTITY_TYPE_PLAYER && !this.isAvailable) return; + if (!this.isAlive) return; + if (!entity.isAlive) return; + if (entity.id === this.id) return; + + const { position, sight, map } = this; + + const containsEntity = this.inDistance[entity.id]; + const isInViewDistance = containsEntity ? Utility.IsInDistance(position, entity.position, sight + 0) : Utility.IsInDistance(position, entity.position, sight - 0); + + if (!containsEntity && isInViewDistance && map.id === entity.map.id) { + const data = entity.getProperties(); + + const diplomacyGuildID = entity.typeID === ENTITY_TYPE_TOWER ? entity.island.guild.id : entity.guild ? entity.guild.id : 0; + data.guildColor = this.guild ? this.guild.diplomacy[diplomacyGuildID] : 0; + + this.inDistance[entity.id] = true; + + this.emit(EVENT_UPDATE_VISIBILITY, { + type: 1, + data + }); + }; + + if (containsEntity && (!isInViewDistance || map.id !== entity.map.id || entity.isInactive)) { + delete this.inDistance[entity.id]; + + this.emit(EVENT_UPDATE_VISIBILITY, { + type: 0, + data: { + typeID: entity.typeID, + id: entity.id + } + }); + }; + } + + checkForCollectables() { + const { position, map, chunk } = this; + + let standingOnCollectable = null; + map[chunk].each((_, entity) => { + if (entity.typeID === ENTITY_TYPE_COLLECTABLE && entity.position.x === position.x && entity.position.y === position.y) { + standingOnCollectable = entity; + + return false; + }; + + return true; + }); + + if (!standingOnCollectable) return; + + const eventCollect = { + typeID: standingOnCollectable.typeID, + targetID: standingOnCollectable.id, + targetEntityTypeID: standingOnCollectable.entityTypeID, + lastShotTypeID: this.typeID, + lastShotID: this.id + }; + + standingOnCollectable.emit(EVENT_DIE, eventCollect, standingOnCollectable.id); + standingOnCollectable.die({ + authorID: this.id, + authorSessionID: this.sessionID + }); + } + + abortAttackers() { + this.attackers.forEach(data => { + const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry); + attackerEntity.wipeAttack(); + }); + + this.attackers.length = 0; + } + + cancelEquippedExtensions() { + if (!this.isAttacking()) return; + + if (this.target.typeID === ENTITY_TYPE_MONSTER) { + this.harpoonerTypesInUse.forEach(h => this.cancelEvent(h.eventID)); + this.harpoonerTypesInUse.length = 0; + } else { + this.cannonTypesInUse.forEach(c => this.cancelEvent(c.eventID)); + this.cannonTypesInUse.length = 0; + this.cannonTypesOutOfRange = []; + }; + } + + haltMovement() { + this.isDirty = this.isMoving; + } + + getProperties() { + const { maximumCannonRange, maximumHarpoonerRange, pirateLevel, eliteLevel, fishingLevel, globalRank, levelRank, isSpeedLowered, isMoving, hp, position, name, guild, map, id, textureID, speed, type, typeID, sight, path } = this; + return { cannonRange: maximumCannonRange, harpoonerRange: maximumHarpoonerRange, pirateLevel, eliteLevel, fishingLevel, globalRank, levelRank, isAlive: this.isAlive, isSpeedLowered, isRepairing: this.isRepairing(), isMoving, hp, position, name, guildTag: guild ? guild.tag : null, mapID: map.id, id, textureID, speed, type, typeID, sight, currentNode: path.length ? path[this.i] : [] }; + } + + isAttacking = () => this.target ? true : false; + + isBeingAttacked = () => this.attackers.length ? true : false; + + isRepairing() { + const event = this.events[TIMING_EVENT_REPAIR]; + return event && !event[event.length - 1].isCancelled ? true : false; + } + + isDisconnecting() { + const event = this.events[TIMING_EVENT_DISCONNECT]; + return event && !event[event.length - 1].isCancelled ? true : false; + } +}; + +module.exports = Player; diff --git a/Game/PlayerBehaviour.js b/Game/PlayerBehaviour.js new file mode 100644 index 0000000..f1ad8c1 --- /dev/null +++ b/Game/PlayerBehaviour.js @@ -0,0 +1,39 @@ +class PlayerBehaviour { + constructor(info) { + this.id = info.id; + this.size = info.size ?? 32; + this.entries = Array.from({ length: this.size }, () => ({ isProcessed: false, timestamp: Infinity, entry: {}, id: 0 })); + this.accumulator = 0; + this.buffer = info.buffer; + }; + + add(entry) { + const index = this.accumulator++ % this.size; + this.entries[index] = { + timestamp: Date.now(), + entry, + id: this.id, + isProcessed: false + }; + } + + save(threshold) { + const now = Date.now(); + + for (let i = 0; i < this.size; i++) { + const entry = this.entries[i]; + if (entry.id === 0) continue; + if (entry.isProcessed) continue; + if (entry.timestamp + threshold < now) continue; + + entry.isProcessed = true; + // require("../Miscellaneous/winston").error(entry) + + this.buffer.push(entry.entry); // entry.entry + }; + + return this.buffer; + } +}; + +module.exports = PlayerBehaviour; \ No newline at end of file diff --git a/Game/Quest.js b/Game/Quest.js new file mode 100644 index 0000000..d292879 --- /dev/null +++ b/Game/Quest.js @@ -0,0 +1,68 @@ +const EventEmitter = require("events"); +const Reward = require("./Reward"); + +class Quest extends EventEmitter { + constructor(configuration, doneCallback) { + super(); + + this.id = configuration.id; + this.currentComplete = configuration.completedAmount; + this.maximumComplete = configuration.maximumComplete; + this.task = JSON.parse(JSON.stringify(configuration.task)); + this.reward = new Reward({ + distributionType: 2, + baseReward: configuration.reward + }); + this.name = configuration.name || "Quest without a name"; + this.finishedCount = 0; + this.totalTasksCount = Object.values(this.task).reduce((pre, cur) => pre + cur.length, 0); + this.doneCallback = doneCallback; + } + + adjustTask(events, amount) { + events.forEach(type => { + const tasksInCategory = this.task[type.taskCategoryID]; + if (!tasksInCategory) return + + tasksInCategory.forEach(task => { + if (task.isFinished) return + + if (type.typeID === task.typeID && type.entityTypeID === task.entityTypeID) { + task.amount += amount; + + + + if (task.requiredAmount <= task.amount) { + task.amount = task.requiredAmount; + task.isFinished = true; + this.finishedCount++; + + + }; + }; + }); + }); + + if (this.finishedCount === this.totalTasksCount) { + + this.doneCallback(this.id); + }; + } + + getTasks() { + const taskTypes = Object.keys(this.task); + return taskTypes.map(t => this.task[t]); + } + + getProgression() { + const taskTypes = Object.keys(this.task); + const progression = []; + taskTypes.forEach(t => this.task[t].forEach((c, i) => { + progression.push({ amount: c.amount, index: i, type: t }); + })); + + return progression; + } +}; + +module.exports = Quest; \ No newline at end of file diff --git a/Game/QuestHandler.js b/Game/QuestHandler.js new file mode 100644 index 0000000..9330807 --- /dev/null +++ b/Game/QuestHandler.js @@ -0,0 +1,86 @@ +const Quest = require("./Quest"); +const quests = require("../Data/quests"); + +class Quests { + constructor(quests, doneCallback) { + let currentQuest = quests[0]; + + this.completedQuests = []; + this.finishedQuests = {}; + this.completedQuestsAmount = {}; + this.doneCallback = doneCallback; + this.list = {}; + this.list = quests.reduce((result, entry) => { + this.completedQuestsAmount[entry.questID] = entry.completedAmount; + if (entry.state === 1 || entry.state === 2) return result; + + const quest = this.add(entry.questID); + if (!quest) return result; + + if (currentQuest.id !== entry.questID) { + result[entry.questID] = quest; + currentQuest = quest; + }; + + if (!entry.amount) return result; + + const { typeID, entityTypeID } = result[entry.questID].task[entry.taskType][entry.taskIndex]; + currentQuest.adjustTask([{ + taskCategoryID: entry.taskType, + typeID, + entityTypeID + }], entry.amount); + + return result; + }, {}); + }; + + add(id) { + const questScheme = quests[id - 1]; + if (!questScheme) return; + + const isAcceptable = !questScheme.maximumComplete || questScheme.maximumComplete && questScheme.maximumComplete > this.completedQuestsAmount[id] || !this.completedQuestsAmount[id] && questScheme.maximumComplete; + if (!isAcceptable) return; + + const isQuestAlreadyAdded = this.list[id]; + if (isQuestAlreadyAdded) return true; + + const quest = new Quest(questScheme, completedQuestID => { + if (questScheme.maximumComplete !== -1) this.completedQuests.push(completedQuestID); + + if (!this.completedQuestsAmount[completedQuestID]) this.completedQuestsAmount[completedQuestID] = 1; + else this.completedQuestsAmount[completedQuestID]++; + + const completedQuest = this.list[completedQuestID]; + this.doneCallback(completedQuest); + this.remove(completedQuestID); + }); + + delete this.finishedQuests[id]; + + this.list[id] = quest; + + return quest; + } + + remove(id) { + const questScheme = quests[id - 1]; + if (!questScheme) return; + + this.finishedQuests[id] = true; + delete this.list[id]; + } + + has(id) { + return this.list[id] ? true : false; + } + + refresh(pirateLevel) { + for (const id in this.list) { + const questScheme = quests[+id - 1]; + if (questScheme.maximumLevel < pirateLevel || questScheme.minimumLevel > pirateLevel) this.remove(+id); + }; + } +}; + +module.exports = Quests; \ No newline at end of file diff --git a/Game/Reward.js b/Game/Reward.js new file mode 100644 index 0000000..adbc64a --- /dev/null +++ b/Game/Reward.js @@ -0,0 +1,134 @@ +const Utility = require("./Utility"); +const chooseWeightedRandom = require("../Utility/chooseWeightedRandom"); + +const uniqueCategories = { + 5: true +}; + +const uniqueItems = {}; + +class Reward { + constructor(information) { + this.distributionType = information.distributionType; + this.baseReward = information.baseReward; + this.damage = information.damage; + }; + + single(amountOfItems, inventory, cb) { + if (this.distributionType !== 2) return; + + const { baseReward } = this; + const allRewardCategories = Object.entries(baseReward); + + const rewardChances = []; + const rewardEntries = []; + const finalRewards = []; + + allRewardCategories.forEach(category => { + const [categoryName, categoryEntries] = category; + + Object.entries(categoryEntries).forEach(subEntry => { + const [id, entry] = subEntry; + + const isUniqueReward = uniqueCategories[categoryName] || uniqueItems[categoryName] && uniqueItems[categoryName][id]; + const isAlreadyAcquired = inventory[categoryName] && inventory[categoryName].hasElement(+id); + if (isUniqueReward && isAlreadyAcquired) return; + + const rewardEntry = { + categoryID: +categoryName, + id: +id, + amount: entry.amountMaximum ? Utility.RandomIntegerBetween(entry.amountMinimum, entry.amountMaximum) : entry.amountMinimum, + index: rewardEntries.length + }; + + if (entry.chance === 100) finalRewards.push(rewardEntry); + else { + rewardChances.push(entry.chance); + rewardEntries.push(rewardEntry); + }; + }); + }); + + const batchedRewards = {}; + if (rewardEntries.length) { + for (let i = 0; i < amountOfItems; i++) { + const chosenItem = chooseWeightedRandom(rewardEntries, rewardChances); + + const batchedEntryIndex = batchedRewards[`${chosenItem.categoryID}-${chosenItem.id}`]; + if (batchedEntryIndex !== undefined) { + finalRewards[batchedEntryIndex].amount += chosenItem.amount; + } else { + batchedRewards[`${chosenItem.categoryID}-${chosenItem.id}`] = finalRewards.length; + finalRewards.push(Object.assign({}, chosenItem)); + + const isUniqueReward = uniqueCategories[chosenItem.categoryID] || uniqueItems[chosenItem.categoryID] && uniqueItems[chosenItem.categoryID][chosenItem.id]; + if (isUniqueReward) rewardChances[chosenItem.index] = 0; + }; + }; + }; + + cb(finalRewards); + } + + multiple(data, cb) { + if (this.distributionType !== 1) return; + + const { baseReward } = this; + const { playerInventory, damage, maximum, lastShotAuthorID } = data; + const allRewardCategories = Object.keys(baseReward); + const totalParticipants = Object.keys(damage); + + totalParticipants.forEach(playerID => { + playerID = parseInt(playerID); + + const rewards = []; + + allRewardCategories.forEach(category => { + category = parseInt(category); + + const rew = baseReward[category]; + + Object.keys(rew).forEach(id => { + id = parseInt(id); + + const isUniqueReward = uniqueCategories[category] || uniqueItems[category] && uniqueItems[category][id]; + const isAlreadyAcquired = playerInventory[category] && playerInventory[category].hasElement(id); + if (isUniqueReward && isAlreadyAcquired) return; + + if (rew[id].lastShot && playerID !== lastShotAuthorID) return; + if (rew[id].chance < Math.random() * 100) return; + + const baseRewardAmount = rew[id].amountMaximum ? Utility.RandomIntegerBetween(rew[id].amountMinimum, rew[id].amountMaximum) : rew[id].amountMinimum; + const amountOfReward = rew[id].lastShot ? baseRewardAmount : Math.round(baseRewardAmount / 100 * (damage[playerID] / maximum * 100)); + if (amountOfReward <= 0) return; + + rewards.push({ + categoryID: category, + id, + amount: amountOfReward + }); + }); + }); + + cb({ id: playerID }, rewards); + }); + } + + dynamic(cb) { + if (this.distributionType !== 3) return; + + for (let i = 0, length = this.baseReward.length; i < length; i++) { + const reward = this.baseReward[i]; + + const baseRewardAmount = reward.amountMaximum ? Utility.RandomIntegerBetween(reward.amountMinimum, reward.amountMaximum) : reward.amountMinimum; + + cb([{ + categoryID: reward.categoryID, + id: reward.id, + amount: baseRewardAmount + }]); + }; + } +}; + +module.exports = Reward; \ No newline at end of file diff --git a/Game/Session.js b/Game/Session.js new file mode 100644 index 0000000..3c626e7 --- /dev/null +++ b/Game/Session.js @@ -0,0 +1,15 @@ +class SessionManager { + constructor() { + this.startAt = Date.now(); + this.endedAt = null; + this.currency = [0, 0, 0, 0] + this.kills = [0, 0, 0, 0, 0]; + this.experience = 0; + this.sunkCount = 0; + this.packets = {}; + this.ammunition = {}; + this.harpoon = {}; + }; +}; + +module.exports = SessionManager; \ No newline at end of file diff --git a/Game/Tower.js b/Game/Tower.js new file mode 100644 index 0000000..7139921 --- /dev/null +++ b/Game/Tower.js @@ -0,0 +1,552 @@ +const Entity = require("./Entity"); +const Utility = require("./Utility"); + +const OVERTIME_HEALTH_EFFECTS = require("../Data/overtime"); +const GUILD_TOWER_SCHEME = require("../Data/schemeTower"); + +const constants = require("../Data/constants"); +const { EVENTS, TIMING_EVENTS, TYPES, MAXIMUM } = constants; + +const { ENTITY_TYPE_TOWER, ENTITY_TYPE_PLAYER } = TYPES; +const { EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_CANNONS_USED, EVENT_ATTACK_BAR, EVENT_HEALTH_CHANGED, EVENT_HEALTH_CHANGED_AUTHORLESS, EVENT_GUILD_ISLAND_CAPTURE } = EVENTS; +const { TIMING_EVENT_RESPAWN, TIMING_EVENT_REPAIR } = TIMING_EVENTS; +const { MAX_SAFE_INTEGER } = MAXIMUM; + +const EVENTS_REMOVE_DEATH_TOWER = [9, 11, 12, 13, 17]; + +class Tower extends Entity { + constructor(info) { + super({ + type: ENTITY_TYPE_TOWER, + id: info.id, + map: info.map, + isAlive: info.currentHitpoint !== 0, + position: info.position + }); + + if (!info.island) return; + + this.order = info.order; + + this.behaviour = { + attackIfAttacked: true, + isAggressive: true, + stopAttackIfAbandoned: false, + selectTargetType: 1 + }; + + this.hp = { + maximum: info.maximumHitpoint, + current: info.currentHitpoint + }; + + this.island = info.island; + + this.island.addTower(this); + + this.entityTypeID = info.entityTypeID; + + this.attackers = []; + this.totalDamageReceived = 0; + this.damageReceived = {}; + + this.activeEffects = {}; + this.activeOvertimeHealthEffects = {}; + + this.dieCallback = info.dieCallback; + this.attackCallback = info.attackCallback; + + this.map.towers.set(this.id, this); + setTimeout(() => { + // this.repair(); + }, 15000); + }; + + trackOvertimeHealthEffect(id) { + const effect = this.activeOvertimeHealthEffects[id]; + if (effect) effect.count++; + else this.activeOvertimeHealthEffects[id] = { count: 0 }; + + return effect ? effect.count : 0; + } + + overtimeDecreaseCurrentHitpoint(event) { + if (!event) return; + + const { id, authorTypeID, authorID, authorGuildID, damage, iid } = event; + const { amount, ticks, time } = OVERTIME_HEALTH_EFFECTS[id]; + + const decrease = this.decreaseCurrentHitpoint(damage * amount, { + authorTypeID, + authorID, + authorGuildID + }); + + if (!decrease) return; + + const currentCount = this.trackOvertimeHealthEffect(id); + if (currentCount < ticks && decrease.currentHitpoint) this.setIndependentEvent(iid, time, event); + else delete this.activeOvertimeHealthEffects[id]; + + const data = { + targetTypeID: this.typeID, + targetID: this.id, + amount: decrease.normalizedAmount, + type: 0, + isCritical: false + }; + + this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, data, this.id); + } + + initializeRepair() { + if (!this.isAlive) return; + if (this.isAtMaximumHitpoints()) return; + + this.repair(); + } + + repair(doneIndex) { + const { id } = this; + + this.lastRepairTickTimestamp = performance.now(); + + const healthUpdate = this.increaseCurrentHitpoint(Math.floor(Math.random() * 2000)); + if (!healthUpdate.normalizedAmount) return console.info("aaa", healthUpdate); + + const eventRepair = { + targetTypeID: ENTITY_TYPE_TOWER, + targetID: id, + amount: healthUpdate.normalizedAmount, + type: 1, + isCritical: false, + }; + + this.emit(EVENT_HEALTH_CHANGED_AUTHORLESS, eventRepair, this.id); + + if (healthUpdate.currentHitpoint >= this.hp.maximum) return this.cancelRepair(); + + this.setReusableSingleEvent(TIMING_EVENT_REPAIR, 1000, doneIndex); + + return true; + } + + cancelRepair() { + if (!this.isRepairing()) return; + + this.cancelEvent(TIMING_EVENT_REPAIR); + } + + increaseCurrentHitpoint(amount) { + amount = Math.abs(amount); + + const normalizedAmount = this.hp.current + amount <= this.hp.maximum ? amount : this.hp.maximum - this.hp.current; + this.hp.current += normalizedAmount; + + const eventGuildIslandTowerHealthChanged = { + id: this.id, + entityTypeID: this.entityTypeID, + guildIslandID: this.island.id, + currentHitpoint: this.hp.current + }; + + this.emit("guildDataIslandTowerHealthChanged", eventGuildIslandTowerHealthChanged, this.id); + + return { + normalizedAmount, + currentHitpoint: this.hp.current + }; + } + + decreaseCurrentHitpoint(amount, author) { + if (this.hp.current === 0) return console.info("couldn't decrease tower"); + + const { authorID, authorGuildID } = author; + amount = Math.abs(amount); + + const damage = Math.min(this.hp.current, amount); + + this.island.updateReceivedDamage(authorGuildID, damage); + + if (this.damageReceived[authorID]) this.damageReceived[authorID] += damage; + else this.damageReceived[authorID] = damage; + + this.totalDamageReceived += damage; + this.hp.current -= damage; + + this.attackCallback({ + typeID: this.typeID, + entityTypeID: this.entityTypeID, + authorID, + damage + }); + + const eventGuildIslandDamage = { + guildIslandID: this.island.id, + guildID: authorGuildID, + damage + }; + + const eventGuildIslandTowerHealthChanged = { + id: this.id, + entityTypeID: this.entityTypeID, + guildIslandID: this.island.id, + currentHitpoint: this.hp.current + }; + + this.emit("guildDataIslandTowerHealthChanged", eventGuildIslandTowerHealthChanged, this.id); + + this.emit("_", eventGuildIslandDamage); + + const isOverSafeLimit = this.totalDamageReceived >= MAX_SAFE_INTEGER; + if (isOverSafeLimit) this.totalDamageReceived = MAX_SAFE_INTEGER; + + if (this.hp.current === 0 || isOverSafeLimit) { + const death = { + typeID: this.typeID, + lastShotTypeID: 1, + lastShotID: author.authorID, + targetID: this.id + }; + + this.die(death); + // this.setSingleEvent(11, 0, die); + }; + + return { + normalizedAmount: damage, + currentHitpoint: this.hp.current + }; + } + + initializeAttackWithCannon(selectedTarget) { + //const cannonsStatistics = this.getCannonsStatistics(); + const isInShootingRange = Utility.IsInDistance(this.position, selectedTarget.position, 600); + + //if (UNATTACKABLE_ENTITY_TYPES.includes(selectedTarget.typeID)) return + if (!isInShootingRange) return + //if (this.isRepairing()) return + // if (selectedTarget.typeID === ENTITY_TYPE_PLAYER && this.map.type === MAP_PVE) return + + //const selected = this.inventory.getSelectedAmmunition(); + //if (!selected || selected.amount <= 0) return + //if (ammunitions[this.inventory.selectedAmmunition].damage < 0 && selectedTarget.isAtMaximumHitpoints()) return + + if (this.target && this.target.id === selectedTarget.id) return + if (selectedTarget.id === this.id) return + if (!selectedTarget.isAlive) return + if (!this.isAlive) return + //if (selectedTarget.typeID === ENTITY_TYPE_PLAYER && selectedTarget.guild.id === this.island.guild.id) return + //if (!this.inventory.getCannons().some(c => c.amountEquipped)) return + + if (this.target && this.target.id !== selectedTarget.id) this.abortAttack(); + + const attackInformation = { + attackerEntry: this.id + }; + + selectedTarget.attackers.push(attackInformation); + + this.target = selectedTarget; + this.searchedTarget = null; + + /*if (selectedTarget.typeID === ENTITY_TYPE_PLAYER) { + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + }; + + selectedTarget.emit(EVENT_ATTACK_STARTED, attacking, this.id); + };*/ + + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + isTarget: false, + source: 1 + }; + + this.emit(EVENT_ATTACK_STARTED, attacking, this.id); + selectedTarget.emit(EVENT_ATTACK_BAR, attacking, selectedTarget.id); + + this.setReusableSingleEvent(97, 0, { id: 1 }); + //this.cannonTypesInUse = cannonsStatistics.distinctCannonTypes; + //this.maximumCannonRange = cannonsStatistics.maximumCannonRange; + + //cannonsStatistics.distinctCannonTypes.forEach(c => this.setReusableSingleEvent(c.eventID, 0, { id: c.id })); + } + + attackWithCannon(event) { + const { target } = this; + if (!target) return; + + const recheckShootingRange = Utility.IsInDistance(this.position, target.position, 600); + if (!recheckShootingRange) { + this.abortAttack(); + + return true; + }; + + this.lastShotTickTimestamp = performance.now(); + + const attackingAnimation = { + authorSessionID: this.sessionID, + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + entityTypeID: target.entityTypeID + }; + + const damage = //this.calculateCannonDamage(1); +{ value: 2000 } + const attacking = { + authorID: this.id, + authorTypeID: this.typeID, + targetID: target.id, + targetTypeID: target.typeID, + amount: 0, + isCritical: false, + type: damage.value <= 0 + }; + + const healthUpdate = damage.value >= 0 ? + target.decreaseCurrentHitpoint(damage.value, { + authorTypeID: this.typeID, + authorID: this.id, + authorGuildID: this.guild ? this.guild.id : 0 + }) : target.increaseCurrentHitpoint(damage.value); + + attacking.amount = healthUpdate.normalizedAmount; + attacking.isCritical = damage.critical; + + const flushBuffer = this.map.eventBuffer[this.id]; + if (flushBuffer) flushBuffer.data[0].amount += attacking.amount; + else { + const data = [attacking, attackingAnimation]; + this.map.eventBuffer[this.id] = { + data, + author: target.id, + target: this.id + }; + + this.map.eventAuthors.push(this.id); + }; + + this.emit(EVENT_CANNONS_USED, { + amount: damage.usedCannonCount, + type: 0 + }, this.id); + + if (healthUpdate.currentHitpoint <= 0) return; + + this.setReusableSingleEvent(97, 2000, event); + + return true; + } + + removeTarget() { + if (!this.target) return; + + this.target.attackers = this.target.attackers.filter(data => data.attackerEntry !== this.id); + this.target = null; + } + + wipeAttack() { + const { target } = this; + if (!target) return; + + this.cancelEvent(97); + + this.target = null; + } + + abortAttack() { + if (!this.isAttacking()) return; + if (!this.target) return; + + this.cancelEvent(97); + + const target = { + id: this.target.id, + typeID: this.target.typeID + }; + + this.removeTarget(); + + return target; + } + + die(eventDeath) { + super.die(); + console.trace("tower death") + this.hp.current = 0; + + this.attackers.forEach(data => { + const attackerEntity = this.map.getEntityFromSubgroup(data.attackerEntry); + attackerEntity.wipeAttack(); + }); + + this.attackers.length = 0; + + const eventGuildIslandTowerDestroyed = { + id: this.id, + entityTypeID: this.entityTypeID, + guildIslandID: this.island.id + }; + + this.emit(EVENT_DIE, [eventDeath, eventGuildIslandTowerDestroyed], this.id); + + const admiralProperties = { + typeID: this.typeID, + entityTypeID: this.entityTypeID, + parentEntityTypeID: this.parentEntityTypeID, + id: this.id, + isAdmiral: this.isAdmiral, + authorID: eventDeath.lastShotID + }; + + this.abortAttack(); + + this.dieCallback(admiralProperties); + + EVENTS_REMOVE_DEATH_TOWER.forEach(e => this.removeEvent(e)); + + const updatedOwnership = this.island.updateOwnership(); + if (updatedOwnership) { + const { guildOld, guildNew, topicOld, topicNew } = updatedOwnership; + + const eventGuildIslandCaptured = { + topicOld: topicOld, + topicNew: topicNew, + packet: { + tag: guildNew.tag + }, + database: { + newID: guildNew.id, + oldID: guildOld.id + } + }; + + this.emit(EVENT_GUILD_ISLAND_CAPTURE, eventGuildIslandCaptured, this.id); + }; + + this.damageReceived = {}; + } + + seekEnemy(searchedTarget) { + if (!searchedTarget) return; + if (!this.isAlive) return; + if (this.isAttacking()) return; + if (!this.behaviour.isAggressive) return; + if (this.island.guild.id && this.island.guild.id === searchedTarget.guild.id) return; + + if (!this.searchedTarget) return this.searchedTarget = searchedTarget; + + let isBehaviourSatisfied = false; + + switch (this.behaviour.selectTargetType) { + case 1: + isBehaviourSatisfied = searchedTarget.hp.current - this.searchedTarget.hp.current <= 0; + break; + case 2: + isBehaviourSatisfied = this.searchedTarget.hp.current - searchedTarget.hp.current <= 0; + break; + case 3: + isBehaviourSatisfied = searchedTarget.hp.maximum - this.searchedTarget.hp.maximum < 0; + break; + case 4: + isBehaviourSatisfied = this.searchedTarget.hp.maximum - searchedTarget.hp.maximum < 0; + break; + case 5: + isBehaviourSatisfied = searchedTarget.speed - this.searchedTarget.speed < 0; + break; + case 6: + isBehaviourSatisfied = this.searchedTarget.speed - searchedTarget.speed < 0; + break; + case 7: + isBehaviourSatisfied = searchedTarget.sight - this.searchedTarget.sight < 0; + break; + case 8: + isBehaviourSatisfied = this.searchedTarget.sight - searchedTarget.sight < 0; + break; + }; + + const isInShootingRange = Utility.IsInDistance(this.position, searchedTarget.position, 600); + const isSelectedInShootingRange = Utility.IsInDistance(this.position, this.searchedTarget.position, 600); + if ((isBehaviourSatisfied && isInShootingRange) || !isSelectedInShootingRange) return this.searchedTarget = searchedTarget; + } + + respawn() { + if (this.isAlive) return; + + this.entityTypeID = 1; + + const baseScheme = GUILD_TOWER_SCHEME[this.entityTypeID]; + if (!baseScheme) return; + + this.hp.maximum = baseScheme.maximumHitpoints; + this.hp.current = this.hp.maximum; + + super.respawn({ + position: this.position + }); + + const eventTowerBuild = { + id: this.id, + entityTypeID: this.entityTypeID, + guildIslandID: this.island.id + }; + + this.emit("guildDataIslandTowerBuilt", eventTowerBuild, this.id); + console.info("guildDataIslandTowerBuilt") + + this.markActive(); + } + + upgrade(entityTypeID) { + if (!this.isAlive) { + this.respawn(); + + return true; + }; + + const baseScheme = GUILD_TOWER_SCHEME[entityTypeID]; + if (!baseScheme) return false; + + const isNextLevelTarget = entityTypeID - this.entityTypeID === 1; + if (isNextLevelTarget) { + this.hp.maximum = baseScheme.maximumHitpoints; + this.hp.current = baseScheme.maximumHitpoints; + this.entityTypeID = entityTypeID; + } else return false; + + const eventTowerBuild = { + id: this.id, + entityTypeID: this.entityTypeID, + guildIslandID: this.island.id + }; + + this.emit("guildDataIslandTowerBuilt", eventTowerBuild, this.id); + + return true; + } + + getProperties() { + const { position, id, hp, type, typeID, isAdmiral, entityTypeID, island, order } = this; + return { position, id, hp, type, typeID, isAdmiral, entityTypeID, guildTag: island.guild ? island.guild.tag : null, order }; + } + + isAtMaximumHitpoints() { + return this.hp.current === this.hp.maximum; + } + + isRepairing() { + const event = this.events[TIMING_EVENT_REPAIR]; + return event && !event[event.length - 1].isCancelled ? true : false; + } + + isAttacking = () => this.target ? true : false; +}; + +module.exports = Tower; diff --git a/Game/Utility.js b/Game/Utility.js new file mode 100644 index 0000000..099a6bb --- /dev/null +++ b/Game/Utility.js @@ -0,0 +1,11 @@ +class Utility { + static RandomIntegerBetween(minimum, maximum) { + return Math.floor(Math.random() * (maximum - minimum + 1) + minimum); + } + + static IsInDistance(basePoint, targetPoint, range) { + return (basePoint.x - targetPoint.x) ** 2 + (basePoint.y - targetPoint.y) ** 2 <= range ** 2; + } +}; + +module.exports = Utility; \ No newline at end of file diff --git a/Handlers/Collectable.js b/Handlers/Collectable.js new file mode 100644 index 0000000..95bb757 --- /dev/null +++ b/Handlers/Collectable.js @@ -0,0 +1,54 @@ +const EntityNewPacket = require("../Packets/EntityNew"); +const EntityDiePacket = require("../Packets/EntityDie"); + +const constants = require("../Data/constants"); +const { TYPES, EVENTS } = constants; +const { ENTITY_TYPE_COLLECTABLE } = TYPES; +const { EVENT_DIE } = EVENTS; + +const queries = require("../Data/queries"); +const { QUERY_FETCH_COLLECTABLE_SPAWN } = queries; + +const Collectable = require("../Game/Collectable"); + +module.exports = async function (map, globalDieCallback, globalRewardCallback) { + const databaseSpawn = await execute(QUERY_FETCH_COLLECTABLE_SPAWN, [map.id, ENTITY_TYPE_COLLECTABLE]); + if (databaseSpawn.error) return process.exit(); + + const spawns = databaseSpawn.data.reduce((spawns, spawn) => { + if (!spawns[spawn.id]) { + spawns[spawn.id] = { + id: spawn.id, + amount: spawn.amount, + respawnTime: spawn.respawnTime, + reward: [] + }; + }; + + if (spawn.categoryId) spawns[spawn.id].reward.push({ + categoryId: spawn.categoryId, + itemId: spawn.itemId, + min: spawn.min, + max: spawn.max, + chance: spawn.chance, + isLastShot: spawn.isLastShot + }); + + return spawns; + }, {}); + + const convertedSpawn = Object.values(spawns); + map.registerCollectable(convertedSpawn, collectablesData => { + collectablesData.forEach(data => { + const { collectable, collectableSpawnProperties } = data; + + const packet = EntityNewPacket.collectable(collectableSpawnProperties); + map.registerNetworkPacket(collectable.id, packet); + + collectable.on(EVENT_DIE, function (data, author) { + const packet = EntityDiePacket(data); + map.registerNetworkPacket(author, packet, [1]); + }); + }); + }, globalDieCallback, globalRewardCallback, Collectable); +}; \ No newline at end of file diff --git a/Handlers/Map.js b/Handlers/Map.js new file mode 100644 index 0000000..69b86cf --- /dev/null +++ b/Handlers/Map.js @@ -0,0 +1,26 @@ +const AdmiralSpawnedPacket = require("../Packets/AdmiralSpawned"); + +const Game = require("../Game/Game"); +const { maps } = Game; + +const nonPlayerCharacterHandler = require("./NPC"); +const monsterHandler = require("./Monster"); +const collectableHandler = require("./Collectable"); +const towerHandler = require("./Tower"); + +const die = require("../Callbacks/die"); +const reward = require("../Callbacks/reward"); +const attack = require("../Callbacks/attack"); + +maps.forEach(map => { + nonPlayerCharacterHandler(map, die, reward, attack); + collectableHandler(map, die, reward); + monsterHandler(map, die, reward, attack); + towerHandler(map, die, reward, attack); + + map.on("admiralSpawnNotification", function (data, emittingTo) { + const packet = AdmiralSpawnedPacket(data); + + emittingTo.forEach(mapID => app.publish(mapID.toString(), packet, true, true)); + }); +}); \ No newline at end of file diff --git a/Handlers/Monster.js b/Handlers/Monster.js new file mode 100644 index 0000000..9f3d84a --- /dev/null +++ b/Handlers/Monster.js @@ -0,0 +1,72 @@ +const EntityNewPacket = require("../Packets/EntityNew"); +const AttackInformationPacket = require("../Packets/AttackInformation"); +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const EntityDiePacket = require("../Packets/EntityDie"); + +const constants = require("../Data/constants"); +const { TYPES, EVENTS } = constants; +const { ENTITY_TYPE_MONSTER } = TYPES; +const { EVENT_ATTACK_STARTED, EVENT_DIE, EVENT_ATTACK_BAR } = EVENTS; + +const queries = require("../Data/queries"); +const { QUERY_FETCH_MONSTER_SPAWN } = queries; + +const Monster = require("../Game/Monster"); + +module.exports = async function (map, globalDieCallback, globalRewardCallback, globalAttackCallback) { + const databaseSpawn = await execute(QUERY_FETCH_MONSTER_SPAWN, [map.id, ENTITY_TYPE_MONSTER]); + if (databaseSpawn.error) return process.exit(); + + const spawns = databaseSpawn.data.reduce((spawns, spawn) => { + if (!spawns[spawn.id]) { + spawns[spawn.id] = { + id: spawn.id, + parentEntityTypeId: spawn.parentEntityTypeId, + name: spawn.name, + minHealth: spawn.minHealth, + maxHealth: spawn.maxHealth, + amount: spawn.amount, + respawnTime: spawn.respawnTime, + rewardEntries: [] + }; + }; + + if (spawn.categoryId) spawns[spawn.id].rewardEntries.push({ + categoryId: spawn.categoryId, + itemId: spawn.itemId, + min: spawn.min, + max: spawn.max, + chance: spawn.chance, + isLastShot: spawn.isLastShot + }); + + return spawns; + }, {}); + + const convertedSpawn = Object.values(spawns); + map.setupMonsterSpawn(convertedSpawn, + function monsterNetworkListeners (data) { + const { monster, monsterSpawnProperties } = data; + + const packet = EntityNewPacket.monster(monsterSpawnProperties); + map.registerNetworkPacket(monster.id, packet); + + monster.on(EVENT_ATTACK_STARTED, function (data, author) { + const packet = AttackInformationPacket(data); + + this.map.registerIndividualNetworkPacket(author, packet); + }); + + monster.on(EVENT_ATTACK_BAR, function (data, author) { + const packet = AttackerInformationPacket(data); + + this.map.registerIndividualNetworkPacket(author, packet); + }); + + monster.on(EVENT_DIE, function (data, author) { + const packet = EntityDiePacket(data); + + this.map.registerNetworkPacket(author, packet, [1]); + }); + }, globalDieCallback, globalRewardCallback, globalAttackCallback, Monster); +}; \ No newline at end of file diff --git a/Handlers/NPC.js b/Handlers/NPC.js new file mode 100644 index 0000000..41f4c98 --- /dev/null +++ b/Handlers/NPC.js @@ -0,0 +1,78 @@ +const getFixedDate = require("../Utility/getFixedDate"); + +const Game = require("../Game/Game"); +const { maps } = Game; + +const constants = require("../Data/constants"); +const { TYPES } = constants; +const { ENTITY_TYPE_NPC } = TYPES; + +const queries = require("../Data/queries"); +const { QUERY_FETCH_NPC_SPAWN } = queries; + +const NPCWorker = global.NPCWorker; + +const NPC = require("../Game/NPC"); + +const nonPlayerCharacterNetworkListeners = require("./NPCTest"); + +const messages = { + 1: function (data) { + const { path, id, mapID } = data; + const map = maps[mapID - 1]; + const npc = map.npcs.get(id); + if (!npc) return; + + npc.startMovement(path); + } +}; + +NPCWorker.on("message", data => { + const { messageId } = data; + const event = messages[messageId]; + if (event) event(data); +}); + +module.exports = async function (map, globalDieCallback, globalRewardCallback, globalAttackCallback) { + const databaseSpawn = await execute(QUERY_FETCH_NPC_SPAWN, [map.id, ENTITY_TYPE_NPC]); + if (databaseSpawn.error) return process.exit(); + + const spawns = databaseSpawn.data.reduce((spawns, spawn) => { + if (!spawns[spawn.id]) { + spawns[spawn.id] = { + id: spawn.id, + parentEntityTypeId: spawn.parentEntityTypeId, + name: spawn.name, + speed: spawn.speed, + minHealth: spawn.minHealth, + maxHealth: spawn.maxHealth, + amount: spawn.amount, + respawnTime: spawn.respawnTime, + rewardEntries: [] + }; + }; + + if (spawn.categoryId) spawns[spawn.id].rewardEntries.push({ + categoryId: spawn.categoryId, + itemId: spawn.itemId, + min: spawn.min, + max: spawn.max, + chance: spawn.chance, + isLastShot: spawn.isLastShot + }); + + return spawns; + }, {}); + + const convertedSpawn = Object.values(spawns); + map.setupNonPlayerCharacterSpawn(convertedSpawn, nonPlayerCharacterNetworkListeners, globalDieCallback, globalRewardCallback, globalAttackCallback, NPC); + + const event = { + fn: map.spawnEntityGroups.bind(map), + arguments: [nonPlayerCharacterNetworkListeners, NPC, globalAttackCallback, globalDieCallback], + next: Game.eventsFixed.add.bind(Game.eventsFixed) + }; + + const nextRunDate = getFixedDate(30); + Game.eventsFixed.add(nextRunDate, event); +}; \ No newline at end of file diff --git a/Handlers/NPCTest.js b/Handlers/NPCTest.js new file mode 100644 index 0000000..3a988f2 --- /dev/null +++ b/Handlers/NPCTest.js @@ -0,0 +1,93 @@ +const EntityNewPacket = require("../Packets/EntityNew"); +const AttackInformationPacket = require("../Packets/AttackInformation"); +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const AttackingPacket = require("../Packets/Attacking"); +const AttackAbortedPacket = require("../Packets/AttackAborted"); +const EntityDiePacket = require("../Packets/EntityDie"); +const EffectAddPacket = require("../Packets/EffectAdd"); +const EntitySpeedChangedPacket = require("../Packets/EntitySpeedChanged"); +const HealthChangedPacket = require("../Packets/HealthChanged"); +const HealthChangedAuthorlessPacket = require("../Packets/HealthChangedAuthorless"); +const MiniMapEntryMovePacket = require("../Packets/MiniMapEntryMove"); +const MiniMapEntryRemovePacket = require("../Packets/MiniMapEntryRemove"); + +const NewDestinationNodePacket = require("../Packets/NewDestinationNode"); + +const constants = require("../Data/constants"); +const { EVENTS } = constants; +const { EVENT_DESTINATION_REACHED, EVENT_ATTACK_STARTED, EVENT_HEALTH_CHANGED, EVENT_ATTACKING, EVENT_ATTACK_ABORTED_BY_USER, EVENT_DIE, EVENT_SPEED_UPDATE, EVENT_DESTINATION_NEW, EVENT_EFFECT_ADD, EVENT_ATTACK_BAR } = EVENTS; + +module.exports = function nonPlayerCharacterNetworkListeners(map, data) { + const { npc, npcSpawnProperties } = data; + + const packet = EntityNewPacket.npc(npcSpawnProperties); + map.registerNetworkPacket(npc.id, packet.buffer ? packet.buffer : packet); + + npc.on(EVENT_DESTINATION_REACHED, function (data) { + NPCWorker.postMessage({ + position: npc.position, + mapID: map.id, + id: npc.id, + preSelectedDestinationNode: data.destination, + predefinedArea: data.predefinedArea + }); + }); + + npc.on(EVENT_ATTACK_STARTED, function (data, author) { + const packet = AttackInformationPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); + }); + + npc.on(EVENT_ATTACK_BAR, function (data, author) { + const packet = AttackerInformationPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); + }); + + npc.on(EVENT_HEALTH_CHANGED, function (data, author) { + const packet = HealthChangedAuthorlessPacket(data); + this.map.registerNetworkPacket(this.id, packet); + }); + + npc.on(EVENT_ATTACKING, function (data, author) { + const _packet = AttackingPacket(data); + const packet = HealthChangedPacket(data); + this.map.registerNetworkPacket(this.id, [_packet, packet], [1, 1]); + }); + + npc.on(EVENT_ATTACK_ABORTED_BY_USER, function (data, author) { + const packet = AttackAbortedPacket(data); + this.map.registerNetworkPacket(this.id, packet); + }); + + npc.on(EVENT_DIE, function (data, author) { + const packet = EntityDiePacket(data); + this.map.registerNetworkPacket(this.id, packet, [1]); + }); + + npc.on(EVENT_SPEED_UPDATE, function (data, author) { + const packet = EntitySpeedChangedPacket(data); + this.map.registerNetworkPacket(this.id, packet); + }); + + npc.on(EVENT_DESTINATION_NEW, function (data, author) { + const packet = NewDestinationNodePacket(data); + this.map.registerNetworkPacket(this.id, packet, [59]); + }); + + npc.on(EVENT_EFFECT_ADD, function (data, author) { + const packet = EffectAddPacket(data); + this.map.registerNetworkPacket(this.id, packet); + }); + + npc.on("miniMapDotRemoveTest", function (data, author) { + const packet = MiniMapEntryRemovePacket(data); + this.map.registerGlobalNetworkPacket(this.id, packet); + }); + + npc.on("miniMapMove", function (data, author) { + const packet = MiniMapEntryMovePacket(data); + this.map.registerGlobalNetworkPacket(this.id, packet); + }); + + npc.determineMovementState(); +}; \ No newline at end of file diff --git a/Handlers/Player.js b/Handlers/Player.js new file mode 100644 index 0000000..0279fa5 --- /dev/null +++ b/Handlers/Player.js @@ -0,0 +1,364 @@ +const MainPlayerPacket = require("../Packets/MainPlayer"); +const QuestRemovalPacket = require("../Packets/QuestRemoval"); +const EntityOldPacket = require("../Packets/EntityOld"); +const EntityNewPacket = require("../Packets/EntityNew"); +const AmmunitionUsedPacket = require("../Packets/AmmunitionUsed"); +const LogoutCancelledPacket = require("../Packets/LogoutCancelled"); +const PropertyChangedPacket = require("../Packets/PropertyChanged") +const AttackInformationPacket = require("../Packets/AttackInformation"); +const AttackingPacket = require("../Packets/Attacking"); +const AttackAbortedPacket = require("../Packets/AttackAborted"); +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const EntityDiePacket = require("../Packets/EntityDie"); +const RespawnConfirmationPacket = require("../Packets/RespawnConfirmation"); +const EffectAddPacket = require("../Packets/EffectAdd"); +const EntitySpeedChangedPacket = require("../Packets/EntitySpeedChanged"); +const MapChangeStatePacket = require("../Packets/MapChangeState"); +const MapChangedPacket = require("../Packets/MapChanged"); +const LevelUpPacket = require("../Packets/LevelUp"); +const LevelIsLowPacket = require("../Packets/LevelIsLow"); +const HealthChangedAuthorlessPacket = require("../Packets/HealthChangedAuthorless"); +const HealthChangedPacket = require("../Packets/HealthChanged"); +const ItemUsedPacket = require("../Packets/ItemUsed"); +const NewDestinationNodePacket = require("../Packets/NewDestinationNode"); +const GuildMemberNewPacket = require("../Packets/GuildMemberNew"); +const GuildDataMemberChangedPacket = require("../Packets/GuildDataMemberChanged"); + +const quests = require("../Data/quests"); +const constants = require("../Data/constants"); + +const Game = require("../Game/Game"); + +const { players } = Game; + +const { TYPES } = constants; +const { ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_MONSTER, ENTITY_TYPE_TOWER, ENTITY_TYPE_COLLECTABLE } = TYPES; + +const [playerEconomy] = require("../Data/batchSave"); + +// just to add "lag" +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +global.PlayerWorker.on("message", data => { + const player = players[data.id]; + if (!player) return; + if (player.map.id !== data.mapId) return; + if (player.movementRequestTracker !== data.movementRequestTracker) return; + + player.startingPosition = data.startingPosition; + + const { path } = data; + + if (!player.isMoving) player.initializeMovement(path, data); + else player.emit("scheduledPathReady", path, data); +}); + +module.exports.scheduledPathReady = function (path, data) { + if (path.length) { + this.data = data; + this.scheduledPath = path; + this.isNewPathReady = true; + } else this.isLastNodeFinished = true; + + if (this.isNewPathReady && this.isLastNodeFinished) { + this.isNewPathReady = false; + this.isLastNodeFinished = false; + + this.initializeMovement(this.scheduledPath, this.data); + this.scheduledPath = []; + }; +}; + +module.exports.updateVisibility = function (entityProps) { + const { type, data } = entityProps; + let packet = null; + + if (type) { + switch (data.typeID) { + case ENTITY_TYPE_PLAYER: packet = EntityNewPacket.player(data); break; + case ENTITY_TYPE_NPC: packet = EntityNewPacket.npc(data); break; + case ENTITY_TYPE_MONSTER: packet = EntityNewPacket.monster(data); break; + case ENTITY_TYPE_TOWER: packet = EntityNewPacket.tower(data); break; + case ENTITY_TYPE_COLLECTABLE: packet = EntityNewPacket.collectable(data); break; + }; + } else packet = EntityOldPacket(data); + + this.map.registerIndividualNetworkPacket(this.id, packet.buffer ? packet.buffer : packet, [packet.metadata]); +}; + +module.exports.disconnect = async function (data) { + const { map, position, hp, inventory, textureID } = this; + const playerID = this.id; + + const oldEntityID = { + typeID: this.typeID, + id: playerID + }; + + const packet = EntityOldPacket(oldEntityID); + this.map.registerNetworkPacket(this.id, packet, [1]); + + if (this.guild.id) { + const guildDataMemberChanged = { + id: playerID, + isOnline: false + }; + + const packet = GuildDataMemberChangedPacket(guildDataMemberChanged); + + const topic = Game.getTopic(this.guild.topic); + topic.push(packet); + }; + + this.isUnderDisconnect = true; + + if (!this.hasUnexpectedlyLeft) { + socket.sockets[this.sessionID].end(); + delete socket.sockets[this.sessionID]; + console.info("player handler end socket", performance.now()); + }; + + const ammunition = inventory.getAmmunitions(); + const harpoon = inventory.getHarpoons(); + const economy = inventory.getEconomy(); + const items = inventory.getItems(); + const designs = inventory.getDesignIDs(); + const cannons = inventory.getCannons(); + const harpooners = inventory.getHarpooners(); + + const resources = []; + const equipments = []; + + economy.forEach(e => { + playerEconomy.removeItem(`${playerID}-1-${e.id}`); + resources.push([playerID, 1, e.id, e.amount]) + }); + + items.forEach(i => { + playerEconomy.removeItem(`${playerID}-2-${i.id}`); + resources.push([playerID, 2, i.id, i.amount]) + }); + + ammunition.forEach(a => { + playerEconomy.removeItem(`${playerID}-3-${a.id}`); + resources.push([playerID, 3, a.id, a.amount]) + }); + + harpoon.forEach(h => { + playerEconomy.removeItem(`${playerID}-4-${h.id}`); + resources.push([playerID, 4, h.id, h.amount]) + }); + + designs.forEach(id => { + playerEconomy.removeItem(`${playerID}-5-${id}`); + resources.push([playerID, 5, id, 1]) + }); + + this.statusPoints.forEach(s => { + playerEconomy.removeItem(`${playerID}-6-${s.id}`); + resources.push([playerID, 6, s.id, s.amount]) + }); + + cannons.forEach(c => { + playerEconomy.removeItem(`${playerID}-7-${c.id}`); + + resources.push([playerID, 7, c.id, c.amount]); + equipments.push([playerID, 7, c.id, c.amountEquipped]); + }); + + harpooners.forEach(h => { + playerEconomy.removeItem(`${playerID}-8-${h.id}`); + + resources.push([playerID, 8, h.id, h.amount]); + equipments.push([playerID, 8, h.id, h.amountEquipped]) + }); + + // await delay(20000) + Object.entries(this.quests.completedQuestsAmount).forEach(async q => { + const [questID, amount] = q; + const questScheme = quests[questID - 1]; + const isQuestAccepted = this.quests.has(questID); + let questState = 0; + + if (questScheme.maximumComplete && amount >= questScheme.maximumComplete) questState = 2; + else if (!isQuestAccepted) questState = 1; + else questState = 0; + + await execute("INSERT INTO quests (playerID, questID, state, completedAmount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE state = ?, completedAmount = ?", [playerID, questID, questState, amount, questState, amount]); + + if (isQuestAccepted) { + const quest = this.quests.list[questID]; + const questProgression = quest.getProgression(); + questProgression.forEach(async progression => { + const { amount, index, type } = progression; + await execute("INSERT INTO questprogression (playerID, questID, amount, taskIndex, taskType) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?", [playerID, quest.id, amount, index, type, amount]); + }); + }; + }); + + const finishedQuestsID = Object.keys(this.quests.finishedQuests); + if (finishedQuestsID.length) await execute("UPDATE questprogression SET amount = 0 WHERE questID IN (?)", finishedQuestsID); + + await execute("UPDATE playerdata SET mapID = ?, x = ?, y = ?, currentHitpoints = ?, maximumHitpoints = ?, selectedAmmunitionID = ?, selectedHarpoonID = ?, designID = ? WHERE playerID = ?", [map.id, position.x, position.y, hp.current, hp.maximum, inventory.selectedAmmunition, inventory.selectedHarpoon, textureID, playerID]); + + if (resources.length) await query(`INSERT INTO resources (playerID, categoryID, itemID, amount) VALUES ? ON DUPLICATE KEY UPDATE amount = VALUES(amount)`, [resources]); + + const activeStatusEffects = this.activeStatusEffects.length ? this.activeStatusEffects : [0]; + const activeStatusEffectsFormatted = this.activeStatusEffects.map(e => [playerID, e, 1]); + if (this.activeStatusEffects.length) await query(`INSERT INTO status (playerID, statusID, isActive) VALUES ? ON DUPLICATE KEY UPDATE isActive = ?`, [activeStatusEffectsFormatted, 1]); + await query(`UPDATE status SET isActive = ? WHERE statusID NOT IN (?) AND playerID = ?`, [0, activeStatusEffects, playerID]); + + if (equipments.length) await query("INSERT INTO equipments (playerID, categoryID, itemID, amountEquipped) VALUES ? ON DUPLICATE KEY UPDATE amountEquipped = VALUES(amountEquipped)", [equipments]); + + if (this.isUnderDisconnect) { + console.info("player handler disconnect finished", performance.now()); + delete players[playerID]; + } +}; + +module.exports.disconnectCancel = function() { + const packet = LogoutCancelledPacket(); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.healthChangedAuthorless = function (data, author) { + const packet = HealthChangedAuthorlessPacket(data); + this.map.registerNetworkPacket(author, packet); +}; + +module.exports.attackBar = function (data, author) { + const packet = AttackerInformationPacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.attackStarted = function (data, author) { + const packet = AttackInformationPacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.attacking = function (data, author) { + const packet = AttackingPacket(data[1]); + const packett = HealthChangedPacket(data[0]); + this.map.registerNetworkPacket(author, [packet, packett], [1, 1]); +}; + +module.exports.attackAbortedByUser = function (data, author) { + const packet = AttackAbortedPacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.die = function (data, author) { + const packet = EntityDiePacket(data); + this.map.registerNetworkPacket(author, packet, [19]); +}; + +module.exports.propertyChanged = function (data, author) { + const packet = PropertyChangedPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); +}; + +module.exports.ableToJump = function (data) { + const packet = MapChangeStatePacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.unableToJump = function (data) { + const packet = MapChangeStatePacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.mapChanged = function (data, author) { + const selfSwitchPacket = MapChangedPacket(data.newMap); + this.map.registerIndividualNetworkPacket(this.id, selfSwitchPacket); + + const socketInstance = socket.sockets[this.sessionID]; + socketInstance.unsubscribe(data.oldMap.id.toString()); + socketInstance.subscribe(data.newMap.mapID.toString()); +}; + +module.exports.levelUp = function (data) { + if (this.guild.id) { + const guildDataMemberChanged = { + id: this.id, + level: data.level + }; + + const guildDataMemberChangedPacket = GuildDataMemberChangedPacket(guildDataMemberChanged); + + const topic = Game.getTopic(this.guild.topic); + topic.push(guildDataMemberChangedPacket); + } + + const packet = LevelUpPacket(data); + this.map.registerNetworkPacket(this.id, packet); +}; + +module.exports.levelIsLow = function() { + const packet = LevelIsLowPacket(); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.questDone = async function (data) { + await execute("INSERT INTO quests (playerID, questID, state, completedAmount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE state = ?, completedAmount = completedAmount + 1", [this.id, data.questID, 1, 1, 1]); + + const packet = QuestRemovalPacket(data); + this.map.registerIndividualNetworkPacket(this.id, packet); +}; + +module.exports.testOvertimeDamage = function (data, author) { + const packet = HealthChangedPacket(data); + this.map.registerNetworkPacket(author, packet); +}; + +module.exports.itemUsed = function (data, author) { + const packet = ItemUsedPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); +}; + +module.exports.cannonsUsed = function (data, author) { + const packet = AmmunitionUsedPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); +}; + +module.exports.harpoonersUsed = function (data, author) { + const packet = AmmunitionUsedPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); +}; + +module.exports.newDestination = function (data) { + const packet = NewDestinationNodePacket(data); + this.map.registerNetworkPacket(this.id, packet); +}; + +module.exports.effectAdd = function (data) { + const packet = EffectAddPacket(data); + this.map.registerNetworkPacket(this.id, packet); +}; + +module.exports.speedUpdate = function (data) { + const packet = EntitySpeedChangedPacket(data); + this.map.registerNetworkPacket(this.id, packet); +}; + +module.exports.respawn = async function() { + const respawnPacket = RespawnConfirmationPacket(); + this.map.registerIndividualNetworkPacket(this.id, respawnPacket); + + const respawningPlayerProperties = this.getProperties(); + const respawningPlayerPacket = MainPlayerPacket(respawningPlayerProperties); + const packetsToSend = [respawningPlayerPacket]; + + if (this.guild && this.guild.id) { + const guildProperties = { + playerID: this.id, + tag: this.guild.tag + }; + + const guildPacket = GuildMemberNewPacket(guildProperties); + packetsToSend.push(guildPacket); + }; + + this.map.registerIndividualNetworkPacket(this.id, packetsToSend); +}; \ No newline at end of file diff --git a/Handlers/Tower.js b/Handlers/Tower.js new file mode 100644 index 0000000..1052d17 --- /dev/null +++ b/Handlers/Tower.js @@ -0,0 +1,184 @@ +const EntityNewPacket = require("../Packets/EntityNew"); +const EntityDiePacket = require("../Packets/EntityDie"); +const GuildIslandCapturedPacket = require("../Packets/GuildIslandCaptured"); +const AttackInformationPacket = require("../Packets/AttackInformation"); +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const AttackingPacket = require("../Packets/Attacking"); +const HealthChangedPacket = require("../Packets/HealthChanged"); +const HealthChangedAuthorlessPacket = require("../Packets/HealthChangedAuthorless"); +const GuildDataIslandTowerDestroyedPacket = require("../Packets/GuildDataIslandTowerDestroyed"); +const GuildDataIslandTowerHealthChangedPacket = require("../Packets/GuildDataIslandTowerHealthChanged"); +const GuildDataIslandCapturedPacket = require("../Packets/GuildDataIslandCaptured"); +const GuildDataIslandTowerBuiltPacket = require("../Packets/GuildDataIslandTowerBuilt"); +const WorldMapGuildEntryPacket = require("../Packets/WorldMapGuildEntry"); +const GuildDataIslandPacket = require("../Packets/GuildDataIsland"); +const GuildDataIslandTowerPacket = require("../Packets/GuildDataIslandTower"); + +const schemeTower = require("../Data/schemeTower"); +const queries = require("../Data/queries"); +const { QUERY_FETCH_GUILD_TOWERS, QUERY_UPDATE_GUILD_ISLAND_TOWER_DEAD, QUERY_UPDATE_GUILD_ISLAND_TOWER_UPGRADE, QUERY_UPDATE_GUILD_ISLAND_TOWER_DEFAULT, QUERY_DELETE_GUILD_ISLAND_DAMAGES, QUERY_UPDATE_GUILD_ISLAND_AUTHOR, QUERY_UPDATE_GUILD_ISLAND_DAMAGES } = queries; + +const constants = require("../Data/constants"); +const { EVENTS } = constants; +const { EVENT_ATTACK_STARTED, EVENT_HEALTH_CHANGED_AUTHORLESS, EVENT_ATTACKING, EVENT_ATTACK_BAR, EVENT_DIE, EVENT_GUILD_ISLAND_CAPTURE, EVENT_GUILD_ISLAND_TOWER_BUILT, EVENT_GUILD_ISLAND_TOWER_HEALTH_CHANGED } = EVENTS; + +const Tower = require("../Game/Tower"); +const Game = require("../Game/Game"); +const { guildIslands } = Game; + +module.exports = async function (map, globalDieCallback, globalRewardCallback, globalAttackCallback) { + const towers = await execute(QUERY_FETCH_GUILD_TOWERS, [map.id]); + if (towers.error) return process.exit(); + + const allTowersMap = towers.data; + if (!allTowersMap) return; + + let order = 0; + let previousIslandID = 0; + + for (let i = 0, length = allTowersMap.length; i < length; i++) { + const tower = allTowersMap[i]; + const scheme = schemeTower[tower.entityTypeID]; + if (!scheme) continue; + + const island = guildIslands.find(island => island.id === tower.guildIslandID); + + if (previousIslandID !== island.id) order = 0; + previousIslandID = island.id; + + const towerInstance = new Tower({ + id: tower.id, + order: ++order, + entityTypeID: tower.entityTypeID, + island, + map, + maximumHitpoint: scheme.maximumHitpoints, + currentHitpoint: tower.currentHitpoints, + position: { + x: tower.positionX, + y: tower.positionY + }, + attackCallback: globalAttackCallback, + dieCallback: globalDieCallback + }); + + const packet = EntityNewPacket.tower(towerInstance.getProperties()); + map.registerNetworkPacket(towerInstance.id, packet); + + towerInstance.on(EVENT_DIE, function (data, id) { + const packet = EntityDiePacket(data[0]); + this.map.registerNetworkPacket(id, [packet], [1]); + + const packet_2 = GuildDataIslandTowerDestroyedPacket(data[1]); + const topic = Game.getTopic(this.island.guild.topic); + topic.push(packet_2); + + execute(QUERY_UPDATE_GUILD_ISLAND_TOWER_DEAD, [0, Tower.getID(this.id)]); + }); + + towerInstance.on(EVENT_ATTACK_STARTED, function (data, author) { + const packet = AttackInformationPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); + }); + + towerInstance.on(EVENT_HEALTH_CHANGED_AUTHORLESS, function (data, author) { + const packet = HealthChangedAuthorlessPacket(data); + this.map.registerNetworkPacket(this.id, packet, [1]); + }); + + towerInstance.on(EVENT_ATTACK_BAR, function (data, author) { + const packet = AttackerInformationPacket(data); + this.map.registerIndividualNetworkPacket(author, packet); + }); + + towerInstance.on(EVENT_ATTACKING, function (data, author) { + const packet = AttackingPacket(data[1]); + const packet_2 = HealthChangedPacket(data[0]); + this.map.registerNetworkPacket(author, [packet, packet_2], [1, 1]); + }); + + towerInstance.on("_", function (data) { + execute(QUERY_UPDATE_GUILD_ISLAND_DAMAGES, [data.damage, data.guildIslandID, data.guildID, data.damage]); + }); + + towerInstance.on(EVENT_GUILD_ISLAND_TOWER_HEALTH_CHANGED, function (data, author) { + const topic = Game.getTopic(this.island.guild.topic); + const packet = GuildDataIslandTowerHealthChangedPacket(data); + topic.push(packet); + }); + + towerInstance.on(EVENT_GUILD_ISLAND_TOWER_BUILT, async function (data, author) { + await execute(QUERY_UPDATE_GUILD_ISLAND_TOWER_UPGRADE, [this.entityTypeID, this.hp.maximum, Tower.getID(this.id)]); + + const topic = Game.getTopic(this.island.guild.topic); + const packet = GuildDataIslandTowerBuiltPacket(data); + topic.push(packet); + }); + + towerInstance.on(EVENT_GUILD_ISLAND_CAPTURE, async function (data, author) { + const packet = GuildIslandCapturedPacket(data.packet); + app.publish(this.map.id.toString(), packet, true, true); + + if (data.database.oldID !== data.database.newID) { + const packet_2 = GuildDataIslandCapturedPacket({ + guildIslandID: this.island.id + }); + + const topicOld = Game.getTopic(data.topicOld); + topicOld.push(packet_2); + + const islandProperties = { + id: this.island.id, + mapID: this.map.id, + position: this.island.position, + }; + + const guildDataIslandPacket = GuildDataIslandPacket(islandProperties); + + const topicNew = Game.getTopic(data.topicNew); + topicNew.push(guildDataIslandPacket); + + this.island.towers.forEach(tower => { + const towerProperties = { + entityTypeID: tower.entityTypeID, + id: tower.id, + guildIslandID: island.id, + order: tower.order, + hp: { + current: tower.hp.current + }, + isRepairing: tower.isRepairing() + }; + + const guildDataIslandTowerPacket = GuildDataIslandTowerPacket(towerProperties); + topicNew.push(guildDataIslandTowerPacket); + }); + }; + + const packet_3 = WorldMapGuildEntryPacket({ + locationMapID: this.map.id, + index: this.island.index, + bufferTag: this.island.guild.tag + }); + + Game.maps.forEach(map => app.publish(map.id.toString(), packet_3, true, true)); + + let dbConnection = null; + + try { + dbConnection = await db.getConnection(); + await dbConnection.beginTransaction(); + + await execute(QUERY_UPDATE_GUILD_ISLAND_TOWER_DEFAULT, [5000, this.island.id]); + await execute(QUERY_DELETE_GUILD_ISLAND_DAMAGES, [this.island.id]); + await execute(QUERY_UPDATE_GUILD_ISLAND_AUTHOR, [data.database.newID, this.island.id]); + + await dbConnection.commit(); + } catch { + await dbConnection.rollback(); + } finally { + dbConnection.release(); + }; + }); + }; +}; \ No newline at end of file diff --git a/Maps/Map_1.js b/Maps/Map_1.js new file mode 100644 index 0000000..ad34673 --- /dev/null +++ b/Maps/Map_1.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[0]); + +module.exports = map; diff --git a/Maps/Map_10.js b/Maps/Map_10.js new file mode 100644 index 0000000..8ac971c --- /dev/null +++ b/Maps/Map_10.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[9]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_11.js b/Maps/Map_11.js new file mode 100644 index 0000000..92326ee --- /dev/null +++ b/Maps/Map_11.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[10]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_12.js b/Maps/Map_12.js new file mode 100644 index 0000000..57cc363 --- /dev/null +++ b/Maps/Map_12.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[11]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_13.js b/Maps/Map_13.js new file mode 100644 index 0000000..066827a --- /dev/null +++ b/Maps/Map_13.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[12]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_14.js b/Maps/Map_14.js new file mode 100644 index 0000000..ce6cb36 --- /dev/null +++ b/Maps/Map_14.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[13]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_15.js b/Maps/Map_15.js new file mode 100644 index 0000000..3d78bd7 --- /dev/null +++ b/Maps/Map_15.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[14]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_16.js b/Maps/Map_16.js new file mode 100644 index 0000000..fd74606 --- /dev/null +++ b/Maps/Map_16.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[15]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_17.js b/Maps/Map_17.js new file mode 100644 index 0000000..564f317 --- /dev/null +++ b/Maps/Map_17.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[16]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_18.js b/Maps/Map_18.js new file mode 100644 index 0000000..92c822d --- /dev/null +++ b/Maps/Map_18.js @@ -0,0 +1,5 @@ +const GameMap = require("../Game/Map"); +const data = require("../Data/maps"); +const map = new GameMap(data[17]); + +module.exports = map; \ No newline at end of file diff --git a/Maps/Map_19.js b/Maps/Map_19.js new file mode 100644 index 0000000..473ec9b --- /dev/null +++ b/Maps/Map_19.js @@ -0,0 +1,5 @@ +const GameMap = require("../Game/Map"); +const data = require("../Data/maps"); +const map = new GameMap(data[18]); + +module.exports = map; \ No newline at end of file diff --git a/Maps/Map_2.js b/Maps/Map_2.js new file mode 100644 index 0000000..ad04194 --- /dev/null +++ b/Maps/Map_2.js @@ -0,0 +1,5 @@ +const GameMap = require("../Game/Map"); +const data = require("../Data/maps"); +const map = new GameMap(data[1]); + +module.exports = map; diff --git a/Maps/Map_20.js b/Maps/Map_20.js new file mode 100644 index 0000000..9173765 --- /dev/null +++ b/Maps/Map_20.js @@ -0,0 +1,5 @@ +const GameMap = require("../Game/Map"); +const data = require("../Data/maps"); +const map = new GameMap(data[19]); + +module.exports = map; \ No newline at end of file diff --git a/Maps/Map_3.js b/Maps/Map_3.js new file mode 100644 index 0000000..11ea76f --- /dev/null +++ b/Maps/Map_3.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[2]); + +module.exports = map; diff --git a/Maps/Map_4.js b/Maps/Map_4.js new file mode 100644 index 0000000..2479010 --- /dev/null +++ b/Maps/Map_4.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[3]); + +module.exports = map; \ No newline at end of file diff --git a/Maps/Map_5.js b/Maps/Map_5.js new file mode 100644 index 0000000..e7f5fc0 --- /dev/null +++ b/Maps/Map_5.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[4]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_6.js b/Maps/Map_6.js new file mode 100644 index 0000000..1585db8 --- /dev/null +++ b/Maps/Map_6.js @@ -0,0 +1,5 @@ +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[5]); + +module.exports = map; diff --git a/Maps/Map_7.js b/Maps/Map_7.js new file mode 100644 index 0000000..c2302c1 --- /dev/null +++ b/Maps/Map_7.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[6]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_8.js b/Maps/Map_8.js new file mode 100644 index 0000000..ce07f79 --- /dev/null +++ b/Maps/Map_8.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[7]); + +module.exports = map \ No newline at end of file diff --git a/Maps/Map_9.js b/Maps/Map_9.js new file mode 100644 index 0000000..ed04b4f --- /dev/null +++ b/Maps/Map_9.js @@ -0,0 +1,6 @@ + +const GameMap = require('../Game/Map'); +const data = require("../Data/maps"); +const map = new GameMap(data[8]); + +module.exports = map \ No newline at end of file diff --git a/Messages/AmmunitionChangeMessage.js b/Messages/AmmunitionChangeMessage.js new file mode 100644 index 0000000..d6e7dc8 --- /dev/null +++ b/Messages/AmmunitionChangeMessage.js @@ -0,0 +1,34 @@ +const AmmunitionChangeConfirmationPacket = require("../Packets/AmmunitionChangeConfirmation"); + +module.exports = function (ws, message, player) { + const type = message.getUint8(1); + const id = message.getUint8(2); + + let selectedAmmunitionConfirmedID = 0; + let selectedAmmunitionTypeConfirmedID = 0; + + switch (type) { + case 1: + selectedAmmunitionConfirmedID = player.inventory.setSelectedAmmunition(id); + selectedAmmunitionTypeConfirmedID = 1; + break; + case 2: + selectedAmmunitionConfirmedID = player.inventory.setSelectedHarpoon(id); + selectedAmmunitionTypeConfirmedID = 2; + break; + }; + + const ammunitionChangeConfirmation = { + typeID: selectedAmmunitionTypeConfirmedID, + id: selectedAmmunitionConfirmedID + }; + + console.info(ammunitionChangeConfirmation); + + const ammunitionChangeConfirmationPacket = AmmunitionChangeConfirmationPacket(ammunitionChangeConfirmation); + player.map.registerIndividualNetworkPacket(player.id, ammunitionChangeConfirmationPacket); + + const metadata = [player.hp.current / player.hp.maximum, player.isAttacking(), 0, player.inventory.selectedAmmunition]; + + return metadata; +}; \ No newline at end of file diff --git a/Messages/AttackAbortMessage.js b/Messages/AttackAbortMessage.js new file mode 100644 index 0000000..f8abbdd --- /dev/null +++ b/Messages/AttackAbortMessage.js @@ -0,0 +1,26 @@ +const AttackAbortedPacket = require("../Packets/AttackAborted"); + +const constants = require("../Data/constants"); +const { TYPES } = constants; +const { ENTITY_TYPE_NPC } = TYPES; + +module.exports = function (ws, message, player) { + const targetIdentifier = player.abortAttack(); + if (!targetIdentifier) return; + + const packet = AttackAbortedPacket({ + authorID: targetIdentifier.id, + authorTypeID: targetIdentifier.typeID, + targetID: player.id, + targetTypeID: player.typeID + }); + + player.map.registerIndividualNetworkPacket(player.id, packet); + + if (targetIdentifier.typeID === ENTITY_TYPE_NPC) { + const target = player.map.getEntityFromSubgroup(targetIdentifier.id); + if (!target) return; + + target.determineMovementState(); + }; +}; \ No newline at end of file diff --git a/Messages/AttackStartMessage.js b/Messages/AttackStartMessage.js new file mode 100644 index 0000000..c3609af --- /dev/null +++ b/Messages/AttackStartMessage.js @@ -0,0 +1,18 @@ +const constants = require("../Data/constants"); +const { TYPES } = constants; +const { ENTITY_TYPE_MONSTER } = TYPES; + +module.exports = function (ws, message, player) { + const typeID = message.getUint8(1); + const id = message.getUint32(2); + + const wasAttacking = player.isAttacking(); + + const target = player.map.getEntityFromSubgroup(id); + if (target) { + if (typeID === ENTITY_TYPE_MONSTER) player.initializeAttackWithHarpooner(target); + else player.initializeAttackWithCannon(target); + }; + + return [player.inventory.selectedAmmunition, player.hp.current / player.hp.maximum, player.position.x, player.position.y, wasAttacking, 0 - 0]; +}; \ No newline at end of file diff --git a/Messages/CauldronMessage.js b/Messages/CauldronMessage.js new file mode 100644 index 0000000..eb5aa45 --- /dev/null +++ b/Messages/CauldronMessage.js @@ -0,0 +1,44 @@ +const Reward = require("../Game/Reward"); + +const rewardCallback = require("../Callbacks/reward"); + +const MojoRemovedPacket = require("../Packets/MojoRemoved"); + +const MAXIMUM_BATCH_COUNT_MOJO = 1000; + +const cauldronReward = new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 1500, chance: 16.5 }, + 6: { amountMinimum: 1500, chance: 16.5 }, + 7: { amountMinimum: 1500, chance: 16.5 }, + 8: { amountMinimum: 1500, chance: 16.5 }, + 9: { amountMinimum: 1500, chance: 16.5 } + }, + 7: { + 10: { amountMinimum: 1, amountMaximum: 5, chance: 16.5 } + }, + 5: { + 17: { amountMinimum: 1, chance: 20 } + } + } +}); + +module.exports = function (ws, message, player) { + let amount = message.getUint16(1); + if (!amount) return + + if (amount > MAXIMUM_BATCH_COUNT_MOJO) amount = MAXIMUM_BATCH_COUNT_MOJO; + + const usedAmount = player.inventory.alterAmount(1, 3, amount); + if (!usedAmount) return + + const playerInventory = player.inventory.getPosessions(); + cauldronReward.single(usedAmount, playerInventory, rewards => rewardCallback({ isRegular: false, authorID: player.id, rewards, source: {} })); + + const packet = MojoRemovedPacket(usedAmount); + player.map.registerIndividualNetworkPacket(player.id, packet); + + // global.test.accumulateItem(`${player.id}-1-3`, -amount); +}; \ No newline at end of file diff --git a/Messages/CloseMessage.js b/Messages/CloseMessage.js new file mode 100644 index 0000000..7dec9b5 --- /dev/null +++ b/Messages/CloseMessage.js @@ -0,0 +1,13 @@ +const packet_255 = require("../Packets/packet_255"); + +module.exports = function (ws, message, player) { + if (player.isBeingAttacked()) return; + + const shouldCancelDisconnect = player.isDisconnecting() && !player.hasUnexpectedlyLeft; + const packet = packet_255({ packetId: 12, isSuccessful: shouldCancelDisconnect }); + player.map.registerIndividualNetworkPacket(player.id, packet); + + if (shouldCancelDisconnect) return player.cancelDisconnect(); + + player.regularDisconnect(); +}; \ No newline at end of file diff --git a/Messages/DesignChangeMessage.js b/Messages/DesignChangeMessage.js new file mode 100644 index 0000000..9187d94 --- /dev/null +++ b/Messages/DesignChangeMessage.js @@ -0,0 +1,23 @@ +const DesignChangedPacket = require("../Packets/DesignChanged"); +const packet_255 = require("../Packets/packet_255"); + +module.exports = function (ws, message, player) { + const designID = message.getUint16(1); + + const newDesignID = player.setDesign(designID); + if (!newDesignID) { + const packet = packet_255({ packetId: 40, isSuccessful: Boolean(newDesignID) }) + return player.map.registerIndividualNetworkPacket(player.id, packet); + }; + + //setTimeout(() => { + const data = { + typeID: player.typeID, + id: player.id, + designID + }; + + const packet = DesignChangedPacket(data); + player.map.registerNetworkPacket(player.id, packet); + //}, 3000); +}; \ No newline at end of file diff --git a/Messages/EquipmentChangeMessage.js b/Messages/EquipmentChangeMessage.js new file mode 100644 index 0000000..6398b13 --- /dev/null +++ b/Messages/EquipmentChangeMessage.js @@ -0,0 +1,75 @@ +const PropertyChangedPacket = require("../Packets/PropertyChanged"); +const EquipmentConfirmationPacket = require("../Packets/EquipmentConfirmation"); + +module.exports = function (ws, message, player) { + let packet = null; + + if (player.isAttacking()) { + packet = EquipmentConfirmationPacket(false); + player.map.registerIndividualNetworkPacket(player.id, packet); + + return; + }; + + const equimpentTypeID = message.getUint8(2); + const id = message.getUint8(3); + + const equimpents = equimpentTypeID === 1 ? player.inventory.getCannons() : player.inventory.getHarpooners(); + const equimpent = equimpents.find(e => e.id === id); + if (!equimpent) return + + const playerID = player.id; + const currentRange = equimpentTypeID === 1 ? player.maximumCannonRange : player.maximumHarpoonerRange; + const typeID = equimpentTypeID === 1 ? 3 : 4; + + const type = message.getUint8(1); + const quantity = message.getUint8(4); + + let propertyChangedPacket = null; + let maximumRange = 0; + + if (type) { + if (!equimpent.amountEquipped && equimpent.range > currentRange) { + propertyChangedPacket = PropertyChangedPacket({ + playerID, + typeID, + value: equimpent.range + }); + + maximumRange = equimpent.range; + }; + + const isEquipmentAlterSuccessful = equimpent.equip(quantity); + + packet = EquipmentConfirmationPacket(isEquipmentAlterSuccessful); + } else { + const isEquipmentAlterSuccessful = equimpent.unequip(quantity); + + if (!equimpent.amountEquipped) { + for (let i = 0, length = equimpents.length; i < length; i++) + if (equimpents[i].range > maximumRange && equimpents[i].amountEquipped) maximumRange = equimpents[i].range; + + if (currentRange > maximumRange) + propertyChangedPacket = PropertyChangedPacket({ + playerID, + typeID, + value: maximumRange + }); + }; + + packet = EquipmentConfirmationPacket(isEquipmentAlterSuccessful); + }; + + if (propertyChangedPacket) { + if (equimpentTypeID === 1) player.maximumCannonRange = maximumRange; + else player.maximumHarpoonerRange = maximumRange; + + player.map.registerNetworkPacket(player.id, propertyChangedPacket); + }; + + player.map.registerIndividualNetworkPacket(player.id, packet); + + const metadata = [player.hp.current / player.hp.maximum, equimpentTypeID, 0, quantity]; + + return metadata; +}; \ No newline at end of file diff --git a/Messages/GuildCreateMessage.js b/Messages/GuildCreateMessage.js new file mode 100644 index 0000000..59ceaa2 --- /dev/null +++ b/Messages/GuildCreateMessage.js @@ -0,0 +1,79 @@ +const GuildDataMemberPacket = require("../Packets/GuildDataMember"); +const GuildMemberNewPacket = require("../Packets/GuildMemberNew"); +const GuildLeaveConfirmationPacket = require("../Packets/GuildLeaveConfirmation"); +const GuildDataCreationFeedbackPacket = require("../Packets/GuildDataCreationFeedback"); + +const Guild = require("../Game/Guild"); + +const Game = require("../Game/Game"); +const { guilds } = Game; + +const constants = require("../Data/constants"); + +module.exports = async function (ws, message, player) { + if (player.guild.id) return; + + const memberAuthor = player.guild.getMember(player.id); + if (memberAuthor) return; + + const guildTagLength = message.getUint8(1); + const guildNameLength = message.getUint8(1 + 1 + guildTagLength); + const guildDescriptionLength = message.getUint8(1 + 1 + 1 + guildTagLength + guildNameLength); + + const guildTag = message.buffer.slice(2, 2 + guildTagLength); + const guildName = message.buffer.slice(2 + 1 + guildTagLength, 2 + 1 + guildTagLength + guildNameLength); + const guildDescription = message.buffer.slice(2 + 1 + 1 + guildTagLength + guildNameLength, 2 + 1 + 1 + guildNameLength + guildTagLength + guildDescriptionLength); + + const guildTagBuffer = Buffer.from(guildTag); + const guildNameBuffer = Buffer.from(guildName); + const guildDescriptionBuffer = Buffer.from(guildDescription); + console.info(player.id, "???") + const guildCreateQuery = await execute("INSERT INTO guilds (tag, name, description, createdBy) VALUES (?, ?, ?, ?)", [guildTagBuffer, guildNameBuffer, guildDescriptionBuffer, player.id]); + if (!guildCreateQuery.data || guildCreateQuery.data.affectedRows !== 1) { + const guildDataCreationFeedback = { + reason: 1 + }; + + const guildDataCreationFeedbackPacket = GuildDataCreationFeedbackPacket(guildDataCreationFeedback); + return player.map.registerIndividualNetworkPacket(player.id, guildDataCreationFeedbackPacket); + }; + + const guildID = guildCreateQuery.data.insertId; + console.info(guildCreateQuery, guildID, player.id) + + const guild = guilds[guildID] = new Guild({ + id: guildID, + name: guildNameBuffer, + tag: guildTagBuffer, + description: guildDescriptionBuffer, + ownerID: player.id, + experiencePoints: 0, + taxRates: [0, 0], + timestamps: { + taxRatesEditedAt: 0, + tagEditedAt: 0, + nameEditedAt: 0, + descriptionEditedAt: 0 + } + }); + + player.guild = guild; + + const member = { + id: player.id, + permission: 31 + }; + + guild.addMember(member); + + const guildStatePacket = GuildLeaveConfirmationPacket(true); // should rename everywhere + player.map.registerNetworkPacket(player.id, guildStatePacket); + + const guildMemberNew = { + playerID: player.id, + tag: guild.tag + }; + + const guildMemberNewPacket = GuildMemberNewPacket(guildMemberNew); + player.map.registerNetworkPacket(player.id, guildMemberNewPacket); +}; \ No newline at end of file diff --git a/Messages/GuildDataMessage.js b/Messages/GuildDataMessage.js new file mode 100644 index 0000000..cc7b0f1 --- /dev/null +++ b/Messages/GuildDataMessage.js @@ -0,0 +1,150 @@ +const GuildDataGeneralPacket = require("../Packets/GuildDataGeneral"); +const GuildDataMemberPacket = require("../Packets/GuildDataMember"); +const GuildDataApplicantPacket = require("../Packets/GuildDataApplicant"); +const GuildDataEconomyPacket = require("../Packets/GuildDataEconomy"); +const GuildDataIslandPacket = require("../Packets/GuildDataIsland"); +const GuildDataIslandTowerPacket = require("../Packets/GuildDataIslandTower"); + +const LEVELS = require("../Data/levels"); + +const queries = require("../Data/queries"); +const { QUERY_FETCH_GUILD_OFFLINE_MEMBERS } = queries; + +const Game = require("../Game/Game"); +const { players, guildIslands } = Game; + +module.exports = async function (ws, message, player) { + console.info(player.guild.id, player.guild.ownerID) + if (!player.guild.id) return console.info("nuh uh GuildDataMessage"); + + const applicantsOffline = []; + const playersOffline = []; + const packets = []; + console.info("GuildDataMessage", player.id); + player.guild.members.forEach(member => { + if (!players[member.id]) playersOffline.push(member.id); + else { + const onlinePlayer = players[member.id]; + const guildDataMember = { + id: member.id, + permission: member.permission, + isOnline: true, + pirateLevel: onlinePlayer.pirateLevel, + name: onlinePlayer.name + }; + + const guildMemberPacket = GuildDataMemberPacket(guildDataMember); + packets.push(guildMemberPacket); + }; + }); + + if (playersOffline.length) { + const offlinePlayersQuery = await execute(QUERY_FETCH_GUILD_OFFLINE_MEMBERS, playersOffline); + if (offlinePlayersQuery.data) { + offlinePlayersQuery.data.forEach(offlinePlayer => { + const guildDataMember = { + id: offlinePlayer.id, + isOnline: false, + pirateLevel: LEVELS.pirateLevel(offlinePlayer.amount), + name: offlinePlayer.username + }; + + const guildMemberPacket = GuildDataMemberPacket(guildDataMember); + packets.push(guildMemberPacket); + }); + }; + }; + + player.guild.requests.forEach(request => { + const applicantPlayer = players[request.playerID]; + if (!applicantPlayer) applicantsOffline.push(request.playerID); + else { + const guildDataApplicant = { + id: request.playerID, + pirateLevel: applicantPlayer.pirateLevel, + username: applicantPlayer.name + }; + + const guildDataApplicantPacket = GuildDataApplicantPacket(guildDataApplicant); + packets.push(guildDataApplicantPacket); + }; + }); + + if (applicantsOffline.length) { + const offlineApplicantsQuery = await execute(QUERY_FETCH_GUILD_OFFLINE_MEMBERS, applicantsOffline); + if (offlineApplicantsQuery.data) { + offlineApplicantsQuery.data.forEach(offlineApplicant => { + const guildDataApplicant = { + id: offlineApplicant.id, + pirateLevel: LEVELS.pirateLevel(offlineApplicant.amount), + username: offlineApplicant.username + }; + + const guildDataApplicantPacket = GuildDataApplicantPacket(guildDataApplicant); + packets.push(guildDataApplicantPacket); + }); + }; + }; + + const guildDataGeneral = { + level: player.guild.level, + experience: player.guild.experiencePoints, + size: { + current: player.guild.size, + maximum: player.guild.sizeMaximum + }, + taxRates: player.guild.taxRates.map(r => r * 100), + tag: player.guild.tag, + name: player.guild.name, + description: player.guild.description, + timestamps: player.guild.timestamps + }; + + const guildDataGeneralPacket = GuildDataGeneralPacket(guildDataGeneral); + packets.push(guildDataGeneralPacket); + console.info(guildDataGeneral) + const guildDataEconomy = []; + player.guild.bank.list.each((_, value) => { + guildDataEconomy.push({ + id: value.id, + amount: value.amount + }); + }); + console.info(guildDataEconomy, "guildDataEconomy") + const guildDataEconomyPacket = GuildDataEconomyPacket(guildDataEconomy); + packets.push(guildDataEconomyPacket); + + guildIslands.forEach(island => { + if (player.guild.id !== island.guild.id) return; + + const islandProperties = { + id: island.id, + mapID: island.locationMapID, + position: island.position, + }; + + const guildDataIslandPacket = GuildDataIslandPacket(islandProperties); + packets.push(guildDataIslandPacket); + + island.towers.forEach(tower => { + const towerProperties = { + entityTypeID: tower.entityTypeID, + id: tower.id, + guildIslandID: island.id, + order: tower.order, + hp: { + current: tower.hp.current + }, + isRepairing: tower.isRepairing() + }; + + const guildDataIslandTowerPacket = GuildDataIslandTowerPacket(towerProperties); + packets.push(guildDataIslandTowerPacket); + }); + }); + + const guildTopic = Game.getTopic(player.guild.topic); + player.topics.addItem(player.guild.topic, guildTopic); + console.info("Sending out general data to", player.id); + player.map.registerIndividualNetworkPacket(player.id, packets); +}; diff --git a/Messages/GuildDetailsIdentityEditMessage.js b/Messages/GuildDetailsIdentityEditMessage.js new file mode 100644 index 0000000..04613e2 --- /dev/null +++ b/Messages/GuildDetailsIdentityEditMessage.js @@ -0,0 +1,102 @@ +const GuildDataIdentityModifiedPacket = require("../Packets/GuildDataIdentityModified"); + +const Game = require("../Game/Game"); +const { players } = Game; + +const constants = require("../Data/constants"); +const { GUILD_COOLDOWNS } = constants; +const { GUILD_COOLDOWN_EDIT_TAG, GUILD_COOLDOWN_EDIT_NAME, GUILD_COOLDOWN_EDIT_DESCRIPTION } = GUILD_COOLDOWNS; + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return; + + const memberAuthor = player.guild.getMember(player.id); + if (!memberAuthor) return; + + const hasEditIdentitiyPermission = memberAuthor.permission & 8; +// if (!hasEditIdentitiyPermission) return; + + const guildTagLength = message.getUint8(1); + const guildNameLength = message.getUint8(1 + 1 + guildTagLength); + const guildDescriptionLength = message.getUint8(1 + 1 + 1 + guildTagLength + guildNameLength); + + const guildTag = message.buffer.slice(2, 2 + guildTagLength); + const guildName = message.buffer.slice(2 + 1 + guildTagLength, 2 + 1 + guildTagLength + guildNameLength); + const guildDescription = message.buffer.slice(2 + 1 + 1 + guildTagLength + guildNameLength, 2 + 1 + 1 + guildNameLength + guildTagLength + guildDescriptionLength); + + const guildTagBuffer = Buffer.from(guildTag); + const guildNameBuffer = Buffer.from(guildName); + const guildDescriptionBuffer = Buffer.from(guildDescription); + + const noww = new Date(); + const now = noww.getTime(); + + const isCooldownOverTag = player.guild.timestamps.tagEditedAt * 1000 + GUILD_COOLDOWN_EDIT_TAG < now; + const isCooldownOverName = player.guild.timestamps.nameEditedAt * 1000 + GUILD_COOLDOWN_EDIT_NAME < now; + const isCooldownOverDescription = player.guild.timestamps.descriptionEditedAt * 1000 + GUILD_COOLDOWN_EDIT_DESCRIPTION < now; + console.info(guildTagLength, guildTagBuffer, player.guild.tag, isCooldownOverTag, isCooldownOverName, isCooldownOverDescription); + let queryField = []; + let queryArguments = []; + + const r = [0, 0, 0]; + + if (guildTagLength + && !guildTagBuffer.equals(player.guild.tag) + && isCooldownOverTag) { + queryField.push("tag", "tagEditedAt"); + queryArguments.push(guildTag, noww); + r[0] = 1; + + player.guild.timestamps.tagEditedAt = now / 1000; + }; + + if (guildNameLength + && !guildNameBuffer.equals(player.guild.name) + && isCooldownOverName) { + queryField.push("name", "nameEditedAt"); + queryArguments.push(guildName, noww); + r[1] = 1; + + player.guild.timestamps.nameEditedAt = now / 1000; + }; + + if (guildDescriptionLength + && !guildDescriptionBuffer.equals(player.guild.description) + && isCooldownOverDescription) { + queryField.push("description", "descriptionEditedAt"); + queryArguments.push(guildDescription, noww); + r[2] = 1; + + player.guild.timestamps.descriptionEditedAt = now / 1000; + }; + + if (!queryArguments.length) return console.info("can't modify anything"); + + queryArguments.push(player.guild.id); + + const queryBody = queryField.map(f => `${f} = ?`).join(", "); + const guildIdentityEditQuery = await execute(`UPDATE guilds SET ${queryBody} WHERE id = ?`, queryArguments); + if (!guildIdentityEditQuery.data) return; + + if (guildIdentityEditQuery.data.changedRows === 1) { + player.guild.tag = guildTagBuffer; + player.guild.name = guildNameBuffer; + player.guild.description = guildDescriptionBuffer; + console.info(guildTagBuffer, guildNameBuffer, guildDescriptionBuffer); + const guildDataIdentityModified = { + r, + timestamps: { + tagEditedAt: (player.guild.timestamps.tagEditedAt + GUILD_COOLDOWN_EDIT_TAG / 1000), + nameEditedAt: (player.guild.timestamps.nameEditedAt + GUILD_COOLDOWN_EDIT_NAME / 1000), + descriptionEditedAt: (player.guild.timestamps.descriptionEditedAt + GUILD_COOLDOWN_EDIT_DESCRIPTION / 1000) + }, + bufferTag: guildTagBuffer, + bufferName: guildNameBuffer, + bufferDescription: guildDescriptionBuffer + }; + + const topic = Game.getTopic(player.guild.topic); + const guildDataIdentityModifiedPacket = GuildDataIdentityModifiedPacket(guildDataIdentityModified); + topic.push(guildDataIdentityModifiedPacket); + }; +}; \ No newline at end of file diff --git a/Messages/GuildDetailsTaxEditMessage.js b/Messages/GuildDetailsTaxEditMessage.js new file mode 100644 index 0000000..b8f6aad --- /dev/null +++ b/Messages/GuildDetailsTaxEditMessage.js @@ -0,0 +1,30 @@ +const GuildDataTaxRatesModifiedPacket = require("../Packets/GuildDataTaxRatesModified"); + +const constants = require("../Data/constants"); +const { MAXIMUM } = constants; +const { MAX_GUILD_TAX_RATE_GOLD, MAX_GUILD_TAX_RATE_EMERALD } = MAXIMUM; + +const queries = require("../Data/queries"); +const { QUERY_UPDATE_GUILD_TAX_RATES } = queries; + +const Game = require("../Game/Game"); + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return; + + const taxRateGold = message.getUint8(1); + if (taxRateGold > MAX_GUILD_TAX_RATE_GOLD) return; + + const taxRateEmerald = message.getUint8(2); + if (taxRateEmerald > MAX_GUILD_TAX_RATE_EMERALD) return; + + const guildDataTaxRatesModified = [taxRateGold, taxRateEmerald]; + + const taxRatesUpdateQuery = await execute(QUERY_UPDATE_GUILD_TAX_RATES, [taxRateGold, taxRateEmerald, player.guild.id]); + if (!taxRatesUpdateQuery.data || taxRatesUpdateQuery.data.affectedRows !== 1) [guildDataTaxRatesModified[0], guildDataTaxRatesModified[1]] = [255, 255]; + else player.guild.taxRates = guildDataTaxRatesModified.map(t => t / 100); + + const topic = Game.getTopic(player.guild.topic); + const guildDataTaxRatesModifiedPacket = GuildDataTaxRatesModifiedPacket(guildDataTaxRatesModified); + topic.push(guildDataTaxRatesModifiedPacket); +}; \ No newline at end of file diff --git a/Messages/GuildDonateMessage.js b/Messages/GuildDonateMessage.js new file mode 100644 index 0000000..d24e07b --- /dev/null +++ b/Messages/GuildDonateMessage.js @@ -0,0 +1,58 @@ +const GuildDataEconomyDonatedPacket = require("../Packets/GuildDataEconomyDonated"); + +const Game = require("../Game/Game"); + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return; + + if (player.pirateLevel < 10) return console.info("pirate level donate"); + + const categoryID = 1; + const resourceID = message.getUint8(1); + const amount = message.getUint32(2); + + const playerEconomy = player.inventory.getEconomy(); + const economyResource = playerEconomy.find(r => r.id === resourceID); + if (economyResource.amount < amount) return console.info("not enough resource", economyResource); + + const guildEconomy = player.guild.bank.list.getElement(resourceID); + if (!guildEconomy) return; + + let dbConnection = null; + try { + dbConnection = await db.getConnection(); + await dbConnection.beginTransaction(); + await dbConnection.execute("INSERT INTO resources (playerID, categoryID, itemID, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?", [player.id, categoryID, resourceID, economyResource.amount - amount, economyResource.amount - amount]); + await dbConnection.execute("INSERT INTO guildresources (guildID, categoryID, itemID, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?", [player.guild.id, categoryID, resourceID, guildEconomy.amount + amount, guildEconomy.amount + amount]); + await dbConnection.commit(); + + const amountCapped = player.inventory.alterAmount(categoryID, resourceID, amount); + + const resource = { + categoryID, + id: resourceID, + amount: amountCapped + }; + + player.guild.bank.depositTax(resource); + console.info("success desposit", resource); + + const topic = Game.getTopic(player.guild.topic); + const guildDataEconomyDonatedPacket = GuildDataEconomyDonatedPacket(resource); + topic.push(guildDataEconomyDonatedPacket); + } catch (e) { + console.info(e) + await dbConnection.rollback(); + + const resource = { + categoryID, + id: resourceID, + amount: 0 + }; + + const guildDataEconomyDonatedPacket = GuildDataEconomyDonatedPacket(resource); + player.map.registerIndividualNetworkPacket(player.id, guildDataEconomyDonatedPacket); + } finally { + dbConnection.release(); + }; +}; \ No newline at end of file diff --git a/Messages/GuildIslandTowerBuildMessage.js b/Messages/GuildIslandTowerBuildMessage.js new file mode 100644 index 0000000..f4d7856 --- /dev/null +++ b/Messages/GuildIslandTowerBuildMessage.js @@ -0,0 +1,69 @@ +const DesignChangedPacket = require("../Packets/DesignChanged"); +const PropertyChangedGeneralPacket = require("../Packets/PropertyChangedGeneral"); +const HealthChangedAuthorlessPacket = require("../Packets/HealthChangedAuthorless"); + +const Game = require("../Game/Game"); +const { players, guildIslands, packets } = Game; + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return; + + const guildIslandID = message.getUint8(1); + const guildIsland = guildIslands.find(island => island.id === guildIslandID); + if (!guildIsland) return console.info("guildIslandTowerBuildMessage 1"); + + const id = message.getUint32(2); + const guildTower = guildIsland.getTower(id); + if (!guildTower) return console.info("guildIslandTowerBuildMessage 2"); + + const entityTypeID = message.getUint8(6); + + const currentHitpointBefore = guildTower.hp.current; + const isUpgradeSuccessful = guildTower.upgrade(entityTypeID); + if (!isUpgradeSuccessful) return console.info("not successful build for tower", guildTower.id, "on guild island", guildIslandID); + + console.info("Tower upgrade", guildTower.id, guildIslandID); + + const designChanged = { + typeID: guildTower.typeID, + id: guildTower.id, + designID: guildTower.entityTypeID + }; + + const designChangedPacket = DesignChangedPacket(designChanged); + guildTower.map.registerNetworkPacket(guildTower.id, designChangedPacket); + + const propertyChangedGeneral = { + typeID: guildTower.typeID, + id: guildTower.id, + propertyTypeID: 1, + value: guildTower.hp.maximum + }; + + const propertyChangedGeneral2 = { + typeID: guildTower.typeID, + id: guildTower.id, + propertyTypeID: 2, + value: 0 + }; + + const propertyChangedGeneralPacket = PropertyChangedGeneralPacket(propertyChangedGeneral); + const propertyChangedGeneralPacket2 = PropertyChangedGeneralPacket(propertyChangedGeneral2); + const packets = [propertyChangedGeneralPacket, propertyChangedGeneralPacket2]; + + const hitpointToAdd = guildTower.hp.maximum - currentHitpointBefore; + if (hitpointToAdd) { + const healthChangedAuthorless = { + targetTypeID: guildTower.typeID, + targetID: guildTower.id, + amount: hitpointToAdd, + type: 1, + isCritical: false + }; + + const healthChangedAuthorlessPacket = HealthChangedAuthorlessPacket(healthChangedAuthorless); + packets.push(healthChangedAuthorlessPacket); + }; + + guildTower.map.registerNetworkPacket(guildTower.id, packets); +}; diff --git a/Messages/GuildIslandTowerRepairMessage.js b/Messages/GuildIslandTowerRepairMessage.js new file mode 100644 index 0000000..deb833b --- /dev/null +++ b/Messages/GuildIslandTowerRepairMessage.js @@ -0,0 +1,18 @@ +const Game = require("../Game/Game"); +const { players, guildIslands } = Game; + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return; + + const guildIslandID = message.getUint8(1); + const guildIsland = guildIslands.find(island => island.id === guildIslandID); + if (!guildIsland) return console.info("asd"); + + const id = message.getUint32(2); + const tower = guildIsland.getTower(id); + if (!tower) return console.info("asd2"); + + tower.initializeRepair(); + + console.info("Tower", tower.id, "is being repaired on guild island", guildIslandID); +}; diff --git a/Messages/GuildJoinRequestRevokeMessage.js b/Messages/GuildJoinRequestRevokeMessage.js new file mode 100644 index 0000000..9c9e2e1 --- /dev/null +++ b/Messages/GuildJoinRequestRevokeMessage.js @@ -0,0 +1,40 @@ +const GuildSearchResultStateChanged = require("../Packets/GuildSearchResultStateChanged"); +const GuildDataApplicantRemove = require("../Packets/GuildDataApplicantRemove"); + +const Game = require("../Game/Game"); +const { players, guilds } = Game; + +const QUERY_GUILD_REQUEST_REVOKE = "DELETE FROM guildrequests WHERE guildID = ? AND playerID = ?"; + +module.exports = async function (ws, message, player) { + const guildID = message.getUint16(1); + const guild = guildID ? guilds[guildID] : player.guild; + if (!guild || !guild.id) return; + + if (guild.id === player.guild.id) return; + + const playerID = message.getUint32(3); + const targetPlayer = playerID ? players[playerID] : player; + + const guildJoinRequestRevokeQuery = await execute(QUERY_GUILD_REQUEST_REVOKE, [guild.id, targetPlayer.id]); + if (!guildJoinRequestRevokeQuery.data) return console.info("shit"); + + guild.removeRequest(targetPlayer.id); + + const guildSearchResultStateChanged = { + id: guild.id, + hasApplied: false + }; + + const guildSearchResultStateChangedPacket = GuildSearchResultStateChanged(guildSearchResultStateChanged); + targetPlayer.map.registerIndividualNetworkPacket(targetPlayer.id, guildSearchResultStateChangedPacket); + + const guildDataApplicantRemove = { + id: targetPlayer.id + }; + + const guildDataApplicantRemovePacket = GuildDataApplicantRemove(guildDataApplicantRemove); + const topic = Game.getTopic(guild.topic); + topic.push(guildDataApplicantRemovePacket); +}; + diff --git a/Messages/GuildJoinRequestSubmitMessage.js b/Messages/GuildJoinRequestSubmitMessage.js new file mode 100644 index 0000000..6fec584 --- /dev/null +++ b/Messages/GuildJoinRequestSubmitMessage.js @@ -0,0 +1,55 @@ +const GuildSearchResultStateChangedPacket = require("../Packets/GuildSearchResultStateChanged"); +const GuildDataApplicantPacket = require("../Packets/GuildDataApplicant"); + +const Game = require("../Game/Game"); +const { guilds } = Game; + +const MAXIMUM_GUILD_REQUEST_MESSAGE_LENGTH = 256; + +const QUERY_GUILD_REQUEST_SUBMIT = "INSERT INTO guildrequests (guildID, playerID, message) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE message = ?"; +const QUERY_GUILD_REQUEST_SUBMIT_DEFAULT = "INSERT INTO guildrequests (guildID, playerID) VALUES (?, ?) ON DUPLICATE KEY UPDATE message = ?"; + +module.exports = async function (ws, message, player) { + if (player.guild.id) return; + + const guildID = message.getUint16(1); + + const guild = guilds[guildID]; + if (!guild) return; + + // if (guild.hasRequested()) return + + const requestMessageLength = message.getUint8(3); + if (requestMessageLength > MAXIMUM_GUILD_REQUEST_MESSAGE_LENGTH) return; + + const requestMessage = requestMessageLength > 0 ? message.buffer.slice(4, 4 + requestMessageLength) : "test"; + console.info(requestMessage, guildID, "GuildJoinRequestSubmitMessage"); + const isRequestSuccessful = guild.addRequest(player.id, requestMessage); + if (!isRequestSuccessful) return console.info("shiitt"); + + const query = requestMessageLength > 0 ? QUERY_GUILD_REQUEST_SUBMIT : QUERY_GUILD_REQUEST_SUBMIT_DEFAULT; + const queryParameters = requestMessageLength > 0 ? [guildID, player.id, requestMessage, requestMessage] : [guildID, player.id, requestMessage]; + + const guildJoinRequestSubmitQuery = await execute(query, queryParameters); + console.info(guildJoinRequestSubmitQuery) + if (!guildJoinRequestSubmitQuery.data) return console.info("shit"); + + const guildSearchResultStateChanged = { + id: guildID, + hasApplied: true + }; + + const guildSearchResultStateChangedPacket = GuildSearchResultStateChangedPacket(guildSearchResultStateChanged); + player.map.registerIndividualNetworkPacket(player.id, guildSearchResultStateChangedPacket); + + const guildDataApplicant = { + id: player.id, + pirateLevel: player.pirateLevel, + username: player.name + }; + + const guildDataApplicantPacket = GuildDataApplicantPacket(guildDataApplicant); + const topic = Game.getTopic(guild.topic); + topic.push(guildDataApplicantPacket); +}; + diff --git a/Messages/GuildMemberJoinMessage.js b/Messages/GuildMemberJoinMessage.js new file mode 100644 index 0000000..83de799 --- /dev/null +++ b/Messages/GuildMemberJoinMessage.js @@ -0,0 +1,91 @@ +const GuildMemberNewPacket = require("../Packets/GuildMemberNew"); +const GuildDataMemberPacket = require("../Packets/GuildDataMember"); +const GuildLeaveConfirmationPacket = require("../Packets/GuildLeaveConfirmation"); +const GuildDataApplicantRemovePacket = require("../Packets/GuildDataApplicantRemove"); + +const LEVELS = require("../Data/levels"); + +const queries = require("../Data/queries"); +const { QUERY_FETCH_GUILD_OFFLINE_MEMBERS } = queries; + +const Game = require("../Game/Game"); +const { players } = Game; + +module.exports = async function (ws, message, player) { + if (!player.guild.id) return + + const playerID = message.getUint32(1); + const guild = player.guild; + + const isRequested = guild.hasRequested(playerID); + if (!isRequested) return + + const acceptedMember = { + id: playerID + }; + + const isAcceptSuccessful = guild.addMember(acceptedMember); + if (!isAcceptSuccessful) return; + + const topic = Game.getTopic(guild.topic); + + const onlinePlayer = players[playerID]; + if (onlinePlayer) { + onlinePlayer.guild = guild; + + const guildMemberNew = { + playerID: onlinePlayer.id, + tag: player.guild.tag + }; + + const guildMemberStateConfirmation = GuildLeaveConfirmationPacket(true); + onlinePlayer.map.registerIndividualNetworkPacket(playerID, guildMemberStateConfirmation); + + const packet = GuildMemberNewPacket(guildMemberNew); + onlinePlayer.map.registerNetworkPacket(onlinePlayer.id, packet); + + const guildDataMember = { + id: onlinePlayer.id, + permission: 0, + isOnline: true, + pirateLevel: onlinePlayer.pirateLevel, + name: onlinePlayer.name + }; + + const guildMemberPacket = GuildDataMemberPacket(guildDataMember); + topic.push(guildMemberPacket); + + const guildDataApplicantRemove = { + id: playerID + }; + + const guildDataApplicantRemovePacket = GuildDataApplicantRemovePacket(guildDataApplicantRemove); + topic.push(guildDataApplicantRemovePacket); + } else { + const offlinePlayerQuery = await execute(QUERY_FETCH_GUILD_OFFLINE_MEMBERS, [playerID]); + if (!offlinePlayerQuery.data) return; + + const { username, amount } = offlinePlayerQuery.data[0]; + + const guildDataMember = { + id: playerID, + permission: 0, + isOnline: false, + pirateLevel: LEVELS.pirateLevel(amount), + name: username + }; + + const guildMemberPacket = GuildDataMemberPacket(guildDataMember); + topic.push(guildMemberPacket); + + const guildDataApplicantRemove = { + id: playerID + }; + + const guildDataApplicantRemovePacket = GuildDataApplicantRemovePacket(guildDataApplicantRemove); + topic.push(guildDataApplicantRemovePacket); + }; + + await execute("INSERT INTO guildmembers (playerID, guildID) VALUES (?, ?)", [playerID, guild.id]); + await execute("DELETE FROM guildrequests WHERE playerID = ?", [playerID]); +}; \ No newline at end of file diff --git a/Messages/GuildMemberLeaveMessage.js b/Messages/GuildMemberLeaveMessage.js new file mode 100644 index 0000000..ca613dc --- /dev/null +++ b/Messages/GuildMemberLeaveMessage.js @@ -0,0 +1,57 @@ +const GuildMemberOldPacket = require("../Packets/GuildMemberOld"); +const GuildLeaveConfirmationPacket = require("../Packets/GuildLeaveConfirmation"); +const GuildDataMemberRemovedPacket = require("../Packets/GuildDataMemberRemoved"); + +const queries = require("../Data/queries"); +const { QUERY_DELETE_GUILD_MEMBER } = queries; + +const constants = require("../Data/constants"); +const { GUILD_PERMISSIONS } = constants; +const { GUILD_PERMISSION_KICK } = GUILD_PERMISSIONS; + +const Game = require("../Game/Game"); +const { players, guilds } = Game; + +module.exports = async function (ws, message, player) { + const playerID = message.getUint32(1); + + const targetKickPlayer = players[playerID]; + const guild = targetKickPlayer ? targetKickPlayer.guild : player.guild; + + const targetMember = guild.getMember(playerID); + if (!targetMember) return; + + const authorMember = guild.getMember(player.id); + if (!authorMember) return; + + const permissionKick = authorMember.permission & GUILD_PERMISSION_KICK; + if (playerID !== player.id && !permissionKick) return; + if (playerID === player.id && guild.isLeader(playerID)) return; + + const kickResultQuery = await execute(QUERY_DELETE_GUILD_MEMBER, [playerID, 0]); + if (!kickResultQuery.data || kickResultQuery.data.affectedRows !== 1) return; + + guild.removeMember(playerID); + + const guildDataMemberRemoved = { + id: playerID + }; + + const guildDataMemberRemovedPacket = GuildDataMemberRemovedPacket(guildDataMemberRemoved); + // player.map.registerIndividualNetworkPacket(player.id, guildDataMemberRemovedPacket); + const topic = Game.getTopic(guild.topic); + topic.push(guildDataMemberRemovedPacket); + + if (!targetKickPlayer) return; + + targetKickPlayer.topics.removeItem(targetKickPlayer.guild.topic); + console.info("removed guild topic from player", targetKickPlayer.guild.topic); + targetKickPlayer.guild = guilds[0]; + + const guildLeaveConfirmation = GuildLeaveConfirmationPacket(false); + targetKickPlayer.map.registerIndividualNetworkPacket(playerID, guildLeaveConfirmation); + + const guildMemberOld = { playerID }; + const guildMemberOldPacket = GuildMemberOldPacket(guildMemberOld); + targetKickPlayer.map.registerNetworkPacket(playerID, guildMemberOldPacket); +}; \ No newline at end of file diff --git a/Messages/GuildSearchMessage.js b/Messages/GuildSearchMessage.js new file mode 100644 index 0000000..1b85bfa --- /dev/null +++ b/Messages/GuildSearchMessage.js @@ -0,0 +1,70 @@ +const GuildSearchResultPacket = require("../Packets/GuildSearchResult"); + +const queries = require("../Data/queries"); +const { QUERY_DELETE_GUILD_MEMBER } = queries; + +const constants = require("../Data/constants"); +const { GUILD_PERMISSIONS } = constants; +const { GUILD_PERMISSION_KICK } = GUILD_PERMISSIONS; + +const Game = require("../Game/Game"); +const { players, guilds } = Game; + +const dec = new TextDecoder(); + +module.exports = async function (ws, message, player) { + const guildTagLength = message.getUint8(1); + const guildNameLength = message.getUint8(1 + guildTagLength + 1); + if (!guildTagLength && !guildNameLength) return; + + const guildTagView = new DataView(message.buffer, 2, guildTagLength); + const guildTag = dec.decode(guildTagView); + + const guildNameView = new DataView(message.buffer, 2 + guildTagLength + 1, guildNameLength); + const guildName = dec.decode(guildNameView); + + const guildSearchResult = await execute("SELECT guilds.id, guilds.tag, guilds.name, d.size AS size, CASE WHEN guildrequests.playerID IS NULL THEN 0 ELSE 1 END AS hasApplied FROM guilds LEFT JOIN guildrequests ON guilds.id = guildrequests.guildID AND guildrequests.playerID = ? JOIN (SELECT guildID, COUNT(*) AS size FROM guildmembers GROUP BY guildID) AS d ON guilds.id = d.guildID WHERE guilds.tag = ? OR guilds.name LIKE ?", [player.id, guildTag, + `%${guildName}%`]); + if (!guildSearchResult.data) return; + + const packets = []; + + const length = guildSearchResult.data.length; + if (!length) { + const guildSearchResult = { + id: 0, + tag: "", + name: "", + size: { + current: 0, + maximum: 0 + }, + hasApplied: false, + isLastEntry: true + }; + + const guildSearchResultPacket = GuildSearchResultPacket(guildSearchResult); + packets.push(guildSearchResultPacket); + } else { + guildSearchResult.data.forEach((g, i) => { + const guild = guilds[g.id]; + const sizeMaximum = guild ? guild.sizeMaximum : 0; + + const guildSearchResult = { + id: g.id, + tag: g.tag, + name: g.name, + size: { + current: g.size, + maximum: sizeMaximum + }, + hasApplied: g.hasApplied, + isLastEntry: i === length - 1 + }; + + const guildSearchResultPacket = GuildSearchResultPacket(guildSearchResult); + packets.push(guildSearchResultPacket); + }); + }; + + player.map.registerIndividualNetworkPacket(player.id, packets); +}; \ No newline at end of file diff --git a/Messages/ItemBuyMessage.js b/Messages/ItemBuyMessage.js new file mode 100644 index 0000000..5941094 --- /dev/null +++ b/Messages/ItemBuyMessage.js @@ -0,0 +1,75 @@ +const ItemBoughtPacket = require("../Packets/ItemBought"); +const packet_57 = require("../Packets/packet_57"); + +const BASE_SHOP = require("../Data/shop"); +const { SHOP, SHOP_ENTRIES_BUY } = BASE_SHOP; + +module.exports = async function (ws, message, player) { + const categoryID = message.getUint8(1); + const itemID = message.getUint8(2); + const rawAmount = message.getUint32(3); + + let bought = { + categoryID, + id: itemID, + amount: 0 + }; + + const itemToBuy = SHOP.find(entry => entry.categoryID === categoryID && entry.itemID === itemID); + console.info(categoryID, itemID, SHOP) + if (!itemToBuy) { + const boughtItemConfirmationPacket = ItemBoughtPacket(bought); + return player.map.registerIndividualNetworkPacket(player.id, boughtItemConfirmationPacket); + }; + + const isItemAvailable = itemToBuy.isAvailable(player); + if (!isItemAvailable) return; + + if (SHOP_ENTRIES_BUY[player.id] && SHOP_ENTRIES_BUY[player.id]?.[categoryID]?.[itemID] >= itemToBuy.maximumPlayerAmount) { + bought = { + categoryID, + id: itemID, + amount: 0 + }; + + const boughtItemConfirmationPacket = ItemBoughtPacket(bought); + return player.map.registerIndividualNetworkPacket(player.id, boughtItemConfirmationPacket); + }; + + const amount = rawAmount; // Math.min(rawAmount, (itemToBuy.maximumPlayerAmount || Infinity) - alreadyBoughtAmount) + bought.amount = amount; + + player.rewardCallback({ + isRegular: true, + authorID: player.id, + source: {}, + rewards: [{ + categoryID, + id: itemID, + amount + }] + }); + + player.inventory.refreshSingle({ + categoryID: 1, + id: itemToBuy.currencyType, + amount: itemToBuy.currencyAmount * amount * -1 + }); + + const packets = []; + const boughtItemConfirmationPacket = ItemBoughtPacket(bought); + packets.push(boughtItemConfirmationPacket); + + console.info("bought", bought); + + if (false && alreadyBoughtAmount + amount >= itemToBuy.maximumPlayerAmount) { + const removeShopEntryPacket = packet_57({ + id: itemID, + shopCategoryID: itemToBuy.shopCategoryID + }); + + packets.push(removeShopEntryPacket); + }; + + player.map.registerIndividualNetworkPacket(player.id, packets); +}; \ No newline at end of file diff --git a/Messages/ItemUseMessage.js b/Messages/ItemUseMessage.js new file mode 100644 index 0000000..6dcfd81 --- /dev/null +++ b/Messages/ItemUseMessage.js @@ -0,0 +1,113 @@ +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const HealthChangedPacket = require("../Packets/HealthChanged"); +const ItemUsedPacket = require("../Packets/ItemUsed"); + +const itemTable = require("../Data/items"); +const constants = require("../Data/constants"); + +const { CATEGORIES, AFFECTS, TYPES } = constants; +const { CATEGORY_ITEM } = CATEGORIES; +const { AFFECT_HITPOINT, AFFECT_SAILING_SPEED } = AFFECTS; +const { ENTITY_TYPE_PLAYER } = TYPES; + +module.exports = function (ws, message, player) { + const itemID = message.getUint8(6); + + const itemScheme = itemTable[itemID]; + if (!itemScheme) return; + + if (!player.isAlive) return; + + const targetTypeID = message.getUint8(1); + const targetID = message.getUint32(2); + if (targetTypeID === player.typeID && targetID === player.id) return; // ??? + + const inventoryItem = player.inventory.getItem(itemID); + const resolution = { + statusID: itemScheme.statusID, + itemID, + amountToRemove: 0, + isActive: !inventoryItem.isActive + }; + + if (itemScheme.statusID && itemScheme.isDurable) { + const index = player.activeStatusEffects.findIndex(id => id === itemScheme.statusID); + if (index === -1) player.activeStatusEffects.push(itemScheme.statusID); + else player.activeStatusEffects.splice(index, 1); + }; + + const metadata = [player.hp.current / player.hp.maximum, player.isAttacking(), itemID, inventoryItem.isActive ? 0 : 1]; + + if (inventoryItem.isActive) { + const packet = ItemUsedPacket(resolution); + player.map.registerIndividualNetworkPacket(player.id, packet); + + player.inventory.disable(itemID); + + return metadata; + }; + + const aliveTarget = itemScheme.isSelf ? player : player.map.getAliveEntityFromSubgroup(targetTypeID, targetID); + const amount = 1; + + if (!itemScheme.isSelf && !aliveTarget) return; + + const targetMapMode = itemScheme.isSelf ? player.map.type : aliveTarget.map.type; + if (itemScheme.allowedTargetTypes && !itemScheme.allowedTargetTypes[targetMapMode].includes(targetTypeID)) return; + + if (inventoryItem.nextUseAt > performance.now()) return; + + const isInRange = !itemScheme.isSelf && Math.pow(aliveTarget.position.x - player.position.x, 2) + Math.pow(aliveTarget.position.y - player.position.y, 2) < Math.pow(itemScheme.range, 2); + if (itemScheme.range && !isInRange) return; + if (inventoryItem.affects.includes(AFFECT_HITPOINT) && itemScheme.scale > 0 && aliveTarget.hp.maximum === aliveTarget.hp.current) return; + + const item = inventoryItem.activate(amount); + if (!item) return; + + resolution.amountToRemove = item.removedAmount; + + const packet = ItemUsedPacket(resolution); + player.map.registerIndividualNetworkPacket(player.id, packet); + + if (!itemScheme.isSelf) { + const attackInitialized = { + authorID: player.id, + authorTypeID: player.typeID + }; + + const attackInitializedPacket = AttackerInformationPacket(attackInitialized); + player.map.registerIndividualNetworkPacket(aliveTarget.id, attackInitializedPacket); + }; + + item.affects.forEach(affects => { + switch (affects) { + case AFFECT_HITPOINT: + const healthUpdate = item.scale > 0 ? + aliveTarget.increaseCurrentHitpoint(item.scale) : + aliveTarget.decreaseCurrentHitpoint(item.scale, { authorSessionID: player.sessionID, authorGuildID: player.guild.id, authorID: player.id, authorTypeID: player.typeID }); + + if (aliveTarget.typeID === ENTITY_TYPE_PLAYER && aliveTarget.isAtMaximumHitpoints()) aliveTarget.cancelRepair(); + + const data = { + authorTypeID: player.typeID, + authorID: player.id, + targetTypeID: aliveTarget.typeID, + targetID: aliveTarget.id, + amount: healthUpdate.normalizedAmount, + isCritical: false, + type: item.scale > 0 + }; + + const packet = HealthChangedPacket(data); + player.map.registerNetworkPacket(player.id, packet); + break; + + case AFFECT_SAILING_SPEED: + aliveTarget.changeSpeed(item.scale); + aliveTarget.setReusableSingleEvent(itemScheme.iid, item.time, -item.scale); + break; + }; + }); + + return metadata; +}; diff --git a/Messages/LeaderboardDataMessage.js b/Messages/LeaderboardDataMessage.js new file mode 100644 index 0000000..d7f0c9a --- /dev/null +++ b/Messages/LeaderboardDataMessage.js @@ -0,0 +1,25 @@ +const LeaderboardPositionsPacket = require("../Packets/LeaderboardPositions"); + +const leaderboards = require("../Data/leaderboard"); + +module.exports = async function (ws, message, player) { + const leaderboardPositionsData = leaderboards.map(async (leaderboard, i) => { + const leaderboardPositionFromDB = await execute(`SELECT 1 + COUNT(*) AS position FROM leaderboard_${i + 1} WHERE amount > COALESCE((SELECT amount FROM leaderboard_${i + 1} WHERE id = ?), 0)`, [player.id]); + if (!leaderboardPositionFromDB.data) + return { + selfPosition: 0, + totalPositionsLength: leaderboard.size + }; + + const selfPosition = leaderboardPositionFromDB.data[0].position; + + return { + selfPosition: selfPosition > leaderboard.size ? 0 : selfPosition, + totalPositionsLength: leaderboard.size + }; + }); + + const leaderboardPositions = await Promise.all(leaderboardPositionsData); + const leaderboardPositionsPacket = LeaderboardPositionsPacket(leaderboardPositions); + player.map.registerIndividualNetworkPacket(player.id, leaderboardPositionsPacket); +}; \ No newline at end of file diff --git a/Messages/LeaderboardPageMessage.js b/Messages/LeaderboardPageMessage.js new file mode 100644 index 0000000..7dbd072 --- /dev/null +++ b/Messages/LeaderboardPageMessage.js @@ -0,0 +1,42 @@ +const LeaderboardPagePacket = require("../Packets/LeaderboardPage"); + +const leaderboards = require("../Data/leaderboard"); + +module.exports = async function (ws, message, player) { + const leaderboardID = message.getUint8(1); + const leaderboard = leaderboards[leaderboardID]; + if (!leaderboard) return console.time("leaderboard 1"); + + const page = message.getUint16(2); + + if (!leaderboard.size) { + const leaderboardCountFromDB = await execute(`SELECT COUNT(*) AS count FROM leaderboard_${leaderboardID + 1}`); + if (!leaderboardCountFromDB.data) return console.info("leaderboard 2"); + + leaderboard.size = leaderboardCountFromDB.data[0].count; + }; + + const lastPage = Math.ceil(leaderboard.size / 10); + if (page <= 0 || page > lastPage) return; + + let entries = null; + + const startingFrom = (page - 1) * 10; + const cachedEntries = leaderboard.cache[page]; + if (cachedEntries) entries = cachedEntries; + else { + const leaderboardFromDB = await query(`SELECT accounts.id, accounts.username, leaderboard_${leaderboardID + 1}.amount FROM leaderboard_${leaderboardID + 1} INNER JOIN accounts ON accounts.id = leaderboard_${leaderboardID + 1}.id ORDER BY amount DESC LIMIT ?, 10`, [startingFrom]) + if (!leaderboardFromDB.data) return console.info("leaderboard 3"); + + entries = leaderboardFromDB.data + leaderboard.cache[page] = entries; + }; + + const leaderboardPacket = LeaderboardPagePacket.init(leaderboardID, entries.length); + entries.forEach(e => leaderboardPacket.update({ + username: e.username, + amount: e.amount + })); + + player.map.registerIndividualNetworkPacket(player.id, leaderboardPacket.get()); +}; \ No newline at end of file diff --git a/Messages/MapChangeMessage.js b/Messages/MapChangeMessage.js new file mode 100644 index 0000000..87e47c1 --- /dev/null +++ b/Messages/MapChangeMessage.js @@ -0,0 +1,3 @@ +module.exports = function (ws, message, player) { + player.setSingleEvent(15, 0); +}; \ No newline at end of file diff --git a/Messages/MovementMessage.js b/Messages/MovementMessage.js new file mode 100644 index 0000000..a891751 --- /dev/null +++ b/Messages/MovementMessage.js @@ -0,0 +1,44 @@ +const PlayerWorker = global.PlayerWorker; + +module.exports = function (ws, message, player) { + if (!player.isAlive) return; + + const worldX = message.getUint16(1); + const worldY = message.getUint16(3); + + const destination = player.map.grid.getCell(worldX, worldY); + if (!player.map.grid.isValidNode(destination)) return + + const { xw, xh } = player.previousDestination; + if (destination.xw === xw && destination.xh === xh) return; + + const position = player.isMoving ? { x: player.nextNode[0], y: player.nextNode[1] } : player.position; + const { x, y } = position; + const currentPosition = player.map.grid.getCell(x, y); + if (currentPosition.xw === destination.xw && currentPosition.xh === destination.xh) return; + + player.previousDestination = destination; + + if (player.isMoving) { + player.intermediateMovementWasStarted = true; + player.isNewPathReady = false; + player.isLastNodeFinished = false; + player.scheduledPath = []; + }; + + player.cancelDisconnect(); + + if (!player.movementRequestTracker) player.movementRequestTracker = 1; + else player.movementRequestTracker++; + + PlayerWorker.postMessage({ + destination, + position, + id: player.id, + mapId: player.map.id, + wasMoving: player.isMoving, + movementRequestTracker: player.movementRequestTracker + }); + + return [x, y, worldX, worldY, player.hp.current / player.hp.maximum, player.attackers.length, player.isAttacking()]; +}; diff --git a/Messages/QuestMessage.js b/Messages/QuestMessage.js new file mode 100644 index 0000000..f00a6d9 --- /dev/null +++ b/Messages/QuestMessage.js @@ -0,0 +1,42 @@ +const QuestRemovalPacket = require("../Packets/QuestRemoval"); +const QuestAddedPacket = require("../Packets/QuestAdded"); + +const quests = require("../Data/quests"); + +module.exports = async function (ws, message, player) { + const questID = message.getUint16(1); + const questScheme = quests[questID - 1]; + if (!questScheme) return; + + const isLevelInRange = questScheme.minimumLevel <= player.pirateLevel && questScheme.maximumLevel >= player.pirateLevel; + // if (!isLevelInRange) return console.info("in range"); + + if (player.quests.has(questID)) { + const questDisableQuery = await execute("INSERT INTO quests (playerID, questID) VALUES (?, ?) ON DUPLICATE KEY UPDATE state = ?", [player.id, questID, 1]); + if (!questDisableQuery.data) return; + + const questRemoval = { + questID, + isSuccessful: false + }; + + const questRemovalPacket = QuestRemovalPacket(questRemoval); + player.map.registerIndividualNetworkPacket(player.id, questRemovalPacket); + console.info("quest removed") + return player.quests.remove(questID); + }; + + const quest = player.quests.add(questID); + if (!quest) return; + + const questEnableQuery = await execute("INSERT INTO quests (playerID, questID) VALUES (?, ?) ON DUPLICATE KEY UPDATE state = ?", [player.id, questID, 0]); + if (!questEnableQuery.data) return; + + const questAdded = { + questID + }; + + const questAddedPacket = QuestAddedPacket(questAdded); + player.map.registerIndividualNetworkPacket(player.id, questAddedPacket); + console.info("quest added", questAdded); +}; \ No newline at end of file diff --git a/Messages/ReadyMessage.js b/Messages/ReadyMessage.js new file mode 100644 index 0000000..40fa61c --- /dev/null +++ b/Messages/ReadyMessage.js @@ -0,0 +1,242 @@ +const MainPlayerPacket = require("../Packets/MainPlayer"); +const ObtainedDesignsPacket = require("../Packets/ObtainedDesigns"); +const PropertyChangedPacket = require("../Packets/PropertyChanged"); +const AttackInformationPacket = require("../Packets/AttackInformation"); +const AttackerInformationPacket = require("../Packets/AttackerInformation"); +const QuestsPacket = require("../Packets/Quests"); +const QuestsCompletedAmountPacket = require("../Packets/QuestsCompletedAmount"); +const CannonballsPacket = require("../Packets/Cannonballs"); +const HarpoonsPacket = require("../Packets/Harpoons"); +const ItemsPacket = require("../Packets/Items"); +const EquipmentCapacitiesMaximumPacket = require("../Packets/EquipmentCapacitiesMaximum"); +const StatusPointsPacket = require("../Packets/StatusPoints"); +const EconomyPacket = require("../Packets/Economy"); +const ActiveStatusEffectsPacket = require("../Packets/ActiveStatusEffects"); +const EquipmentsPacket = require("../Packets/Equipments"); +const GuildMemberNewPacket = require("../Packets/GuildMemberNew"); +const WorldMapGuildEntryPacket = require("../Packets/WorldMapGuildEntry"); +const RankThresholdPointsPacket = require("../Packets/RankThresholdPoints"); +const RankPointPacket = require("../Packets/RankPoint"); +const GuildDataMemberChangedPacket = require("../Packets/GuildDataMemberChanged"); +const ShopEntryPacket = require("../Packets/ShopEntry"); + +const Entity = require("../Game/Entity"); + +const Game = require("../Game/Game"); +const { guildIslands } = Game; + +const packet_58 = require("../Packets/packet_58"); +const packet_57 = require("../Packets/packet_57"); + +const BASE_SHOP = require("../Data/shop"); +const { SHOP, SHOP_ENTRIES_BUY } = BASE_SHOP; + +const leaderboards = require("../Data/leaderboard"); + +module.exports = async function (ws, message, player) { + if (player.isAvailable) return; + + player.hasUnexpectedlyLeft = false; + + const connectingPlayer = player.getProperties(); + const connectingPlayerPacket = MainPlayerPacket(connectingPlayer); + player.map.registerIndividualNetworkPacket(player.id, connectingPlayerPacket); + + const designIDs = player.inventory.getDesignIDs(); + const designsPacket = ObtainedDesignsPacket(designIDs); + + const questPacketInitialized = QuestsPacket.init(); + Object.values(player.quests.list).forEach(quest => { + const questPacket = { + id: quest.id, + progression: quest.getProgression(), + currentComplete: quest.currentComplete + }; + + questPacketInitialized.update(questPacket); + }); + + const sightPacket = PropertyChangedPacket({ + typeID: 1, + value: player.sight / 12 + }); + + const questsCompletedAmountPacket = QuestsCompletedAmountPacket(player.quests.completedQuestsAmount); + + const ammunitionPacket = CannonballsPacket({ + ammunition: player.inventory.getAmmunitions(), + selected: player.inventory.selectedAmmunition + }); + + const harpoonPacket = HarpoonsPacket({ + harpoon: player.inventory.getHarpoons(), + selected: player.inventory.selectedHarpoon + }); + + const items = player.inventory.getItems(); + const itemPacket = ItemsPacket(items); + + const resourceSharedLimiter = player.inventory.resourceSharedLimiter; + const maximumEquipmentCapacities = [resourceSharedLimiter.cannon.quantity.maximum, resourceSharedLimiter.harpooner.quantity.maximum, resourceSharedLimiter.sail.quantity.maximum, resourceSharedLimiter.pirate.quantity.maximum]; + const propertiesPacket = EquipmentCapacitiesMaximumPacket(maximumEquipmentCapacities); + + const statusPointsPacket = StatusPointsPacket({ statusPoints: player.statusPoints }); + + const economy = player.inventory.getEconomy(); + const currencyPacket = EconomyPacket(economy); + + const activeStatusEffectsPacket = ActiveStatusEffectsPacket(player.activeStatusEffects); + +/* + const asd = await gameRedis.keys("*"); + asd.forEach(key => { + const data = gameRedis.getAsync(key); + + });*/ + + /* + const playerIrrelevantEntries = !global.unavailablePlayerShopEntries[player.id] ? [] : Object.keys(global.unavailablePlayerShopEntries[player.id]); + const globalIrrelevantEntries = Object.keys(global.unavailableGlobalShopEntries); + const irrelevantShopEntries = []; + const shopEntryIDTracker = {}; + + playerIrrelevantEntries.forEach(id => { + if (!shopEntryIDTracker[id]) irrelevantShopEntries.push(id); + shopEntryIDTracker[id] = true; + }); + + globalIrrelevantEntries.forEach(id => { + if (!shopEntryIDTracker[id]) irrelevantShopEntries.push(id); + shopEntryIDTracker[id] = true; + }); + */ + const irrelevantShopEntriesPacket = packet_58([]); + + const cannons = player.inventory.getCannons(); + const harpooners = player.inventory.getHarpooners(); + + const equipmentsPacket = EquipmentsPacket.init(cannons.length + harpooners.length); + cannons.forEach(c => equipmentsPacket.update(c.getProperties())); + harpooners.forEach(h => equipmentsPacket.update(h.getProperties())); + + const packets = [statusPointsPacket, designsPacket, sightPacket, equipmentsPacket.get(), questsCompletedAmountPacket, questPacketInitialized.get(), itemPacket, ammunitionPacket, harpoonPacket, propertiesPacket, currencyPacket, activeStatusEffectsPacket, irrelevantShopEntriesPacket]; + + /* + const currentDate = new Date(); + const currentMonth = currentDate.getMonth() + 1; + const currentDay = currentDate.getDay() - 1; + + const currentLoginReward = loginReward[currentMonth][currentDay]; + if (currentLoginReward) { + const loginReward = new Reward({ + distributionType: 2, + baseReward: currentLoginReward + }); + + setTimeout(() => loginReward.single(1, player.inventory, rewards => rewardCallback({ isRegular: true, authorID: player.id, rewards }))); + }; +*/ + + if (player.guild.id) { + const guildMemberNew = { + playerID: player.id, + tag: player.guild.tag + }; + + const guildMemberNewPacket = GuildMemberNewPacket(guildMemberNew); + packets.push(guildMemberNewPacket); + + const guildDataMemberChanged = { + id: player.id, + isOnline: true + }; + + const guildDataMemberChangedPacket = GuildDataMemberChangedPacket(guildDataMemberChanged); + const topic = Game.getTopic(player.guild.topic); + + topic.push(guildDataMemberChangedPacket); + }; + +/* const mapped = { + 5: 2 + }; + + for (categoryID in SHOP) { + const category = SHOP[categoryID]; + + for (itemID in category) { + const entry = category[itemID]; + const isEntryAvailable = entry.isAvailable(player); + if (!isEntryAvailable) continue; + + const irrelevantShopEntry = { + id: itemID, + shopCategoryID: mapped[categoryID] + }; + + const packet = packet_57(irrelevantShopEntry); + packets.push(packet); + }; + };*/ + + player.ready(); + + if (player.isAlive) { + if (player.target) { + const attackInformationPacket = AttackInformationPacket({ + authorTypeID: player.target.typeID, + authorID: player.target.id + }); + + const attackerPacket = AttackerInformationPacket({ + authorTypeID: player.target.typeID, + authorID: player.target.id, + isTarget: true, + source: 1 + }); + + packets.push(attackInformationPacket, attackerPacket); + }; + + player.attackers.forEach(attacker => { + const attackerPacket = AttackerInformationPacket({ + authorTypeID: Entity.getType(attacker.attackerEntry) + 1, + authorID: attacker.attackerEntry, + isTarget: false, + source: 1 + }); + + packets.push(attackerPacket); + }); + }; + + guildIslands.forEach(g => { + const worldMapGuildEntryPacketData = { + locationMapID: g.locationMapID, + index: g.index, + bufferTag: g.guild.tag + }; + console.info("bufferTag", g.guild.tag) + const worldMapGuildEntryPacket = WorldMapGuildEntryPacket(worldMapGuildEntryPacketData); + packets.push(worldMapGuildEntryPacket); + }); + + for (let i = 0, length = player.rankingPoints.length; i < length; i++) { + const { id, amount } = player.rankingPoints[i]; + + const rankPoint = { + rankTypeID: id, + value: amount + }; + + const rankPointPacket = RankPointPacket(rankPoint); + packets.push(rankPointPacket); + }; + + SHOP.forEach(shopEntry => { + const shopEntryPacket = ShopEntryPacket(shopEntry); + packets.push(shopEntryPacket); + }); + + player.map.registerIndividualNetworkPacket(player.id, packets); +}; \ No newline at end of file diff --git a/Messages/RepairMessage.js b/Messages/RepairMessage.js new file mode 100644 index 0000000..f1aad3e --- /dev/null +++ b/Messages/RepairMessage.js @@ -0,0 +1,20 @@ +const packet_255 = require("../Packets/packet_255"); + +module.exports = function (ws, message, player) { + let isRepairSuccessful = null; + + if (player.isRepairing()) { + player.cancelRepair(); + isRepairSuccessful = false; + } else { + player.initializeRepair(); + isRepairSuccessful = true; + }; + + const packet = packet_255({ packetId: 14, isSuccessful: isRepairSuccessful }); + ws.send(packet, true, true); // should use central packet distributor + + const metadata = [player.hp.current / player.hp.maximum, player.isAttacking()]; + + return metadata; +}; \ No newline at end of file diff --git a/Messages/RespawnMessage.js b/Messages/RespawnMessage.js new file mode 100644 index 0000000..e16ce39 --- /dev/null +++ b/Messages/RespawnMessage.js @@ -0,0 +1,5 @@ +module.exports = async function (ws, message, player) { + if (player.isAlive) return; + + player.setSingleEvent(14, 0); +}; diff --git a/Messages/SpyMessage.js b/Messages/SpyMessage.js new file mode 100644 index 0000000..4c9f645 --- /dev/null +++ b/Messages/SpyMessage.js @@ -0,0 +1,34 @@ +const game = require("../Game/Game"); +const { players } = game; + +const SpyResultPacket = require("../Packets/SpyResult"); + +module.exports = function (ws, message, player) { + const spyID = message.getUint32(1); + const spiedPlayer = players[spyID]; + + let spiedPlayerLocation = {}; + + if (!spiedPlayer) { + spiedPlayerLocation = { + mapID: 255, + x: 0, + y: 0 + }; + } else if (spiedPlayer.isAlive) { + spiedPlayerLocation = { + mapID: spiedPlayer.map.id, + x: spiedPlayer.position.x, + y: spiedPlayer.position.y + }; + } else { + spiedPlayerLocation = { + mapID: 0, + x: 0, + y: 0 + }; + }; + + const packet = SpyResultPacket(spiedPlayerLocation); + ws.send(packet, true, true); +}; \ No newline at end of file diff --git a/Messages/TopicInterestedMessage.js b/Messages/TopicInterestedMessage.js new file mode 100644 index 0000000..f80baf1 --- /dev/null +++ b/Messages/TopicInterestedMessage.js @@ -0,0 +1,13 @@ +const game = require("../Game/Game"); +const { players, packets } = game; + +module.exports = function (ws, message, player) { + console.info(packets) + const topic = packets.getElement("test"); + if (!topic) return console.info("no topic"); + + if (!player.topics) player.topics = [topic]; + else player.topics.push(topic); + + console.info("player topics", player.topics); +}; \ No newline at end of file diff --git a/Messages/_.js b/Messages/_.js new file mode 100644 index 0000000..1f689ea --- /dev/null +++ b/Messages/_.js @@ -0,0 +1,44 @@ +const Game = require("../Game/Game"); +const { players, guilds } = Game; +const buffer = new ArrayBuffer(3 + 250); +const view = new DataView(buffer); + +const MAXIMUM_GUILD_REQUEST_MESSAGE_LENGTH = 256; +view.setUint8(0, 10); +view.setUint16(1, 213); + +const QUERY_GUILD_REQUEST_SUBMIT = "INSERT INTO guildrequests (guildID, playerID, message) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE message = ?"; +const QUERY_GUILD_REQUEST_SUBMIT_DEFAULT = "INSERT INTO guildrequests (guildID, playerID) VALUES (?, ?) ON DUPLICATE KEY UPDATE message = ?"; + +let str = new Array(250).fill("a").join(""); +for (let i = 0, strLen = str.length; i < strLen; i++) { + const charCode = str.charCodeAt(i); + view.setUint8(3 + i, charCode); +}; + +module.exports = function (ws, message, player) { + const guildID = view.getUint16(1); + const guild = guilds[guildID]; + if (!guild) return; + + const playerID = 45; // player.id + const requestingPlayer = players[playerID]; + // if (!requestingPlayer) return; + + // if (requestingPlayer.guild) return console.log("has guild, can't request"); + if (guild.hasRequested(playerID)) return console.log("has already requested"); + + const requestMessageLength = view.byteLength - 3; + console.log(requestMessageLength) + if (requestMessageLength > MAXIMUM_GUILD_REQUEST_MESSAGE_LENGTH) return console.log("request message length is > 256"); + + const requestMessage = view.byteLength > 3 ? view.buffer.slice(3, view.byteLength) : null; + const isRequestSuccessful = guild.addRequest(playerID, requestMessage); + if (!isRequestSuccessful) return console.log("request was unsuccessful"); + + const query = view.byteLength > 3 ? QUERY_GUILD_REQUEST_SUBMIT : QUERY_GUILD_REQUEST_SUBMIT_DEFAULT; + const queryParameters = view.byteLength > 3 ? [guildID, playerID, requestMessage, requestMessage] : [guildID, playerID, requestMessage]; + console.log(requestMessage) + + db.execute(query, queryParameters).then(_ => console.log("guild request is in the db")); +}; \ No newline at end of file diff --git a/Messages/message.js b/Messages/message.js new file mode 100644 index 0000000..50aef0b --- /dev/null +++ b/Messages/message.js @@ -0,0 +1,94 @@ +const packetTable = require("../Data/packets"); +const { players } = require("../Game/Game"); + +const actions = []; +global.actions = actions; + +const { MongoClient } = require("mongodb"); +let db = null; + +async function connect() { + const client = new MongoClient("mongodb://localhost:27017"); + await client.connect({ + maxPoolSize: 100, + minPoolSize: 20, + maxConnecting: 20, + waitQueueTimeoutMS: 0, + socketTimeoutMS: 30000, + connectTimeoutMS: 10000, + retryWrites: false, + directConnection: true + }); + + return client.db('test'); +}; + +(async function() { + db = await connect(); + global.mongodb = db; + + return; + setInterval(async () => { + const t = []; + for (let i = 0; i < 32; i++) { + t.push({ + playerID: 1, + timestamp: new Date(), + opcode: Math.floor(Math.random() * 120) + }); + }; + + const bulkOps = t.map(doc => ({ + insertOne: doc + })); + + const n = performance.now(); + await db.collection("Actions").insertMany(t, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + /* await db.collection('Actions').bulkWrite(bulkOps, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + });*/ + }, 500); +})(); + +module.exports = async function (ws, message, isBinary) { + const player = players[ws.accountID]; + if (!player) return; + + const isLimitReached = ws.limiter.update(); + if (isLimitReached) return ws.close(); + + if (player.isInactive) return; // this shouldn't happen, though + + const view = new DataView(message); + const packetID = view.getUint8(0); + const packet = packetTable[packetID]; + + if (packetID !== 1 && !player.isAvailable) return; + if (!packet) return; + if (packet.expectedSize && packet.expectedSize !== view.byteLength) return console.info("expectedSize", view.byteLength, packet.expectedSize); + if (!packet.isAvailable) return; + + player.lastInteractionTimestamp = performance.now(); + + const metadata = packet.handler(ws, view, player) ?? []; + + if (packetID > 1) { + const action = { + ts: new Date(), + pid: player.id, + // opcode: packetID, + // metadata + }; + + const relevantContext = global.actionContext.getElement(player.id).save(2000); + relevantContext.push(action); + console.info("asd", relevantContext.length) + }; +}; \ No newline at end of file diff --git a/Miscellaneous/mysql.js b/Miscellaneous/mysql.js new file mode 100644 index 0000000..f1defe8 --- /dev/null +++ b/Miscellaneous/mysql.js @@ -0,0 +1,54 @@ +const logger = require("./winston"); +const mysql = require("mysql2/promise"); +require("dotenv").config({ path: ".env" }); + +db = mysql.createPool({ + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + enableKeepAlive: true, + waitForConnections: true, + connectionLimit: 20, + queueLimit: 0 +}); + +async function query(sql, params = []) { + try { + const [rows] = await db.query(sql, params); + + return { + success: true, + data: rows + }; + } catch (error) { + logger.error(error); + + return { + success: false, + error + }; + }; +}; + +async function execute(sql, params = []) { + try { + const [rows] = await db.execute(sql, params); + + return { + success: true, + data: rows + }; + } catch (error) { + logger.error(error); + + return { + success: false, + error + }; + }; +}; + +global.query = query; +global.execute = execute; \ No newline at end of file diff --git a/Miscellaneous/redis.js b/Miscellaneous/redis.js new file mode 100644 index 0000000..f82b896 --- /dev/null +++ b/Miscellaneous/redis.js @@ -0,0 +1,7 @@ +require("dotenv").config({ path: ".env" }); + +const { createClient } = require("redis"); +const redisClient = createClient({ host: "127.0.0.1", port: 6379, password: process.env.REDIS_PASSWORD }); +redisClient.connect().catch(e => console.log("Redis connection error!", e)); + +module.exports = redisClient; \ No newline at end of file diff --git a/Miscellaneous/stripe.js b/Miscellaneous/stripe.js new file mode 100644 index 0000000..08056c4 --- /dev/null +++ b/Miscellaneous/stripe.js @@ -0,0 +1,3 @@ +const stripe = require("stripe")(process.env.STRIPE_SECRET); + +module.exports = stripe; \ No newline at end of file diff --git a/Miscellaneous/winston.js b/Miscellaneous/winston.js new file mode 100644 index 0000000..cf6f684 --- /dev/null +++ b/Miscellaneous/winston.js @@ -0,0 +1,26 @@ +const winston = require("winston"); + +const prettyFormat = winston.format.printf(({ timestamp, level, message, error, ...meta }) => { + const logObject = { + timestamp, + level, + message, + ...(Object.keys(meta).length ? { meta } : {}), + }; + + return JSON.stringify(logObject, null, 2); +}); + +const logger = winston.createLogger({ + level: "info", + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + prettyFormat), + transports: [ + new winston.transports.Console(), + new winston.transports.File({ filename: "./Logs/error.log", level: "error" }), + ], +}); + +module.exports = logger; \ No newline at end of file diff --git a/Packets/ActiveStatusEffects.js b/Packets/ActiveStatusEffects.js new file mode 100644 index 0000000..c4d900f --- /dev/null +++ b/Packets/ActiveStatusEffects.js @@ -0,0 +1,19 @@ +/* + @Packet name ActiveStatusEffects + @Packet identifier 161 + @Description Sends which status effects are active + @Data pId, designId, playerId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + data.length; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 161); + view.setUint8(1, packetLength); + data.forEach((id, i) => view.setUint8(2 + i, id)); + + return buffer; +}; diff --git a/Packets/AdmiralSpawned.js b/Packets/AdmiralSpawned.js new file mode 100644 index 0000000..cf2fb13 --- /dev/null +++ b/Packets/AdmiralSpawned.js @@ -0,0 +1,23 @@ +/* + @Packet name AdmiralSpawned + @Packet identifier 145 + @Description Sends information about a spawned admiral + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 4 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { mapID, typeID, entityTypeID } = data; + + view.setUint8(0, 145); + view.setUint8(1, packetLength); + view.setUint8(2, mapID); + view.setUint8(3, typeID); + view.setUint8(4, entityTypeID); + + return buffer; +}; diff --git a/Packets/AmmunitionChangeConfirmation.js b/Packets/AmmunitionChangeConfirmation.js new file mode 100644 index 0000000..03df907 --- /dev/null +++ b/Packets/AmmunitionChangeConfirmation.js @@ -0,0 +1,20 @@ +/* + @Packet name AmmunitionChangeConfirmation + @Packet identifier 200 + @Description Ammunition change confirmation + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 200); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint8(3, data.id); + + return buffer; +}; diff --git a/Packets/AmmunitionUsed.js b/Packets/AmmunitionUsed.js new file mode 100644 index 0000000..3ea3781 --- /dev/null +++ b/Packets/AmmunitionUsed.js @@ -0,0 +1,20 @@ +/* + @Packet name AmmunitionUsed + @Packet identifier 135 + @Description unused + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 135); + view.setUint8(1, packetLength); + view.setUint8(2, data.amount); + view.setUint8(3, data.type); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/AttackAborted.js b/Packets/AttackAborted.js new file mode 100644 index 0000000..15f53f3 --- /dev/null +++ b/Packets/AttackAborted.js @@ -0,0 +1,22 @@ +/* + @Packet name AttackAborted + @Packet identifier 141 + @Description Sends the + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 11 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 141); + view.setUint8(1, packetLength); + view.setUint8(2, data.authorTypeID); + view.setUint32(3, data.authorID); + view.setUint8(7, data.targetTypeID); + view.setUint32(8, data.targetID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/AttackInformation.js b/Packets/AttackInformation.js new file mode 100644 index 0000000..2ad17e1 --- /dev/null +++ b/Packets/AttackInformation.js @@ -0,0 +1,20 @@ +/* + @Packet name AttackInformation + @Packet identifier 139 + @Description - + @Data pId, typeId, entityId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 139); + view.setUint8(1, packetLength); + view.setUint8(2, data.authorTypeID); + view.setUint32(3, data.authorID); + + return buffer; +}; diff --git a/Packets/AttackerInformation.js b/Packets/AttackerInformation.js new file mode 100644 index 0000000..266065c --- /dev/null +++ b/Packets/AttackerInformation.js @@ -0,0 +1,22 @@ +/* + @Packet name AttackerInformation + @Packet identifier 142 + @Description Sends the + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 8 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 142); + view.setUint8(1, packetLength); + view.setUint8(2, data.authorTypeID); + view.setUint32(3, data.authorID); + view.setUint8(7, data.isTarget); + view.setUint8(8, data.source) + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Attacking.js b/Packets/Attacking.js new file mode 100644 index 0000000..b8025e0 --- /dev/null +++ b/Packets/Attacking.js @@ -0,0 +1,25 @@ +/* + @Packet name Attacking + @Packet identifier 140 + @Description Sends the attack's data to the nearby players + @Data pId, authorTypeId, authorId, targetTypeId, targetId, damage, critical + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 17 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 140); + view.setUint8(1, packetLength); + view.setUint8(2, data.authorTypeID); + view.setUint32(3, data.authorID); + view.setUint8(7, data.targetTypeID); + view.setUint32(8, data.targetID); + view.setUint32(12, data.damage); + view.setUint8(16, +data.critical); + view.setUint8(17, data.entityTypeID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Cannonballs.js b/Packets/Cannonballs.js new file mode 100644 index 0000000..29e938d --- /dev/null +++ b/Packets/Cannonballs.js @@ -0,0 +1,30 @@ +/* + @Packet name Cannonballs + @Packet identifier 153 + @Description Sends the + @Data pId, (ammunitionId, damage, amount), selectedAmmunition + @Structure - +*/ + +module.exports = function (data) { + const { ammunition, selected } = data; + + const packetLength = ammunition.length * 6 + 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 153); + view.setUint8(1, packetLength); + + for (let i = 0; i < ammunition.length; i++) { + const ammo = ammunition[i]; + + view.setUint8(i * 6 + 2, ammo.id); + view.setUint8(i * 6 + 3, 0); + view.setUint32(i * 6 + 4, ammo.amount); + }; + + view.setUint8(2 + ammunition.length * 6, selected); + + return buffer; +}; diff --git a/Packets/CauldronMessage.js b/Packets/CauldronMessage.js new file mode 100644 index 0000000..2d4ecf4 --- /dev/null +++ b/Packets/CauldronMessage.js @@ -0,0 +1,40 @@ +const Reward = require("../Game/Reward"); +const rewardCallback = require("../Callbacks/reward"); + +const MAXIMUM_COUNT_MOJO = 1000; + +const cauldronReward = new Reward({ + distributionType: 2, + baseReward: { + 3: { + 5: { amountMinimum: 1500, chance: 16.5 }, + 6: { amountMinimum: 1500, chance: 16.5 }, + 7: { amountMinimum: 1500, chance: 16.5 }, + 8: { amountMinimum: 1500, chance: 16.5 }, + 9: { amountMinimum: 1500, chance: 16.5 }, + 10: { amountMinimum: 1500, chance: 16.5 }, + }/*, + 5: { + 17: { amount: 1, chance: 1 } + }*/ + } +}); + +module.exports = function (ws, message, player) { + let amount = message.getUint16(1); + if (!amount) return; + + if (amount > MAXIMUM_COUNT_MOJO) amount = MAXIMUM_COUNT_MOJO; + // const usedItem = player.inventory.use(1, 3, amount); + // const economy = player.inventory.getEconomy(); + const usedAmount = player.inventory.alterAmount(1, 3, amount); + if (!usedAmount) return console.log("not enough mojo"); + + console.log("mojo thrown | amount:", usedAmount); + + const playerInventory = player.inventory.getPosessions(); + cauldronReward.single(usedAmount, playerInventory, rewards => { + rewardCallback({ isRegular: false, death: { authorID: player.id }, rewards }) + console.info(rewards) + }); +}; \ No newline at end of file diff --git a/Packets/DesignChanged.js b/Packets/DesignChanged.js new file mode 100644 index 0000000..3805940 --- /dev/null +++ b/Packets/DesignChanged.js @@ -0,0 +1,23 @@ +/* + @Packet name DesignChanged + @Packet identifier 160 + @Description Includes data about which entity changed texture + @Data pId, designId, typeId, playerId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 8 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { designID, typeID, id } = data; + + view.setUint8(0, 160); + view.setUint8(1, packetLength); + view.setUint16(2, designID); + view.setUint8(4, typeID); + view.setUint32(5, id); + + return buffer; +}; diff --git a/Packets/Economy.js b/Packets/Economy.js new file mode 100644 index 0000000..79804fc --- /dev/null +++ b/Packets/Economy.js @@ -0,0 +1,24 @@ +/* + @Packet name Economy + @Packet identifier 158 + @Description Sends economy data + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + data.length * 5; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + for (let i = 0, length = data.length; i < length; i++) { + const economy = data[i]; + view.setUint8(2 + i * 5, economy.id); + view.setUint32(2 + i * 5 + 1, economy.amount); + }; + + view.setUint8(0, 158); + view.setUint8(1, packetLength); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EffectAdd.js b/Packets/EffectAdd.js new file mode 100644 index 0000000..43fbefa --- /dev/null +++ b/Packets/EffectAdd.js @@ -0,0 +1,23 @@ +/* + @Packet name EffectAdd + @Packet identifier 146 + @Description Sends + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { typeID, id, effectID } = data; + + view.setUint8(0, 146); + view.setUint8(1, packetLength); + view.setUint8(2, effectID) + view.setUint8(3, typeID); + view.setUint32(4, id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EntityDie.js b/Packets/EntityDie.js new file mode 100644 index 0000000..0ddfcc8 --- /dev/null +++ b/Packets/EntityDie.js @@ -0,0 +1,23 @@ +/* + @Packet name EntityDie + @Packet identifier 143 + @Description Sends the death information to the nearby players + @Data pId, deadEntityTypeId, entityId, lastShotEntityTypeID, lastShotId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 12 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 143); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint32(3, data.targetID); + view.setUint8(7, data.lastShotTypeID); + view.setUint32(8, data.lastShotID); + view.setUint8(12, data.targetEntityTypeID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EntityNew.js b/Packets/EntityNew.js new file mode 100644 index 0000000..e689985 --- /dev/null +++ b/Packets/EntityNew.js @@ -0,0 +1,214 @@ +/* + @Packet name EntityNew + @Packet identifier 134 + @Description Holds data about the entity that a player encountered + @Data - + @Structure - +*/ + +module.exports.player = function (data) { + const destinationX = data.currentNode[0]; + const destinationY = data.currentNode[1]; + + const curHP = data.hp.current; + const maxHP = data.hp.maximum; + const x = data.position.x; + const y = data.position.y; + const name = data.name; + const guild = data.guildTag; + const id = data.id; + const mapID = data.mapID; + const speed = data.speed; + const typeID = data.typeID; + const textureID = data.textureID; + const isMoving = data.isMoving; + const pirateLevel = data.pirateLevel; + const globalRank = data.globalRank; + const levelRank = data.levelRank; + + let packetLength = 28 + name.length + (guild ? guild.length : 0) + 1 + 4; + if (isMoving) packetLength += 4; + + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 134); + view.setUint8(1, packetLength); + view.setUint8(2, typeID); + view.setUint32(3, id); + view.setUint32(7, curHP); + view.setUint32(11, maxHP); + view.setUint16(15, x); + view.setUint16(17, y); + view.setUint16(19, speed); + view.setUint8(21, pirateLevel); + view.setUint8(22, mapID); + view.setUint8(23, textureID); + view.setUint8(24, isMoving); + view.setUint8(25, globalRank); + view.setUint8(26, levelRank); + view.setUint8(27, name.length); + view.setUint8(28, guild ? guild.length : 0); + + if (isMoving) { + view.setUint16(29, destinationX); + view.setUint16(31, destinationY); + }; + + const nameBuffer = Buffer.from(name); + for (const pair of nameBuffer.entries()) { + const index = pair[0]; + view.setUint8(isMoving ? index + 33 : index + 29, pair[1]); + }; + + if (guild) { + const guildBuffer = Buffer.from(guild); + for (const pair of guildBuffer.entries()) { + const index = pair[0]; + view.setUint8(isMoving ? index + 33 + name.length : index + 29 + name.length, pair[1]); + }; + + view.setUint32(isMoving ? guild.length + name.length + 33 : guild.length + name.length + 29, data.guildColor); + }; + + const metadata = [curHP / maxHP, x, y, typeID]; + + return { + buffer, + metadata + }; +}; + +module.exports.npc = function (data) { + const destinationX = data.path[0]; + const destinationY = data.path[1]; + + const { id, speed, size, typeID, entityTypeID, behaviourBits, activeEffectsBits, isAdmiral, isMoving, isSpeedLowered } = data; + + const packetLength = (isMoving ? 31 : 27) + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const curHP = data.hp.current; + const maxHP = data.hp.maximum; + const x = data.position.x; + const y = data.position.y; + + const intermediatePositionX = 0 + const intermediatePositionY = 0 + + view.setUint8(0, 134); + view.setUint8(1, packetLength); + view.setUint8(2, typeID) + view.setUint32(3, id); + view.setUint8(7, entityTypeID) + view.setUint32(8, curHP); + view.setUint32(12, maxHP); + view.setUint16(16, x); + view.setUint16(18, y); + view.setUint16(20, speed); + view.setUint8(22, activeEffectsBits); + view.setUint8(23, +isMoving); + view.setUint8(24, +isSpeedLowered); + view.setUint8(25, +isAdmiral); + view.setUint8(26, activeEffectsBits); + view.setUint8(27, size); + + if (isMoving) { + view.setUint16(28, destinationX); + view.setUint16(30, destinationY); + }; + + const metadata = [curHP / maxHP, x, y, typeID]; + + return { + buffer, + metadata + }; +}; + +module.exports.monster = function (data) { + const packetLength = 20 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { typeID, id, position, isAdmiral, entityTypeID, hp } = data; + + view.setUint8(0, 134); + view.setUint8(1, packetLength); + view.setUint8(2, typeID); + view.setUint32(3, id); + view.setUint8(7, entityTypeID); + view.setUint8(8, isAdmiral); + view.setUint32(9, hp.current); + view.setUint32(13, hp.maximum); + view.setUint16(17, position.x); + view.setUint16(19, position.y); + + const metadata = [hp.current / hp.maximum, position.x, position.y, typeID]; + + return { + buffer, + metadata + }; +}; + +module.exports.tower = function (data) { + const packetlength = (data.guildTag ? 23 : 20) + 1 + 4; + const buffer = new ArrayBuffer(packetlength); + const view = new DataView(buffer); + + const { hp, position, guildTag, id, entityTypeID, typeID, guildColor, order } = data; + + view.setUint8(0, 134); + view.setUint8(1, packetlength); + view.setUint8(2, typeID); + view.setUint32(3, id); + view.setUint16(7, position.x); + view.setUint16(9, position.y); + view.setUint32(11, hp.current); + view.setUint32(15, hp.maximum); + view.setUint8(19, entityTypeID); + view.setUint8(20, order); + + if (guildTag) { + const guildBuffer = Buffer.from(guildTag); + + for (const pair of guildBuffer.entries()) { + const index = pair[0]; + view.setUint8(21 + index, pair[1]); + }; + + view.setUint32(24, guildColor); + }; + + const metadata = [hp.current / hp.maximum, position.x, position.y, typeID]; + + return { + buffer, + metadata + }; +}; + +module.exports.collectable = function (data) { + const packetLength = 11 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { typeID, id, entityTypeID, position } = data; + + view.setUint8(0, 134); + view.setUint8(1, packetLength); + view.setUint8(2, typeID); + view.setUint32(3, id); + view.setUint8(7, entityTypeID); + view.setUint16(8, position.x); + view.setUint16(10, position.y); + + const metadata = [position.x, position.y, typeID]; + + return { + buffer, + metadata + }; +}; \ No newline at end of file diff --git a/Packets/EntityOld.js b/Packets/EntityOld.js new file mode 100644 index 0000000..0a52380 --- /dev/null +++ b/Packets/EntityOld.js @@ -0,0 +1,22 @@ +/* + @Packet name EntityOld + @Packet identifier 133 + @Description Sends the entity type and id to the player who left it + @Data pId, typeId, entityId + @Structure - +*/ + +module.exports = function (data) { + const { typeID, id } = data; + + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 133); + view.setUint8(1, packetLength); + view.setUint8(2, typeID); + view.setUint32(3, id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EntitySpeedChanged.js b/Packets/EntitySpeedChanged.js new file mode 100644 index 0000000..16f0cf8 --- /dev/null +++ b/Packets/EntitySpeedChanged.js @@ -0,0 +1,21 @@ +/* + @Packet name EntitySpeedChanged + @Packet identifier 150 + @Description Sends information about the new speed of NPC + @Data pId, mapId, guildName + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 150); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint32(3, data.id); + view.setUint16(7, data.speed); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EnvironmentSetup.js b/Packets/EnvironmentSetup.js new file mode 100644 index 0000000..9e7a60c --- /dev/null +++ b/Packets/EnvironmentSetup.js @@ -0,0 +1,17 @@ +/* + @Packet name ready + @Packet identifier 2 + @Description Ready event + @Data pId, mapId + @Structure - +*/ + +module.exports = function (data) { + const buffer = new ArrayBuffer(2); + const view = new DataView(buffer); + + view.setUint8(0, 2); + view.setUint8(1, data); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/EquipmentCapacitiesMaximum.js b/Packets/EquipmentCapacitiesMaximum.js new file mode 100644 index 0000000..756a21f --- /dev/null +++ b/Packets/EquipmentCapacitiesMaximum.js @@ -0,0 +1,22 @@ +/* + @Packet name EquiptmentCapacitiesMaximum + @Packet identifier 156 + @Description - + @Data pId, cannonSlots, harpoonerSlots, sailSlots, pirateSlots + @Structure - +*/ + +module.exports = function (data) { + const packetLength = data.length * 2 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 156); + view.setUint8(1, packetLength); + + data.forEach((e, i) => { + view.setUint16(2 + i * 2, e); + }); + + return buffer; +}; diff --git a/Packets/EquipmentConfirmation.js b/Packets/EquipmentConfirmation.js new file mode 100644 index 0000000..f325818 --- /dev/null +++ b/Packets/EquipmentConfirmation.js @@ -0,0 +1,19 @@ +/* + @Packet name EquipmentConfirmation + @Packet identifier 148 + @Description - + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 148); + view.setUint8(1, packetLength); + view.setUint8(2, data) + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Equipments.js b/Packets/Equipments.js new file mode 100644 index 0000000..011923b --- /dev/null +++ b/Packets/Equipments.js @@ -0,0 +1,36 @@ +/* + @Packet name Equipments + @Packet identifier 168 + @Description - + @Data pId + @Structure - +*/ + +module.exports = { + init: length => { + const packetLength = length * 4 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 168); + view.setUint8(1, packetLength); + + let i = 0; + + return { + update: d => { + const startIndex = i * 4; + + view.setUint8(2 + startIndex, d.typeID); + view.setUint8(2 + startIndex + 1, d.id); + view.setUint8(2 + startIndex + 2, d.amount); + view.setUint8(2 + startIndex + 3, d.amountEquipped); + + i++; + }, + get: () => { + return buffer; + } + }; + } +}; \ No newline at end of file diff --git a/Packets/GuildDataApplicant.js b/Packets/GuildDataApplicant.js new file mode 100644 index 0000000..81fd6bb --- /dev/null +++ b/Packets/GuildDataApplicant.js @@ -0,0 +1,27 @@ +/* + @Packet name GuildDataApplicant + @Packet identifier 191 + @Description Holds data about a single applicant + @Data - + @Structure - +*/ + +const encoder = new TextEncoder(); + +module.exports = function (data) { + const nameBuffer = encoder.encode(data.username); + + const packetLength = 8 + 1 + nameBuffer.byteLength; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 191); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + view.setUint16(6, data.pirateLevel); + + view.setUint8(8, nameBuffer.byteLength); + new Uint8Array(buffer, 9).set(nameBuffer); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataApplicantRemove.js b/Packets/GuildDataApplicantRemove.js new file mode 100644 index 0000000..ae4d993 --- /dev/null +++ b/Packets/GuildDataApplicantRemove.js @@ -0,0 +1,19 @@ +/* + @Packet name GuildDataApplicantRemove + @Packet identifier 192 + @Description Holds data about a single applicant who was removed + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 5 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 192); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataCreationFeedback.js b/Packets/GuildDataCreationFeedback.js new file mode 100644 index 0000000..f31e7af --- /dev/null +++ b/Packets/GuildDataCreationFeedback.js @@ -0,0 +1,19 @@ +/* + @Packet name GuildDataCreationFeedback + @Packet identifier 230 + @Description Holds data about feedback for guild creation + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 230); + view.setUint8(1, packetLength); + view.setUint8(2, data.reason); + + return buffer; +}; diff --git a/Packets/GuildDataEconomy.js b/Packets/GuildDataEconomy.js new file mode 100644 index 0000000..d70a6dd --- /dev/null +++ b/Packets/GuildDataEconomy.js @@ -0,0 +1,20 @@ +/* + @Packet name GuildDataEconomy + @Packet identifier 178 + @Description Includes data about guild currencies + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = data.length * 4 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 178); + view.setUint8(1, packetLength); + + data.forEach((d, i) => view.setUint32(2 + i * 4, d.amount)); + + return buffer; +}; diff --git a/Packets/GuildDataEconomyDonated.js b/Packets/GuildDataEconomyDonated.js new file mode 100644 index 0000000..44d0d32 --- /dev/null +++ b/Packets/GuildDataEconomyDonated.js @@ -0,0 +1,20 @@ +/* + @Packet name GuildDataEconomy + @Packet identifier 187 + @Description Includes confirmation data about donated guild economy + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 187); + view.setUint8(1, packetLength); + view.setUint8(2, data.id); + view.setUint32(3, data.amount); + + return buffer; +}; diff --git a/Packets/GuildDataGeneral.js b/Packets/GuildDataGeneral.js new file mode 100644 index 0000000..358a478 --- /dev/null +++ b/Packets/GuildDataGeneral.js @@ -0,0 +1,44 @@ +/* + @Packet name GuildDataGeneral + @Packet identifier 175 + @Description Includes data about general guild properties + @Data pId, + @Structure - +*/ + +const encoder = new TextEncoder(); + +module.exports = function (data) { + const tagBuffer = encoder.encode(data.tag); + const nameBuffer = encoder.encode(data.name); + const descriptionBuffer = encoder.encode(data.description); + + const packetLength = 29 + 1 + tagBuffer.byteLength + nameBuffer.byteLength + descriptionBuffer.byteLength; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 175); + view.setUint8(1, packetLength); + view.setUint8(2, data.level); + view.setUint8(3, data.size.current); + view.setUint8(4, data.size.maximum); + view.setUint8(5, data.taxRates[0]); + view.setUint8(6, data.taxRates[1]); + view.setUint32(7, data.timestamps.tagEditedAt); + view.setUint32(11, data.timestamps.nameEditedAt); + view.setUint32(15, data.timestamps.descriptionEditedAt); + view.setUint32(19, data.timestamps.taxRatesEditedAt); + view.setUint32(23, data.experience); + console.info(data, "guild general data") + view.setUint8(27, tagBuffer.byteLength); + new Uint8Array(buffer, 28).set(tagBuffer); + + view.setUint8(28 + tagBuffer.byteLength, nameBuffer.byteLength); + new Uint8Array(buffer, 28 + tagBuffer.byteLength + 1).set(nameBuffer); + + view.setUint8(28 + tagBuffer.byteLength + nameBuffer.byteLength + 1, descriptionBuffer.byteLength); + new Uint8Array(buffer, 28 + tagBuffer.byteLength + nameBuffer.byteLength + 1 + 1).set(descriptionBuffer); + + return buffer; +}; + diff --git a/Packets/GuildDataIdentityModified.js b/Packets/GuildDataIdentityModified.js new file mode 100644 index 0000000..d983cb6 --- /dev/null +++ b/Packets/GuildDataIdentityModified.js @@ -0,0 +1,35 @@ +/* + @Packet name GuildDataIdentityModified + @Packet identifier 186 + @Description Includes data about modified guild identity (tag, name, description) and the timestamps of the end of cooldowns + @Data - + @Structure - +*/ + +module.exports = function (data) { + const identityLength = data.bufferTag.byteLength + 1 + data.bufferName.byteLength + 1 + data.bufferDescription.byteLength + 1; + const packetLength = 3 * (4 + 1) + identityLength + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 186); + view.setUint8(1, packetLength); + view.setUint8(2, data.r[0]); + view.setUint32(3, data.timestamps.tagEditedAt); + view.setUint8(7, data.r[1]); + view.setUint32(8, data.timestamps.nameEditedAt); + view.setUint8(12, data.r[2]); + view.setUint32(13, data.timestamps.descriptionEditedAt); + + view.setUint8(17, data.bufferTag.byteLength); + new Uint8Array(buffer, 18).set(data.bufferTag); + + view.setUint8(18 + data.bufferTag.byteLength, data.bufferName.byteLength); + new Uint8Array(buffer, 18 + data.bufferTag.byteLength + 1).set(data.bufferName); + + view.setUint8(18 + data.bufferTag.byteLength + data.bufferName.byteLength + 1, data.bufferDescription.byteLength); + new Uint8Array(buffer, 18 + data.bufferTag.byteLength + 1 + data.bufferName.byteLength + 1).set(data.bufferDescription); + console.info("buffer", buffer) + console.info(data.timestamps) + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataIsland.js b/Packets/GuildDataIsland.js new file mode 100644 index 0000000..db14bd9 --- /dev/null +++ b/Packets/GuildDataIsland.js @@ -0,0 +1,22 @@ +/* + @Packet name GuildDataIsland + @Packet identifier 179 + @Description Includes data about a guild island + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 179); + view.setUint8(1, packetLength); + view.setUint8(2, data.id); + view.setUint8(3, data.mapID) + view.setUint16(4, data.position.x); + view.setUint16(6, data.position.y); + + return buffer; +}; diff --git a/Packets/GuildDataIslandCaptured.js b/Packets/GuildDataIslandCaptured.js new file mode 100644 index 0000000..1b7618c --- /dev/null +++ b/Packets/GuildDataIslandCaptured.js @@ -0,0 +1,19 @@ +/* + @Packet name GuildDataIslandCaptured + @Packet identifier 184 + @Description Includes data about a guild island that has been captured + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 184); + view.setUint8(1, packetLength); + view.setUint8(2, data.guildIslandID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataIslandTower.js b/Packets/GuildDataIslandTower.js new file mode 100644 index 0000000..0f289c0 --- /dev/null +++ b/Packets/GuildDataIslandTower.js @@ -0,0 +1,24 @@ +/* + @Packet name GuildDataIslandTower + @Packet identifier 180 + @Description Includes data about a guild island tower + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 13 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 180); + view.setUint8(1, packetLength); + view.setUint8(2, data.entityTypeID); + view.setUint32(3, data.id); + view.setUint8(7, data.guildIslandID); + view.setUint8(8, data.order); + view.setUint32(9, data.hp.current); + view.setUint8(13, data.isRepairing); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataIslandTowerBuilt.js b/Packets/GuildDataIslandTowerBuilt.js new file mode 100644 index 0000000..6150458 --- /dev/null +++ b/Packets/GuildDataIslandTowerBuilt.js @@ -0,0 +1,21 @@ +/* + @Packet name GuildDataIslandTowerBuilt + @Packet identifier 181 + @Description Includes data about a built guild island tower + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 181); + view.setUint8(1, packetLength); + view.setUint8(2, data.entityTypeID); + view.setUint32(3, data.id); + view.setUint8(7, data.guildIslandID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataIslandTowerDestroyed.js b/Packets/GuildDataIslandTowerDestroyed.js new file mode 100644 index 0000000..cc9f919 --- /dev/null +++ b/Packets/GuildDataIslandTowerDestroyed.js @@ -0,0 +1,21 @@ +/* + @Packet name GuildDataIslandTowerDestroyed + @Packet identifier 182 + @Description Includes data about a destroyed guild island tower + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 182); + view.setUint8(1, packetLength); + view.setUint8(2, data.entityTypeID); + view.setUint32(3, data.id); + view.setUint8(7, data.guildIslandID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataIslandTowerHealthChanged.js b/Packets/GuildDataIslandTowerHealthChanged.js new file mode 100644 index 0000000..5e32e26 --- /dev/null +++ b/Packets/GuildDataIslandTowerHealthChanged.js @@ -0,0 +1,22 @@ +/* + @Packet name GuildDataIslandTowerHealthChanged + @Packet identifier 183 + @Description Includes data about a guild island tower that has its current hitpoint changed + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 11 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 183); + view.setUint8(1, packetLength); + view.setUint8(2, data.entityTypeID); + view.setUint32(3, data.id); + view.setUint8(7, data.guildIslandID); + view.setUint32(8, data.currentHitpoint) + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataMember.js b/Packets/GuildDataMember.js new file mode 100644 index 0000000..5f7a679 --- /dev/null +++ b/Packets/GuildDataMember.js @@ -0,0 +1,29 @@ +/* + @Packet name GuildDataMember + @Packet identifier 176 + @Description Includes data about a guild member + @Data pId, + @Structure - +*/ + +const encoder = new TextEncoder(); + +module.exports = function (data) { + const nameBuffer = encoder.encode(data.name); + + const packetLength = 13 + 1 + nameBuffer.byteLength; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 176); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + view.setUint32(6, data.permission) + view.setUint8(10, data.isOnline); + view.setUint16(11, data.pirateLevel); +console.info(data); + view.setUint8(13, nameBuffer.byteLength); + new Uint8Array(buffer, 14).set(nameBuffer); + + return buffer; +}; diff --git a/Packets/GuildDataMemberChanged.js b/Packets/GuildDataMemberChanged.js new file mode 100644 index 0000000..f9adf28 --- /dev/null +++ b/Packets/GuildDataMemberChanged.js @@ -0,0 +1,60 @@ +/* + @Packet name GuildDataMemberChanged + @Packet identifier 193 + @Description Holds data about a single guild member's changed properties + @Data - + @Structure - +*/ + +const encoder = new TextEncoder(); + +module.exports = function (data) { + const packetLength = 5 + 1 + 1 + + ("isOnline" in data ? 1 : 0) + + ("permission" in data ? 4 : 0) + + ("level" in data ? 2 : 0) + + ("name" in data ? 1 + data.name.length : 0); + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + let offset = 6; + let mask = 0; + console.info(packetLength, "isOnline" in data, "permission" in data, "level" in data, "name" in data) + if ("isOnline" in data) { + mask |= (1 << 0); + view.setUint8(offset + 1, data.isOnline); + + offset += 1; + }; + + if ("permission" in data) { + mask |= (1 << 1); + view.setUint32(offset + 1, data.permission); + + offset += 4; + }; + + if ("level" in data) { + mask |= (1 << 2); + view.setUint16(offset + 1, data.level); + + offset += 2; + }; + + if ("name" in data) { + mask |= (1 << 3); + + const nameBuffer = encoder.encode(data.name); + view.setUint8(offset + 1, nameBuffer.byteLength); + new Uint8Array(buffer, offset + 2).set(nameBuffer); + + offset += 1 + nameBuffer.byteLength; + }; + + view.setUint8(0, 193); + view.setUint8(1, packetLength); + view.setUint8(2, mask); + view.setUint32(3, data.id); + + return buffer; +}; diff --git a/Packets/GuildDataMemberRemoved.js b/Packets/GuildDataMemberRemoved.js new file mode 100644 index 0000000..c8e5552 --- /dev/null +++ b/Packets/GuildDataMemberRemoved.js @@ -0,0 +1,19 @@ +/* + @Packet name GuildDataMemberRemoved + @Packet identifier 185 + @Description Includes data about a guild member who joined a guild + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 5 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 185); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildDataTaxRatesModified.js b/Packets/GuildDataTaxRatesModified.js new file mode 100644 index 0000000..1894e9a --- /dev/null +++ b/Packets/GuildDataTaxRatesModified.js @@ -0,0 +1,20 @@ +/* + @Packet name GuildDataTaxRatesModified + @Packet identifier 188 + @Description Includes data about modified guild tax rates + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = data.length + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 188); + view.setUint8(1, packetLength); + data.forEach((d, i) => view.setUint8(2 + i, d)); + console.info(data, buffer); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildIslandCaptured.js b/Packets/GuildIslandCaptured.js new file mode 100644 index 0000000..2b9953b --- /dev/null +++ b/Packets/GuildIslandCaptured.js @@ -0,0 +1,11 @@ +module.exports = function (data) { + const packetLength = 1 + 1 + data.tag.byteLength; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 174); + view.setUint8(1, packetLength); + new Uint8Array(buffer, 2).set(data.tag); + + return buffer; +}; diff --git a/Packets/GuildLeaveConfirmation.js b/Packets/GuildLeaveConfirmation.js new file mode 100644 index 0000000..eb3b75d --- /dev/null +++ b/Packets/GuildLeaveConfirmation.js @@ -0,0 +1,19 @@ +/* + @Packet name GuildLeaveConfirmation + @Packet identifier 177 + @Description Confirms the requested resignation from guild + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 177); + view.setUint8(1, packetLength); + view.setUint8(2, data); + + return buffer; +}; diff --git a/Packets/GuildMemberNew.js b/Packets/GuildMemberNew.js new file mode 100644 index 0000000..a1f3d69 --- /dev/null +++ b/Packets/GuildMemberNew.js @@ -0,0 +1,20 @@ +/* + this one is for accepted members + sent out to all the players that are near to the accepted player (to update the player's guild tag) +*/ + +module.exports = function (data) { + const packetLength = 6 + 1 + data.tag.byteLength; + + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 45); + view.setUint8(1, packetLength); + view.setUint32(2, data.playerID); + + view.setUint8(6, data.tag.byteLength); + new Uint8Array(buffer, 7).set(data.tag); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/GuildMemberOld.js b/Packets/GuildMemberOld.js new file mode 100644 index 0000000..c26eed9 --- /dev/null +++ b/Packets/GuildMemberOld.js @@ -0,0 +1,28 @@ +/* + this one is for the leaving members + sent out to all the players that are near to the leaving player (to update the player's guild tag to nothing) +*/ + +module.exports = function (data) { + // data: { playerID: number } + + const packetLength = 5 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 46); + view.setUint8(1, packetLength); + view.setUint32(2, data.playerID); + + return buffer; +}; + +/* +const guildMemberOld = serializeGuildMemberOld({ playerID: 7 }); + + +const guildMemberOldView = new DataView(guildMemberOld); +const playerID = guildMemberOldView.getUint32(1); + + +*/ \ No newline at end of file diff --git a/Packets/GuildSearchResult.js b/Packets/GuildSearchResult.js new file mode 100644 index 0000000..1ff9bc1 --- /dev/null +++ b/Packets/GuildSearchResult.js @@ -0,0 +1,34 @@ +/* + @Packet name GuildSearchResult + @Packet identifier 189 + @Description Sends the + @Data Holds data of a single result of a guild when searching guilds + @Structure - +*/ + +const encoder = new TextEncoder(); + +module.exports = function (data) { + const tagBuffer = encoder.encode(data.tag); + const nameBuffer = encoder.encode(data.name); + + const packetLength = 9 + 1 + tagBuffer.byteLength + nameBuffer.byteLength; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 189); + view.setUint8(1, packetLength); + view.setUint16(2, data.id); + view.setUint8(4, data.size.current); + view.setUint8(5, data.size.maximum); + view.setUint8(6, data.hasApplied); + view.setUint8(7, data.isLastEntry); + + view.setUint8(8, tagBuffer.byteLength); + new Uint8Array(buffer, 9).set(tagBuffer); + + view.setUint8(7 + tagBuffer.byteLength + 1 + 1, nameBuffer.byteLength); + new Uint8Array(buffer, 7 + tagBuffer.byteLength + 1 + 1 + 1).set(nameBuffer); + + return buffer; +}; diff --git a/Packets/GuildSearchResultStateChanged.js b/Packets/GuildSearchResultStateChanged.js new file mode 100644 index 0000000..7e26b5e --- /dev/null +++ b/Packets/GuildSearchResultStateChanged.js @@ -0,0 +1,20 @@ +/* + @Packet name GuildSearchResultStateChanged + @Packet identifier 190 + @Description Holds data of a single result's application state change + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 4 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 190); + view.setUint8(1, packetLength); + view.setUint16(2, data.id); + view.setUint8(4, data.hasApplied); + + return buffer; +}; diff --git a/Packets/Harpoons.js b/Packets/Harpoons.js new file mode 100644 index 0000000..d6ee8a5 --- /dev/null +++ b/Packets/Harpoons.js @@ -0,0 +1,30 @@ +/* + @Packet name Harpoons + @Packet identifier 154 + @Description Sends the + @Data pId, (harpoonId, damage, amount), selectedHarpoon + @Structure - +*/ + +module.exports = function (data) { + const { harpoon, selected } = data; + + const packetLength = harpoon.length * 6 + 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 154); + view.setUint8(1, packetLength); + + for (let i = 0; i < harpoon.length; i++) { + const harp = harpoon[i]; + + view.setUint8(i * 6 + 2, harp.id); + view.setUint8(i * 6 + 3, 0); + view.setUint32(i * 6 + 4, harp.amount); + }; + + view.setUint8(2 + harpoon.length * 6, selected); + + return buffer; +}; diff --git a/Packets/HealthChanged.js b/Packets/HealthChanged.js new file mode 100644 index 0000000..ee0d2d4 --- /dev/null +++ b/Packets/HealthChanged.js @@ -0,0 +1,25 @@ +/* + @Packet name HealthChanged + @Packet identifier 163 + @Description Sends information about health modification (increase, decrease) + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 17 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 163); + view.setUint8(1, packetLength); + view.setUint8(2, data.authorTypeID); + view.setUint32(3, data.authorID); + view.setUint8(7, data.targetTypeID); + view.setUint32(8, data.targetID); + view.setUint32(12, data.amount); + view.setUint8(16, data.type); + view.setUint8(17, data.isCritical); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/HealthChangedAuthorless.js b/Packets/HealthChangedAuthorless.js new file mode 100644 index 0000000..6d8ad1c --- /dev/null +++ b/Packets/HealthChangedAuthorless.js @@ -0,0 +1,23 @@ +/* + @Packet name HealthChangedAuthorless + @Packet identifier 162 + @Description - + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 12 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 162); + view.setUint8(1, packetLength); + view.setUint8(2, data.targetTypeID); + view.setUint32(3, data.targetID); + view.setUint32(7, data.amount); + view.setUint8(11, data.type); + view.setUint8(12, data.isCritical); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/ItemBought.js b/Packets/ItemBought.js new file mode 100644 index 0000000..b1434f8 --- /dev/null +++ b/Packets/ItemBought.js @@ -0,0 +1,21 @@ +/* + @Packet name ItemBought + @Packet identifier 170 + @Description Holds data about a bought item's confirmation from shop + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 170); + view.setUint8(1, packetLength); + view.setUint8(2, data.categoryID); + view.setUint8(3, data.id); + view.setUint32(4, data.amount); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/ItemUsed.js b/Packets/ItemUsed.js new file mode 100644 index 0000000..de64527 --- /dev/null +++ b/Packets/ItemUsed.js @@ -0,0 +1,22 @@ +/* + @Packet name ItemUsed + @Packet identifier 164 + @Description Sends information about a used item + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 5 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 164); + view.setUint8(1, packetLength); + view.setUint8(2, data.statusID); + view.setUint8(3, data.itemID); + view.setUint8(4, data.amountToRemove); + view.setUint8(5, data.isActive); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Items.js b/Packets/Items.js new file mode 100644 index 0000000..060cfc4 --- /dev/null +++ b/Packets/Items.js @@ -0,0 +1,23 @@ +/* + @Packet name Items + @Packet identifier 155 + @Description Sends item information + @Data pId, type, amount + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + data.length * 5; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 155); + view.setUint8(1, packetLength); + + data.forEach((item, i) => { + view.setUint8(2 + i * 5, item.id); + view.setUint32(3 + i * 5, item.amount); + }); + + return buffer; +}; diff --git a/Packets/LeaderboardPage.js b/Packets/LeaderboardPage.js new file mode 100644 index 0000000..ff380ad --- /dev/null +++ b/Packets/LeaderboardPage.js @@ -0,0 +1,41 @@ +/* + @Packet name LeaderboardPage + @Packet identifier 166 + @Description - + @Data pId + @Structure - +*/ + +module.exports = { + init: (leaderboardID, length) => { + const packetLength = (length * 16 + length * 4) + 4 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 166); + view.setUint8(1, packetLength); + view.setUint16(2, leaderboardID); + view.setUint8(4, length); + + let i = 0; + + return { + update: d => { + const startIndex = i * (16 + 4); + + const nameBuffer = Buffer.from(d.username); + for (const pair of nameBuffer.entries()) { + const index = pair[0]; + view.setUint8(5 + startIndex + index, pair[1]); + }; + + view.setUint32(5 + startIndex + 16, d.amount); + + i++; + }, + get: () => { + return buffer; + } + }; + } +}; \ No newline at end of file diff --git a/Packets/LeaderboardPositions.js b/Packets/LeaderboardPositions.js new file mode 100644 index 0000000..86d35f6 --- /dev/null +++ b/Packets/LeaderboardPositions.js @@ -0,0 +1,23 @@ +/* + @Packet name LeaderboardPositions + @Packet identifier 165 + @Description + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + 7 * (2 + 2); + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 165); + view.setUint8(1, packetLength); + + for (let i = 0; i < 7; i++) { + view.setUint16(2 + i * 4, data[i].selfPosition); + view.setUint16(2 + i * 4 + 2, data[i].totalPositionsLength); + }; + + return buffer; +}; \ No newline at end of file diff --git a/Packets/LevelIsLow.js b/Packets/LevelIsLow.js new file mode 100644 index 0000000..03dd04d --- /dev/null +++ b/Packets/LevelIsLow.js @@ -0,0 +1,18 @@ +/* + @Packet name LevelIsLow + @Packet identifier 152 + @Description Sends "level is low" notification + @Data pId + @Structure - +*/ + +module.exports = function() { + const packetLength = 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 152); + view.setUint8(1, packetLength); + + return buffer; +}; diff --git a/Packets/LevelUp.js b/Packets/LevelUp.js new file mode 100644 index 0000000..781038d --- /dev/null +++ b/Packets/LevelUp.js @@ -0,0 +1,23 @@ +/* + @Packet name LevelUp + @Packet identifier 159 + @Description - + @Data pId, level, statusLevelID, playerID + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 8 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { level, statusLevelID, id } = data; + + view.setUint8(0, 159); + view.setUint8(1, packetLength); + view.setUint16(2, level); + view.setUint8(4, statusLevelID); + view.setUint32(5, id); + + return buffer; +}; diff --git a/Packets/LogoutCancelled.js b/Packets/LogoutCancelled.js new file mode 100644 index 0000000..c09ca30 --- /dev/null +++ b/Packets/LogoutCancelled.js @@ -0,0 +1,18 @@ +/* + @Packet name LogoutCancelled + @Packet identifier 136 + @Description Sends the player's id to the nearby players + @Data pId, playerId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 136); + view.setUint8(1, packetLength); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/MainPlayer.js b/Packets/MainPlayer.js new file mode 100644 index 0000000..d10e04a --- /dev/null +++ b/Packets/MainPlayer.js @@ -0,0 +1,52 @@ +/* + @Packet name MainPlayer + @Packet identifier 128 + @Description Sends the connected main player's data + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + // const guildTag = data.guildTag; + const packetLength = 52 + 1; // (guildTag ? 51 : 48) + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 128); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + view.setUint32(6, data.hp.current); + view.setUint32(10, data.hp.maximum); + view.setUint16(14, data.position.x); + view.setUint16(16, data.position.y); + view.setUint16(18, data.speed); + view.setUint8(20, data.mapID); + view.setUint16(21, data.textureID); + view.setUint16(23, data.harpoonerRange); + view.setUint16(25, data.cannonRange) + view.setUint8(27, +data.isSpeedLowered) + view.setUint8(28, +data.isRepairing) + view.setUint16(29, data.pirateLevel); + view.setUint16(31, data.eliteLevel); + view.setUint16(33, data.fishingLevel); + view.setUint8(35, data.globalRank); + view.setUint8(36, data.levelRank); + + const nameBuffer = Buffer.from(data.name); + for (const pair of nameBuffer.entries()) { + const index = pair[0]; + view.setUint8(37 + index, pair[1]); + }; + + /* + if (guildTag) { + const guildBuffer = Buffer.from(guildTag); + for (const pair of guildBuffer.entries()) { + const index = pair[0]; + view.setUint8(49 + index, pair[1]); + }; + }; + */ + + return buffer; +}; \ No newline at end of file diff --git a/Packets/MaintenanceScheduled.js b/Packets/MaintenanceScheduled.js new file mode 100644 index 0000000..dbb06a5 --- /dev/null +++ b/Packets/MaintenanceScheduled.js @@ -0,0 +1,20 @@ +/* + @Packet name MaintenanceScheduled + @Packet identifier 254 + @Description Sends the + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 7 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 254); + view.setUint8(1, packetLength); + view.setUint32(2, data.timestamp); + view.setUint16(6, data.timeSeconds); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/MapChangeState.js b/Packets/MapChangeState.js new file mode 100644 index 0000000..601a00d --- /dev/null +++ b/Packets/MapChangeState.js @@ -0,0 +1,20 @@ +/* + @Packet name MapChangeState + @Packet identifier 151 + @Description - + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 4 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 151); + view.setUint8(1, packetLength); + view.setUint8(2, data.isAbleToJump); + view.setUint16(3, data.newMapID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/MapChanged.js b/Packets/MapChanged.js new file mode 100644 index 0000000..245ec38 --- /dev/null +++ b/Packets/MapChanged.js @@ -0,0 +1,23 @@ +/* + @Packet name MapChanged + @Packet identifier 169 + @Description Sends the + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { mapID, x, y } = data; + + view.setUint8(0, 169); + view.setUint8(1, packetLength); + view.setUint8(2, mapID); + view.setUint16(3, x); + view.setUint16(5, y); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/MaximumCannonsSize.js b/Packets/MaximumCannonsSize.js new file mode 100644 index 0000000..0676665 --- /dev/null +++ b/Packets/MaximumCannonsSize.js @@ -0,0 +1,19 @@ +/* + @Packet name MaximumCannonsSize + @Packet identifier 156 + @Description - + @Data pId, cannonSlots + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 156); + view.setUint8(1, packetLength); + view.setUint16(2, data); + + return buffer; +}; diff --git a/Packets/MiniMapEntryMove.js b/Packets/MiniMapEntryMove.js new file mode 100644 index 0000000..ec5330d --- /dev/null +++ b/Packets/MiniMapEntryMove.js @@ -0,0 +1,21 @@ +/* + @Packet name MiniMapEntryMove + @Packet identifier 201 + @Description Includes data about a minimap entry's new position + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 9 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 201); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + view.setUint16(6, data.position.x); + view.setUint16(8, data.position.y); + + return buffer; +}; diff --git a/Packets/MiniMapEntryRemove.js b/Packets/MiniMapEntryRemove.js new file mode 100644 index 0000000..7ac6d47 --- /dev/null +++ b/Packets/MiniMapEntryRemove.js @@ -0,0 +1,19 @@ +/* + @Packet name MiniMapEntryRemove + @Packet identifier 202 + @Description Includes data about a removed minimap entry + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 5 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 202); + view.setUint8(1, packetLength); + view.setUint32(2, data.id); + + return buffer; +}; diff --git a/Packets/MojoRemoved.js b/Packets/MojoRemoved.js new file mode 100644 index 0000000..4f64ba7 --- /dev/null +++ b/Packets/MojoRemoved.js @@ -0,0 +1,19 @@ +/* + @Packet name MojoRemoved + @Packet identifier 172 + @Description Confirms how many mojo was removed. + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 172); + view.setUint8(1, packetLength) + view.setUint16(2, data); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/NewDestinationNode.js b/Packets/NewDestinationNode.js new file mode 100644 index 0000000..dd73bc4 --- /dev/null +++ b/Packets/NewDestinationNode.js @@ -0,0 +1,22 @@ +/* + @Packet name NewDestinationNode + @Packet identifier 167 + @Description Sends the next node + @Data pId + @Structure - +*/ + +const packetLength = 1 + 10; +module.exports = function (data) { + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 167); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint32(3, data.id); + view.setUint16(7, data.x); + view.setUint16(9, data.y); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/ObtainedDesigns.js b/Packets/ObtainedDesigns.js new file mode 100644 index 0000000..028cdb1 --- /dev/null +++ b/Packets/ObtainedDesigns.js @@ -0,0 +1,20 @@ +/* + @Packet name ObtainedDesigns + @Packet identifier 132 + @Description Contains information about the design ID's that the player is owner of + @Data pId, typeId, entityId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + data.length; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 132); + view.setUint8(1, packetLength); + + data.forEach((id, i) => view.setUint8(2 + i, id)); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/PropertyChanged.js b/Packets/PropertyChanged.js new file mode 100644 index 0000000..9ac126a --- /dev/null +++ b/Packets/PropertyChanged.js @@ -0,0 +1,20 @@ +/* + @Packet name PropertyChanged + @Packet identifier 137 + @Description - + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 137); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint32(3, data.value); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/PropertyChangedGeneral.js b/Packets/PropertyChangedGeneral.js new file mode 100644 index 0000000..f674f46 --- /dev/null +++ b/Packets/PropertyChangedGeneral.js @@ -0,0 +1,22 @@ +/* + @Packet name PropertyChangedGeneral + @Packet identifier 199 + @Description - + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 11 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 199); + view.setUint8(1, packetLength); + view.setUint8(2, data.typeID); + view.setUint32(3, data.id); + view.setUint8(7, data.propertyTypeID); + view.setUint32(8, data.value); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/QuestAdded.js b/Packets/QuestAdded.js new file mode 100644 index 0000000..ec5a1ff --- /dev/null +++ b/Packets/QuestAdded.js @@ -0,0 +1,21 @@ +/* + @Packet name QuestAdded + @Packet identifier 173 + @Description Adding a quest by its ID + @Data pId, questId, isSuccessful + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 3; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { questID } = data; + + view.setUint8(0, 173); + view.setUint8(1, packetLength); + view.setUint16(2, questID); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/QuestRemoval.js b/Packets/QuestRemoval.js new file mode 100644 index 0000000..56dedfd --- /dev/null +++ b/Packets/QuestRemoval.js @@ -0,0 +1,22 @@ +/* + @Packet name QuestRemoval + @Packet identifier 129 + @Description Removing a quest by its ID + @Data pId, questId, isSuccessful + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 4; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { questID, isSuccessful } = data; + + view.setUint8(0, 129); + view.setUint8(1, packetLength); + view.setUint16(2, questID); + view.setUint8(4, isSuccessful); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Quests.js b/Packets/Quests.js new file mode 100644 index 0000000..15fa5d5 --- /dev/null +++ b/Packets/Quests.js @@ -0,0 +1,35 @@ +/* + @Packet name Quests + @Packet identifier 147 + @Description Sends the quests' data + @Data pId + @Structure - +*/ + +module.exports = { + init: () => { + const buffer = new ArrayBuffer(512); + const view = new DataView(buffer); + view.setUint8(0, 147); + view.setUint8(1, 0); + + let i = 0; + + return { + update: d => { + const { id, progression } = d; + + view.setUint16(2 + i, id); + view.setUint8(2 + i + 2, progression.length); + progression.forEach((p, j) => view.setUint32(2 + i + 3 + j * 4, p.amount)); + + i += 3 + progression.length * 4; + }, + get: () => { + view.setUint8(1, i + 2); + return buffer.slice(0, i + 2); + } + }; + } + +}; diff --git a/Packets/QuestsCompletedAmount.js b/Packets/QuestsCompletedAmount.js new file mode 100644 index 0000000..54fc13b --- /dev/null +++ b/Packets/QuestsCompletedAmount.js @@ -0,0 +1,24 @@ +/* + @Packet name QuestsCompletedAmount + @Packet identifier 149 + @Description Sends how many times quests have been completed + @Data pId, + @Structure - +*/ + +module.exports = function (data) { + const keys = Object.keys(data); + const packetLength = keys.length * 3 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 149); + view.setUint8(1, packetLength); + + keys.forEach((id, i) => { + view.setUint16(2 + i * 3, id); + view.setUint8(2 + i * 3 + 2, data[id]); + }); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/RankPoint.js b/Packets/RankPoint.js new file mode 100644 index 0000000..df15ab0 --- /dev/null +++ b/Packets/RankPoint.js @@ -0,0 +1,20 @@ +/* + @Packet name RankPoint + @Packet identifier 206 + @Description Holds data about a value needed to calculate a specific rank + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 5 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 206); + view.setUint8(1, packetLength); + view.setUint8(2, data.rankTypeID); + view.setUint32(3, data.value); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/RankThresholdPoints.js b/Packets/RankThresholdPoints.js new file mode 100644 index 0000000..2f67f7e --- /dev/null +++ b/Packets/RankThresholdPoints.js @@ -0,0 +1,24 @@ +/* + @Packet name RankThresholdPoints + @Packet identifier 205 + @Description Holds data about needed points to be in a specific rank bracket + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = data.thresholds.length * 4 + 1 + 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 205); + view.setUint8(1, packetLength); + view.setUint8(2, data.rankTypeID); + + console.info(data) + + for (let i = 0, length = data.thresholds.length; i < length; i++) + view.setUint32(3 + i * 4, data.thresholds[i]); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/RepairConfirmation.js b/Packets/RepairConfirmation.js new file mode 100644 index 0000000..b22cf53 --- /dev/null +++ b/Packets/RepairConfirmation.js @@ -0,0 +1,19 @@ +/* + @Packet name RepairConfirmation + @Packet identifier 138 + @Description Confirms the repair request of the packet author. + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 2 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 138); + view.setUint8(1, packetLength) + view.setUint8(2, data); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/RespawnConfirmation.js b/Packets/RespawnConfirmation.js new file mode 100644 index 0000000..16abd15 --- /dev/null +++ b/Packets/RespawnConfirmation.js @@ -0,0 +1,18 @@ +/* + @Packet name RespawnConfirmation + @Packet identifier 144 + @Description Sends the confirmation of the respawn + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 144); + view.setUint8(1, packetLength); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/Reward.js b/Packets/Reward.js new file mode 100644 index 0000000..6b9804b --- /dev/null +++ b/Packets/Reward.js @@ -0,0 +1,66 @@ +/* + @Packet name Reward + @Packet identifier 131 + @Description Contains information about rewards the player got + @Data pId + @Structure - +*/ + +module.exports = { + init: (isRegular, length) => { + const maxEntries = Math.floor((255 - (1 + 2)) / 6); + let submittedLength = length; + + if (length > maxEntries) length = maxEntries; + submittedLength -= length; + + const offset = 3; + const packetLength = length * 6 + 1 + 2; + let i = 0; + let j = 0; + + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + view.setUint8(0, 131); + view.setUint8(1, packetLength); + view.setUint8(2, isRegular ? 255 : 0); + + const buffers = [buffer]; + const views = [view]; + + return { + update: d => { + const startIndex = i * 6; + views[j].setUint8(offset + startIndex, d.categoryID); + views[j].setUint8(offset + startIndex + 1, d.id); + views[j].setInt32(offset + startIndex + 2, d.amount); + + i++; + + if (maxEntries === i) { + i = 0; + j++; + + if (submittedLength > maxEntries) length = maxEntries; + else length = submittedLength; + + submittedLength -= length + if (submittedLength < 0) length = maxEntries + submittedLength; + + const packetLength = length * 6 + 1 + 2; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + view.setUint8(0, 131); + view.setUint8(1, packetLength); + view.setUint8(2, isRegular ? 255 : 0); + + buffers.push(buffer); + views.push(view); + }; + }, + get: () => { + return buffers; + } + }; + } +}; \ No newline at end of file diff --git a/Packets/ShopEntry.js b/Packets/ShopEntry.js new file mode 100644 index 0000000..eb76a90 --- /dev/null +++ b/Packets/ShopEntry.js @@ -0,0 +1,24 @@ +/* + @Packet name ShopEntry + @Packet identifier 215 + @Description Holds data about a shop entry + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 12 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 215); + view.setUint8(1, packetLength); + view.setUint8(2, data.id); + view.setUint8(3, data.categoryID); + view.setUint16(4, data.itemID); + view.setUint8(6, data.currencyType); + view.setUint32(7, data.currencyAmount); + view.setUint16(11, data.unit); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/ShopEntryRemoved.js b/Packets/ShopEntryRemoved.js new file mode 100644 index 0000000..0da288c --- /dev/null +++ b/Packets/ShopEntryRemoved.js @@ -0,0 +1,20 @@ +/* + @Packet name ShopEntryRemoved + @Packet identifier 216 + @Description Holds the id of a shop entry being removed + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 216); + view.setUint8(1, packetLength); + view.setUint8(2, data.shopCategoryID); + view.setUint8(3, data.id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/SpyResult.js b/Packets/SpyResult.js new file mode 100644 index 0000000..45af99c --- /dev/null +++ b/Packets/SpyResult.js @@ -0,0 +1,23 @@ +/* + @Packet name SpyResult + @Packet identifier 130 + @Description Spy event + @Data pId, mapId, positionX, positionY + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 6 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { mapID, x, y } = data; + + view.setUint8(0, 130); + view.setUint8(1, packetLength); + view.setUint8(2, mapID) + view.setUint16(3, x); + view.setUint16(5, y); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/StatusPoints.js b/Packets/StatusPoints.js new file mode 100644 index 0000000..b538c93 --- /dev/null +++ b/Packets/StatusPoints.js @@ -0,0 +1,27 @@ +/* + @Packet name StatusPoints + @Packet identifier 157 + @Description Sends points data (pirate, elite, fishing levels) + @Data pId, experience + @Structure - +*/ + +module.exports = function (data) { + const { statusPoints } = data; + const statusPointsSize = statusPoints.length * 5; + const packetLength = 1 + 1 + statusPointsSize; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 157); + view.setUint8(1, packetLength); + + for (let i = 0, length = statusPoints.length; i < length; i++) { + const statusPoint = statusPoints[i]; + + view.setUint8(2 + i * 5, statusPoint.id); + view.setUint32(2 + i * 5 + 1, statusPoint.amount); + }; + + return buffer; +}; \ No newline at end of file diff --git a/Packets/WorldMapGuildEntry.js b/Packets/WorldMapGuildEntry.js new file mode 100644 index 0000000..ba5e509 --- /dev/null +++ b/Packets/WorldMapGuildEntry.js @@ -0,0 +1,23 @@ +/* + @Packet name WorldMapGuildEntry + @Packet identifier 194 + @Description - + @Data - + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 2 + (data.bufferTag.byteLength + 1) + 1 + 1 + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 194); + view.setUint8(1, packetLength); + view.setUint8(2, data.locationMapID); + view.setUint8(3, data.index); + + view.setUint8(4, data.bufferTag.byteLength); + new Uint8Array(buffer, 5).set(data.bufferTag); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/packet.js b/Packets/packet.js new file mode 100644 index 0000000..42b1123 --- /dev/null +++ b/Packets/packet.js @@ -0,0 +1,55 @@ +const cache = {}; + +const opcodesInterestedIn = { + // 200: true, // ammunition change + 164: true, // item usage + 163: true, // gets attacked + 162: true, // authorless hp change + 160: true, // design change + 151: true, // map change state + 148: true, // equipment change + 131: true, // rewards + 133: true, // entity disappears + 134: true // entity appears +}; + +function packet(packetsToAdd, batched, id, metadataToAdd) { + if (!(packetsToAdd instanceof Array)) packetsToAdd = [packetsToAdd]; + + if (!cache[id]) cache[id] = new ArrayBuffer(1024, { maxByteLength: 65536 }); + + const playerBehaviourContext = global.actionContext.getElement(id); + + const targetBuffer = cache[id]; + const targetArray = new Uint8Array(targetBuffer); + let offset = batched.offset; + + packetsToAdd.forEach((packet, i) => { + const newSize = offset + packet.byteLength; + if (newSize > targetBuffer.byteLength) { + const newBufferSize = Math.pow(2, Math.ceil(Math.log(newSize) / Math.log(2))); + targetBuffer.resize(newBufferSize); + }; + + const newArray = new Uint8Array(packet); + if (opcodesInterestedIn[newArray[0]]) { + const metadata = { + ts: new Date(), + pid: id, + metadata: metadataToAdd && metadataToAdd[i] ? metadataToAdd[i] : {} + }; + + playerBehaviourContext.add(metadata); + }; + + targetArray.set(newArray, offset); + offset += packet.byteLength; + }); + + return { + packet: targetBuffer, + offset + }; +}; + +module.exports = packet; \ No newline at end of file diff --git a/Packets/packet_255.js b/Packets/packet_255.js new file mode 100644 index 0000000..7febb16 --- /dev/null +++ b/Packets/packet_255.js @@ -0,0 +1,22 @@ +/* + @Packet name confirmationPacket + @Packet identifier 255 + @Description Confirms packets + @Data pId, confirmedpId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + const { packetId, isSuccessful } = data; + + view.setUint8(0, 255); + view.setUint8(1, packetLength); + view.setUint8(2, packetId); + view.setUint8(3, isSuccessful); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/packet_57.js b/Packets/packet_57.js new file mode 100644 index 0000000..6a78551 --- /dev/null +++ b/Packets/packet_57.js @@ -0,0 +1,20 @@ +/* + @Packet name shopCapReached + @Packet identifier 57 + @Description Sends information about reached cap + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 57); + view.setUint8(1, packetLength); + view.setUint8(2, data.shopCategoryID); + view.setUint8(3, data.id); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/packet_58.js b/Packets/packet_58.js new file mode 100644 index 0000000..987c68a --- /dev/null +++ b/Packets/packet_58.js @@ -0,0 +1,20 @@ +/* + @Packet name irrelevantShopEntries + @Packet identifier 58 + @Description Sends information about reached cap + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + 1 + data.length; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 58); + view.setUint8(1, packetLength); + + data.forEach((id, i) => view.setUint8(2 + i, id)); + + return buffer; +}; \ No newline at end of file diff --git a/Packets/packet_61.js b/Packets/packet_61.js new file mode 100644 index 0000000..6488a89 --- /dev/null +++ b/Packets/packet_61.js @@ -0,0 +1,40 @@ +/* + @Packet name - + @Packet identifier 61 + @Description + @Data pId + @Structure - +*/ + +module.exports = { + init: startPosition => { + const packetLength = (10 * 16 + 10 * 4) + 3 + 1; + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 61); + view.setUint8(1, packetLength); + view.setUint16(2, startPosition); + + let i = 0; + + return { + update: d => { + const startIndex = i * (16 + 4); + + const nameBuffer = Buffer.from(d.username); + for (const pair of nameBuffer.entries()) { + const index = pair[0]; + view.setUint8(4 + startIndex + index, pair[1]); + }; + + view.setUint32(4 + startIndex + 16, d.amount); + + i++; + }, + get: () => { + return buffer; + } + }; + } +}; \ No newline at end of file diff --git a/Packets/packet_62.js b/Packets/packet_62.js new file mode 100644 index 0000000..f1c0000 --- /dev/null +++ b/Packets/packet_62.js @@ -0,0 +1,22 @@ +/* + @Packet name leaderboardPosition + @Packet identifier 62 + @Description + @Data pId + @Structure - +*/ + +module.exports = function (data) { + const packetLength = 1 + + 1 + 7 * (2 + 2); + const buffer = new ArrayBuffer(packetLength); + const view = new DataView(buffer); + + view.setUint8(0, 62); + view.setUint8(1, packetLength); + for (let i = 0; i < 7; i++) { + view.setUint16(2 + i * 4, data[i].selfPosition); + view.setUint16(2 + i * 4 + 2, data[i].totalPositionsLength); + }; + + return buffer; +}; \ No newline at end of file diff --git a/Packets/packets.js b/Packets/packets.js new file mode 100644 index 0000000..8651ceb --- /dev/null +++ b/Packets/packets.js @@ -0,0 +1,65 @@ +const ReadyMessage = require("../Messages/ReadyMessage"); +const QuestMessage = require("../Messages/QuestMessage"); +const SpyMessage = require("../Messages/SpyMessage"); +const CauldronMessage = require("../Messages/CauldronMessage"); +const MovementMessage = require("../Messages/MovementMessage"); +const CloseMessage = require("../Messages/CloseMessage"); +const RepairMessage = require("../Messages/RepairMessage"); +const AttackStartMessage = require("../Messages/AttackStartMessage"); +const AttackAbortMessage = require("../Messages/AttackAbortMessage"); +const RespawnMessage = require("../Messages/RespawnMessage"); +const EquipmentChangeMessage = require("../Messages/EquipmentChangeMessage"); +const MapChangeMessage = require("../Messages/MapChangeMessage"); +const AmmunitionChangeMessage = require("../Messages/AmmunitionChangeMessage"); +const DesignChangeMessage = require("../Messages/DesignChangeMessage"); +const ItemUseMessage = require("../Messages/ItemUseMessage"); +const ItemBuyMessage = require("../Messages/ItemBuyMessage"); +const LeaderboardPageMessage = require("../Messages/LeaderboardPageMessage"); +const GuildMemberLeaveMessage = require("../Messages/GuildMemberLeaveMessage"); +const GuildMemberJoinMessage = require("../Messages/GuildMemberJoinMessage"); +const GuildJoinRequestSubmitMessage = require("../Messages/GuildJoinRequestSubmitMessage"); +const GuildDataMessage = require("../Messages/GuildDataMessage"); +const GuildDonateMessage = require("../Messages/GuildDonateMessage"); +const GuildDetailsTaxEditMessage = require("../Messages/GuildDetailsTaxEditMessage"); +const GuildDetailsIdentityEditMessage = require("../Messages/GuildDetailsIdentityEditMessage"); +const GuildIslandTowerRepairMessage = require("../Messages/GuildIslandTowerRepairMessage"); +const GuildIslandTowerBuildMessage = require("../Messages/GuildIslandTowerBuildMessage"); +const TopicInterestedMessage = require("../Messages/TopicInterestedMessage"); + +const packetTable = { + 1: { handler: ReadyMessage, expectedSize: 1, isAvailable: true }, + 2: { handler: QuestMessage, expectedSize: 3, isAvailable: true }, + 3: { handler: SpyMessage, expectedSize: 5, isAvailable: true }, + 4: { handler: CauldronMessage, expectedSize: 3, isAvailable: true }, + 5: { handler: MovementMessage, expectedSize: 5, isAvailable: true }, + 6: { handler: CloseMessage, expectedSize: 1, isAvailable: true }, + 7: { handler: RepairMessage, expectedSize: 1, isAvailable: true }, + 8: { handler: AttackStartMessage, expectedSize: 6, isAvailable: true }, + 9: { handler: AttackAbortMessage, expectedSize: 6, isAvailable: true }, + 10: { handler: RespawnMessage, expectedSize: 1, isAvailable: true }, + 11: { handler: EquipmentChangeMessage, expectedSize: 5, isAvailable: true }, + 12: { handler: MapChangeMessage, expectedSize: 1, isAvailable: true }, + 13: { handler: AmmunitionChangeMessage, expectedSize: 3, isAvailable: true }, + 14: { handler: DesignChangeMessage, expectedSize: 3, isAvailable: true }, + 15: { handler: ItemUseMessage, expectedSize: 7, isAvailable: true }, + 16: { handler: ItemBuyMessage, expectedSize: 7, isAvailable: true }, + 17: { handler: LeaderboardPageMessage, expectedSize: 4, isAvailable: true }, + 18: { handler: GuildMemberLeaveMessage, expectedSize: 5, isAvailable: true }, + 19: { handler: GuildMemberJoinMessage, expectedSize: 5, isAvailable: true }, + 20: { handler: GuildJoinRequestSubmitMessage, expectedSize: 0, isAvailable: true }, + 21: { handler: GuildDataMessage, expectedSize: 1, isAvailable: true }, + 22: { handler: GuildDonateMessage, expectedSize: 6, isAvailable: true }, + 23: { handler: GuildDetailsTaxEditMessage, expectedSize: 3, isAvailable: true }, + 24: { handler: GuildDetailsIdentityEditMessage, expectedSize: 0, isAvailable: true }, + 25: { handler: GuildIslandTowerRepairMessage, expectedSize: 6, isAvailable: true }, + 26: { handler: GuildIslandTowerBuildMessage, expectedSize: 7, isAvailable: true }, + 27: { handler: TopicInterestedMessage, expectedSize: 1, isAvailable: true } +}; + +module.exports = packetTable; + +/* + handler: the function that runs when packet is received + expectedSize: size of the packet + isAvailable: whether the server accepts this packet +*/ \ No newline at end of file diff --git a/Routes/Assets.js b/Routes/Assets.js new file mode 100644 index 0000000..57fbdd3 --- /dev/null +++ b/Routes/Assets.js @@ -0,0 +1,29 @@ +const fs = require("fs/promises"); + +module.exports = async (res, req) => { + const path = req.getParameter(0) + const asset = req.getParameter(1); + + const type = { + "css": "css", + "js": "javascript" + }; + + res.onAborted(() => console.info("aborted")); + + const assetFile = await fs.readFile(`../TestEnvironment/dist/game/${path}/${asset}`).catch(e => e); + if (assetFile.errno) { + return res.cork(() => { + res + .writeStatus("400 Bad request") + .end(); + }); + }; + + return res.cork(() => { + res + .writeStatus("200 OK") + .writeHeader("Content-Type", `text/${type[path]}`) + .end(assetFile); + }); +}; \ No newline at end of file diff --git a/Routes/Auth/Logout.js b/Routes/Auth/Logout.js new file mode 100644 index 0000000..10df4fc --- /dev/null +++ b/Routes/Auth/Logout.js @@ -0,0 +1,33 @@ +function redirectUnauthorized(res, req) { + res.cork(() => res.writeStatus("302").writeHeader("Location", "/").end()); +}; + +module.exports = async (res, req) => { + const cookieHeader = req.getHeader("cookie"); + const cookie = cookieHeader.split("ps=")[1]; + + res.onAborted(() => res.isAborted = true); + + if (!cookie) { + console.log("no cookie - logout"); + return redirectUnauthorized(res, req); + }; + + const formattedCookie = cookie.split(".")[0].slice(4); + const cookieInDB = await redis.get(`sess:${formattedCookie}`); + + if (!cookieInDB) { + console.log("no db - logout"); + return redirectUnauthorized(res, req); + }; + + const parsedCookie = JSON.parse(cookieInDB); + if (!parsedCookie.accountID) { + console.log("no accountid - logout"); + return redirectUnauthorized(res, req); + }; + + await redis.del(`sess:${formattedCookie}`); + + return res.cork(() => res.writeStatus("302").writeHeader("Set-Cookie", "ps=; Domain=.pirate-corsaire.com").writeHeader("Location", "/").end()); +}; \ No newline at end of file diff --git a/Routes/Game/Game.js b/Routes/Game/Game.js new file mode 100644 index 0000000..1b07919 --- /dev/null +++ b/Routes/Game/Game.js @@ -0,0 +1,56 @@ +const fs = require("fs/promises"); + +const redis = require("../../Miscellaneous/redis"); + +module.exports = async (res, req) => { + const cookieHeader = req.getHeader("cookie"); + const cookie = cookieHeader.split("ps=")[1]; + + res.onAborted(() => res.isAborted = true); + + if (!cookie) { + console.info("no cookie"); + return res + .writeStatus("301 moved permanently") + .writeHeader("Location", "redirect") + .end(); + }; + + const formattedCookie = cookie.split(".")[0].slice(4); + const cookieInDB = await redis.get(`sess:${formattedCookie}`); + + if (!cookieInDB) { + console.info("no db"); + return res + .writeStatus("301 moved permanently") + .writeHeader("Location", "redirect") + .end(); + }; + + const parsedCookie = JSON.parse(cookieInDB); + if (!parsedCookie.accountID) { + console.info("no account id"); + return res + .writeStatus("301 moved permanently") + .writeHeader("Location", "redirect") + .end(); + }; + + const html = await fs.readFile("../../TestEnvironment/dist/production/index.html") + .catch(e => { + res.cork(() => { + res + .writeStatus("404") + .end(); + }); + }); + + if (!html) return; + + res.cork(() => { + res + .writeStatus("200 OK") + .writeHeader("Content-Type", "text/html") + .end(html); + }); +} \ No newline at end of file diff --git a/Routes/Routes.js b/Routes/Routes.js new file mode 100644 index 0000000..1a63f74 --- /dev/null +++ b/Routes/Routes.js @@ -0,0 +1,29 @@ +const Router = require("../Utility/Router"); + +const apiRoute = new Router(); +const playerRoute = new Router("/player"); +const gameRoute = new Router("/game"); +const paymentRoute = new Router("/payment"); +const shopRoute = new Router(); + +app.get("/game/:path/:asset", require("./Assets")); +app.get("/game", require("./Game/Game")); +app.get("/logout", require("./Auth/Logout")); + +playerRoute.get("/kick", require("./api/Player/Kick")); + +gameRoute.get("/state", require("./api/Game/State")) +gameRoute.get("/shutdown", require("./api/Game/Shutdown")); + +paymentRoute.get("/payment_intent", require("./api/Payment/payment_intent")); +paymentRoute.get("/webhook", require("./api/Payment/webhook")); + +app.post("/shop", require("./api/Shop/Add")); +app.del("/shop/:id", require("./api/Shop/Remove")); + +apiRoute.get("/player/*", playerRoute.handle.bind(playerRoute)); +apiRoute.get("/game/*", gameRoute.handle.bind(gameRoute)); +apiRoute.get("/payment/*", paymentRoute.handle.bind(paymentRoute)); +// apiRoute.post("/shop/*", shopRoute.handle.bind(shopRoute)); + +app.get("/api/*", apiRoute.handle.bind(apiRoute)); \ No newline at end of file diff --git a/Routes/api/Game/Shutdown.js b/Routes/api/Game/Shutdown.js new file mode 100644 index 0000000..292007f --- /dev/null +++ b/Routes/api/Game/Shutdown.js @@ -0,0 +1,40 @@ +const Game = require("../../../Game/Game"); + +const MaintenanceScheduledPacket = require("../../../Packets/MaintenanceScheduled"); + +const isAuthenticated = require("../../../Utility/isAuthenticated"); + +module.exports = async (res, req) => { + res.onAborted(() => res.isAborted = true); + + const maintenanceTimeSeconds = req.getQuery("time"); + const timestamp = req.getQuery("timestamp"); + + const hasPermission = await isAuthenticated(res, req); + if (!hasPermission) return; + + if (!Game.listeningServer) return res.cork(() => res.writeStatus("400").end("Server is offline!")); + if (!maintenanceTimeSeconds) return res.cork(() => res.writeStatus("400").end("No approximate time was provided!")); + if (!timestamp) return res.cork(() => res.writeStatus("400").end("No timestamp was provided!")); + + const parsedTimestamp = parseInt(timestamp); + if (parsedTimestamp < Date.now()) return res.cork(() => res.writeStatus("400").end("Incorrect timestamp was provided!")); + + const event = { + fn: Game.disconnectAll.bind(Game), + arguments: [] + }; + + Game.eventsFixed.add(parsedTimestamp, event); + + const packet = MaintenanceScheduledPacket({ + timestamp: parsedTimestamp / 1000, + timeSeconds: maintenanceTimeSeconds + }); + + Game.maps.forEach(map => app.publish(map.id.toString(), packet, true, true)); + + res.cork(() => res + .writeStatus("200") + .end("Shutdown has been scheduled!")); +}; \ No newline at end of file diff --git a/Routes/api/Game/State.js b/Routes/api/Game/State.js new file mode 100644 index 0000000..1f079b0 --- /dev/null +++ b/Routes/api/Game/State.js @@ -0,0 +1,20 @@ +const Game = require("../../../Game/Game"); + +const isAuthenticated = require("../../../Utility/isAuthenticated"); + +module.exports = async (res, req) => { + res.onAborted(() => res.isAborted = true); + + const state = req.getQuery("state"); + + const hasPermission = await isAuthenticated(res, req); + if (!hasPermission) return; + + if (!state) return res.cork(() => res.writeStatus("400").end("Missing new game state!")); + + Game.state = parseInt(state); + + res.cork(() => res + .writeStatus("200") + .end(`New game state is set to be ${state}`)); +}; \ No newline at end of file diff --git a/Routes/api/Payment/payment_intent.js b/Routes/api/Payment/payment_intent.js new file mode 100644 index 0000000..abf67ef --- /dev/null +++ b/Routes/api/Payment/payment_intent.js @@ -0,0 +1,73 @@ +const redis = require("../../../Miscellaneous/redis"); +const stripe = require("../../../Miscellaneous/stripe"); + +const entries = require("../../../Data/payment"); + +function badRequest(res) { + console.trace("asd") + res.cork(() => res + .writeStatus("400") + .end()); +}; + +module.exports = async (res, req) => { + const paymentPacketID = req.getQuery("paymentPacketId"); + console.info("payment_intent"); + const cookieHeader = req.getHeader("cookie"); + const cookie = cookieHeader.split("ps=")[1]; + + res.onAborted(() => console.log("aborted")); + + if (!cookie) return badRequest(res); + + const paymentPacket = entries[paymentPacketID]; + if (!paymentPacketID || !paymentPacket) return badRequest(res); + + const formattedCookie = cookie.split(".")[0].slice(4); + const cookieInDB = await redis.get(`sess:${formattedCookie}`); + + if (!cookieInDB) return badRequest(res); + + const parsedCookie = JSON.parse(cookieInDB); + const { accountID } = parsedCookie; + + const existingClientSecret = await redis.get(`o_${accountID}_${paymentPacketID}`); + let clientSecret = null; + + try { + const { amount, unit_amount } = paymentPacket; + if (!existingClientSecret) { + const paymentIntent = await stripe.paymentIntents.create({ + currency: "EUR", + amount: unit_amount, + automatic_payment_methods: { + enabled: true + }, + metadata: { + paymentPacketID, + playerID: accountID + } + }); + + clientSecret = paymentIntent.client_secret; + await redis.set(`o_${accountID}_${paymentPacketID}`, clientSecret); + } else { + clientSecret = existingClientSecret; + }; + + const response = JSON.stringify({ + clientSecret, + amount, + price: unit_amount + }); + + res.cork(() => { + res + .writeStatus("200") + .end(response); + }); + } catch (e) { + console.info(e) + return badRequest(res); + }; +}; \ No newline at end of file diff --git a/Routes/api/Payment/webhook.js b/Routes/api/Payment/webhook.js new file mode 100644 index 0000000..13b449c --- /dev/null +++ b/Routes/api/Payment/webhook.js @@ -0,0 +1,75 @@ +const RewardPacket = require("../../../Packets/Reward"); + +const Game = require("../../../Game/Game"); + +const stripe = require("../../../Miscellaneous/stripe"); +const redis = require("../../../Miscellaneous/redis"); + +const entries = require("../../../Data/payment"); +const STRIPE_IP_COLLECTION = require("../../../Data/stripe"); + +require("dotenv").config({ path: "../.env" }); + +const textDecoder = new TextDecoder(); + +const PAYMENT_INTENT_SUCCEEDED = "payment_intent.succeeded"; + +function badRequest(res) { + res + .writeStatus("400") + .end(); +}; + +module.exports = async (res, req) => { + const sig = req.getHeader("stripe-signature"); + if (!sig) return badRequest(res); + + const realIP = req.getHeader("x-real-ip"); + if (!STRIPE_IP_COLLECTION[realIP]) return badRequest(res); + + res.onAborted(() => console.log("/webhook path aborted connection!")); + + let data; + + // it works because there is no multiple chunks, but should address this issue + res.onData(async (raw, isLast) => { + const stringData = textDecoder.decode(raw); + + try { + data = stripe.webhooks.constructEvent(stringData, sig, process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET); + } catch (err) { + console.log(err.message) + return badRequest(res); + }; + + if (data.type === PAYMENT_INTENT_SUCCEEDED) { + const { paymentPacketID, playerID } = data.data.object.metadata; + if (!paymentPacketID || !playerID) return badRequest(res); + + const paymentPackage = entries[paymentPacketID]; + const { amount } = paymentPackage; + + const player = Game.players[playerID]; + if (player) { + const boughtItem = { + categoryID: 1, + id: 4, + amount + }; + + player.inventory.refreshSingle(boughtItem); + + const initializedPacket = RewardPacket.init(true, 1); + initializedPacket.update(boughtItem); + + const packet = initializedPacket.get(); + player.map.registerIndividualNetworkPacket(player.id, packet); + }; + + await redis.del(`o_${playerID}_${paymentPacketID}`); + await execute("UPDATE economy SET corsaireCoins = corsaireCoins + ? WHERE playerID = ?", [amount, playerID]); + }; + + res.cork(() => res.writeStatus("200").end()); + }); +}; \ No newline at end of file diff --git a/Routes/api/Player/Kick.js b/Routes/api/Player/Kick.js new file mode 100644 index 0000000..3abfb9f --- /dev/null +++ b/Routes/api/Player/Kick.js @@ -0,0 +1,23 @@ +const Game = require("../../../Game/Game"); + +const isAuthenticated = require("../../../Utility/isAuthenticated"); + +module.exports = async (res, req) => { + res.onAborted(() => res.isAborted = true); + + const playerID = req.getQuery("playerId"); + + const hasPermission = await isAuthenticated(res, req); + if (!hasPermission) return; + + if (!playerID) return res.cork(() => res.writeStatus("400").end("Missing player id")); + + const player = Game.players[playerID]; + if (!player) return res.cork(() => res.writeStatus("400").end("Unknown player")); + + player.disconnect(); + + res.cork(() => res + .writeStatus("200") + .end("Player has been disconnected")); +}; \ No newline at end of file diff --git a/Routes/api/Shop/Add.js b/Routes/api/Shop/Add.js new file mode 100644 index 0000000..0147132 --- /dev/null +++ b/Routes/api/Shop/Add.js @@ -0,0 +1,65 @@ +// const Game = require("../../../Game/Game"); + +// const isAuthenticated = require("../../../Utility/isAuthenticated"); + +const ShopEntryPacket = require("../../../Packets/ShopEntry"); + +const BASE_SHOP = require("../../../Data/shop"); + +const { SHOP } = BASE_SHOP; + +module.exports = (res, req) => { + res.onAborted(() => res.isAborted = true); + console.info("hit") + let buffer = Buffer.alloc(0); + + res.onData(async (chunk, isLast) => { + buffer = Buffer.concat([buffer, Buffer.from(chunk)]); + + if (isLast) { + const body = buffer.toString("utf8"); + + let data = {}; + + try { + data = JSON.parse(body); + } catch { + console.info("error") + res + .writeStatus("400 Bad Request") + .end(); + }; + + console.info(data); + + const shopEntryAddQuery = await execute("INSERT INTO shop (itemID, categoryID, currencyType, currencyAmount, unit) VALUES (?, ?, ?, ?, ?)", [data.itemID, data.categoryID, data.currencyType, data.currencyAmount, data.unit]); + if (!shopEntryAddQuery.data) { + return res + .writeStatus("400 Bad Request") + .end(); + }; + + const shopEntry = { + id: shopEntryAddQuery.data.insertId, + itemID: parseInt(data.itemID), + categoryID: parseInt(data.categoryID), + currencyType: parseInt(data.currencyType), + currencyAmount: parseInt(data.currencyAmount), + unit: parseInt(data.unit), + shopCategoryID: 2, + isAvailable: () => true + }; + + console.info(shopEntryAddQuery.data); + + SHOP.push(shopEntry); + + const shopEntryPacket = ShopEntryPacket(shopEntry); + [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20].forEach(mapID => app.publish(mapID.toString(), shopEntryPacket, true, true)); + + res.cork(() => res + .writeStatus("200") + .end("OK")); + }; + }); +}; \ No newline at end of file diff --git a/Routes/api/Shop/Remove.js b/Routes/api/Shop/Remove.js new file mode 100644 index 0000000..f9a58fc --- /dev/null +++ b/Routes/api/Shop/Remove.js @@ -0,0 +1,38 @@ +// const Game = require("../../../Game/Game"); + +// const isAuthenticated = require("../../../Utility/isAuthenticated"); + +const ShopEntryRemovedPacket = require("../../../Packets/ShopEntryRemoved"); + +const BASE_SHOP = require("../../../Data/shop"); + +const { SHOP } = BASE_SHOP; + +module.exports = async (res, req) => { + res.onAborted(() => res.isAborted = true); + + const shopEntryID = req.getParameter(0); + if (!shopEntryID) return; + + const shopEntryRemoveQuery = await execute("DELETE FROM shop WHERE id = ?", [shopEntryID]); + if (!shopEntryRemoveQuery.data) return; + + const shopEntryIndex = SHOP.findIndex(entry => entry.id === parseInt(shopEntryID)); + if (shopEntryIndex === -1) return; + + const { shopCategoryID, itemID } = SHOP[shopEntryIndex]; + + SHOP.splice(shopEntryIndex, 1); + + const shopEntryRemoved = { + shopCategoryID: 4, + id: itemID + }; + + const shopEntryRemovedPacket = ShopEntryRemovedPacket(shopEntryRemoved); + [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20].forEach(mapID => app.publish(mapID.toString(), shopEntryRemovedPacket, true, true)); + + res.cork(() => res + .writeStatus("200") + .end("OK")); +}; \ No newline at end of file diff --git a/Timers/Behaviour.js b/Timers/Behaviour.js new file mode 100644 index 0000000..8b88425 --- /dev/null +++ b/Timers/Behaviour.js @@ -0,0 +1,49 @@ +const leaderboards = require("../Data/leaderboard"); + +const BATCH_SIZE = 1000; +const INTERVAL_TIME = 10000; + +const playerBehaviourContext = global.playerBehaviourContext; +let mongoBehaviourCollection = null; +let mongoDB = null; + +const { MongoClient } = require("mongodb"); +async function connect() { + const client = new MongoClient("mongodb://localhost:27017"); + await client.connect({ + maxPoolSize: 100, + minPoolSize: 20, + maxConnecting: 20, + waitQueueTimeoutMS: 0, + socketTimeoutMS: 30000, + connectTimeoutMS: 10000, + retryWrites: false, + directConnection: true + }); + + return client.db("test"); +}; + +(async function() { + mongoDB = await connect(); + mongoBehaviourCollection = mongoDB.collection("Behaviour"); +})(); + +async function behaviourLoop() { + if (playerBehaviourContext.length === 0) return; + //require("../Miscellaneous/winston").error(playerBehaviourContext); + // console.info(playerBehaviourContext, "loop"); + const playerBehaviourContextCopy = playerBehaviourContext.slice(); + + mongoBehaviourCollection.insertMany(playerBehaviourContextCopy, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + playerBehaviourContext.length = 0; +}; + +setInterval(behaviourLoop, INTERVAL_TIME); + +module.exports = behaviourLoop; \ No newline at end of file diff --git a/Timers/Leaderboard.js b/Timers/Leaderboard.js new file mode 100644 index 0000000..60c9659 --- /dev/null +++ b/Timers/Leaderboard.js @@ -0,0 +1,49 @@ +const leaderboards = require("../Data/leaderboard"); + +const BATCH_SIZE = 1000; +const INTERVAL_TIME = 60000; + +async function leaderboardLoop() { + for (let i = 0, length = leaderboards.length; i < length; i++) { + const leaderboard = leaderboards[i]; + const databaseName = `leaderboard_${i + 1}`; + const queries = []; + const entries = []; + + const dbConnection = await db.getConnection(); + await dbConnection.beginTransaction(); + + leaderboard.list.clearEach((key, value) => entries.push([key, value])); + + for (let j = 0, length = Math.ceil(entries.length / BATCH_SIZE); j < length; j++) { + const batch = entries.slice(j * BATCH_SIZE, (j + 1) * BATCH_SIZE); + let leaderboardFromDB = null; + + if (i === 0) leaderboardFromDB = dbConnection.query(`INSERT INTO ${databaseName} (id, amount) VALUES ? ON DUPLICATE KEY UPDATE amount = VALUES(amount)`, [batch]); + else leaderboardFromDB = dbConnection.query(`INSERT INTO ${databaseName} (id, amount) VALUES ? ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)`, [batch]); + + queries.push(leaderboardFromDB); + }; + + try { + await Promise.all(queries); + await dbConnection.commit(); + } catch { + await dbConnection.rollback(); + } finally { + dbConnection.release(); + }; + + const leaderboardSizeFromDB = await execute(`SELECT COUNT(*) AS size FROM ${databaseName}`); + leaderboard.size = leaderboardSizeFromDB.data ? leaderboardSizeFromDB.data[0].size : 0; + leaderboard.cache = {}; + }; +}; + +setInterval(leaderboardLoop, INTERVAL_TIME); + +module.exports = leaderboardLoop; + +/* + Periodically saves leaderboard changes to disk in batches +*/ \ No newline at end of file diff --git a/Timers/Resource.js b/Timers/Resource.js new file mode 100644 index 0000000..e281291 --- /dev/null +++ b/Timers/Resource.js @@ -0,0 +1,29 @@ +const [playerEconomy, guildEconomy] = require("../Data/batchSave"); + +const BATCH_SIZE = 1000; +const INTERVAL_TIME = 60000; + +function resourceLoop() { + const guildValues = []; + const playerValues = []; + + guildEconomy.clearEach((key, value) => { + const [guildID, categoryID, rewardID] = key.split("-"); + guildValues.push([guildID, categoryID, rewardID, value]); + }); + + if (guildValues.length) query("INSERT INTO guildresources (guildID, categoryID, itemID, amount) VALUES ? ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)", [guildValues]); + + playerEconomy.clearEach((key, value) => { + const [playerID, categoryID, rewardID] = key.split("-"); + playerValues.push([playerID, categoryID, rewardID, value]); + }); + + if (playerValues.length) query("INSERT INTO resources (playerID, categoryID, itemID, amount) VALUES ? ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)", [playerValues]); + + console.info(guildValues.length, playerValues.length, "periodic save") +}; + +setInterval(resourceLoop, INTERVAL_TIME); + +module.exports = resourceLoop; \ No newline at end of file diff --git a/Utility/DoubleLinkedList.js b/Utility/DoubleLinkedList.js new file mode 100644 index 0000000..0e9af67 --- /dev/null +++ b/Utility/DoubleLinkedList.js @@ -0,0 +1,134 @@ +class DoubleLinkedListNode { + constructor(key, value) { + this.key = key; + this.value = value; + this.next = null; + this.prev = null; + }; +}; + +class DoubleLinkedList { + constructor() { + this.map = new Map(); + this.head = null; + this.tail = null; + }; + + getElement(id) { + const element = this.map.get(id); + if (!element) return; + + return element.value; + } + + hasElement(id) { + return this.map.has(id); + } + + isEmpty() { + return this.head === null; + } + + addFirst(key, value) { + if (this.map.has(key)) this.removeItem(key); + + const newNode = new DoubleLinkedListNode(key, value); + + const oldHead = this.head; + this.head = newNode; + this.head.prev = null; + + if (oldHead) { + oldHead.prev = newNode; + this.head.next = oldHead; + }; + + this.map.set(key, { + value, + node: newNode + }); + }; + + accumulateItem(key, value) { + if (isNaN(value)) return; + + const entry = this.map.get(key); + const updatedValue = entry ? entry.value + value : value; + + this.addItem(key, updatedValue); + } + + addItem(key, value) { + if (this.map.has(key)) this.removeItem(key); + + const temp = new DoubleLinkedListNode(key, value); + + if (!this.head) { + this.head = temp; + this.tail = temp; + } else { + this.tail.next = temp; + temp.prev = this.tail; + this.tail = this.tail.next; + }; + + this.map.set(key, { + value, + node: temp + }); + + return value; + } + + removeItem(key) { + const entry = this.map.get(key); + if (!entry) return; + + const node = entry.node; + if (node.prev) node.prev.next = node.next; + if (node.next) node.next.prev = node.prev; + + if (node === this.head) this.head = node.next; + if (node === this.tail) this.tail = node.prev; + + this.map.delete(key); + + return entry; + } + + each(cb) { + if (this.isEmpty()) return; + if (!cb) return; + + let isRunning = true; + let current = this.head; + + while (current && (isRunning === undefined || isRunning)) { // I'm afraid this would backfire one day + isRunning = cb(current.key, current.value); + + current = current.next; + }; + + return isRunning; + } + + clearEach(cb) { + if (this.isEmpty()) return; + if (!cb) return; + + let current = this.head; + while (current) { + cb(current.key, current.value); + + current.prev = null; + current = current.next; + }; + + this.head = null; + this.tail = null; + + this.map.clear(); + } +}; + +module.exports = DoubleLinkedList; \ No newline at end of file diff --git a/Utility/LinkedList.js b/Utility/LinkedList.js new file mode 100644 index 0000000..6eee002 --- /dev/null +++ b/Utility/LinkedList.js @@ -0,0 +1,73 @@ +class LinkedListNode { + constructor(key, value) { + this.key = key; + this.value = value; + this.next = null; + }; +}; + +class LinkedList { + constructor() { + this.map = new Map(); + this.head = null; + this.tail = null; + }; + + getElement(id) { + const element = this.map.get(id); + if (!element) return; + + return element.value; + } + + hasElement(id) { + return this.map.has(id); + } + + isEmpty() { + return this.head === null; + } + + addItem(key, value) { + const temp = new LinkedListNode(key, value); + + if (!this.head) { + this.head = temp; + this.tail = temp; + } else { + this.tail.next = temp; + this.tail = temp; + }; + + this.map.set(key, temp); + } + + each(cb) { + if (this.isEmpty()) return; + + let current = this.head; + while (current) { + cb(current.key, current.value); + + current = current.next; + }; + } + + clearEach(cb) { + if (this.isEmpty()) return; + + let current = this.head; + while (current) { + cb(current.key, current.value); + + current = current.next; + }; + + this.head = null; + this.tail = null; + + this.map.clear(); + } +}; + +module.exports = LinkedList; \ No newline at end of file diff --git a/Utility/PriorityQueue.js b/Utility/PriorityQueue.js new file mode 100644 index 0000000..8b7947a --- /dev/null +++ b/Utility/PriorityQueue.js @@ -0,0 +1,30 @@ +class PriorityQueue { + constructor() { + this.items = []; + }; + + enqueue(item, priority) { + const element = { item, priority }; + let added = false; + + for (let i = 0; i < this.items.length; i++) { + if (priority < this.items[i].priority) { + this.items.splice(i, 0, element); + added = true; + break; + }; + }; + + if (!added) this.items.push(element); + } + + dequeue() { + return this.isEmpty() ? null : this.items.shift().item; + } + + isEmpty() { + return this.items.length === 0; + } +}; + +module.exports = PriorityQueue; diff --git a/Utility/Q.js b/Utility/Q.js new file mode 100644 index 0000000..d53162e --- /dev/null +++ b/Utility/Q.js @@ -0,0 +1,273 @@ + +'use strict'; + +var defaultcomparator = function (a, b) { + return a < b; +}; + +// the provided comparator function should take a, b and return *true* when a < b +function FastPriorityQueue(comparator) { + if (!(this instanceof FastPriorityQueue)) return new FastPriorityQueue(comparator); + this.array = []; + this.size = 0; + this.compare = comparator || defaultcomparator; +} + +// copy the priority queue into another, and return it. Queue items are shallow-copied. +// Runs in `O(n)` time. +FastPriorityQueue.prototype.clone = function() { + var fpq = new FastPriorityQueue(this.compare); + fpq.size = this.size; + fpq.array = this.array.slice(0, this.size); + return fpq; +}; + +// Add an element into the queue +// runs in O(log n) time +FastPriorityQueue.prototype.add = function(value, obj) { + var i = this.size; + this.array[this.size] = { value, obj }; + this.size += 1; + + var p; + var ap; + + while (i > 0) { + p = (i - 1) >> 1; + ap = this.array[p]; + if (!this.compare(value, ap.value)) break; + + this.array[i] = ap; + i = p; + }; + + this.array[i] = { value, obj }; +}; + +// replace the content of the heap by provided array and "heapify it" +FastPriorityQueue.prototype.heapify = function(arr) { + this.array = arr; + this.size = arr.length; + var i; + for (i = this.size >> 1; i >= 0; i--) { + this._percolateDown(i); + } +}; + +// for internal use +FastPriorityQueue.prototype._percolateUp = function(i, force) { + var myval = this.array[i]; + var p; + var ap; + while (i > 0) { + p = (i - 1) >> 1; + ap = this.array[p]; + // force will skip the compare + if (!force && !this.compare(myval.value, ap.value)) { + break; + } + this.array[i] = ap; + i = p; + } + this.array[i] = myval; +}; + +// for internal use +FastPriorityQueue.prototype._percolateDown = function(i) { + var size = this.size; + var hsize = this.size >>> 1; + var ai = this.array[i]; + var l; + var r; + var bestc; + while (i < hsize) { + l = (i << 1) + 1; + r = l + 1; + bestc = this.array[l]; + if (r < size) { + if (this.compare(this.array[r].value, bestc.value)) { + l = r; + bestc = this.array[r]; + } + } + if (!this.compare(bestc.value, ai.value)) { + break; + } + this.array[i] = bestc; + i = l; + } + this.array[i] = ai; +}; + +// internal +// _removeAt(index) will remove the item at the given index from the queue, +// retaining balance. returns the removed item, or undefined if nothing is removed. +FastPriorityQueue.prototype._removeAt = function(index) { + if (index > this.size - 1 || index < 0) return undefined; + + // impl1: + //this.array.splice(index, 1); + //this.heapify(this.array); + // impl2: + this._percolateUp(index, true); + return this.poll(); +}; + +// remove(myval) will remove an item matching the provided value from the +// queue, checked for equality by using the queue's comparator. +// return true if removed, false otherwise. +FastPriorityQueue.prototype.remove = function(myval) { + for (var i = 0; i < this.size; i++) { + if (!this.compare(this.array[i], myval) && !this.compare(myval, this.array[i])) { + // items match, comparator returns false both ways, remove item + this._removeAt(i); + return true; + } + } + return false; +}; + +// removeOne(callback) will execute the callback function for each item of the queue +// and will remove the first item for which the callback will return true. +// return the removed item, or undefined if nothing is removed. +FastPriorityQueue.prototype.removeOne = function(callback) { + if (typeof callback !== "function") { + return undefined; + } + for (var i = 0; i < this.size; i++) { + if (callback(this.array[i])) { + return this._removeAt(i); + } + } +}; + +// remove(callback[, limit]) will execute the callback function for each item of +// the queue and will remove each item for which the callback returns true, up to +// a max limit of removed items if specified or no limit if unspecified. +// return an array containing the removed items. +// The callback function should be a pure function. +FastPriorityQueue.prototype.removeMany = function(callback, limit) { + // Skip unnecessary processing for edge cases + if (typeof callback !== "function" || this.size < 1) { + return []; + } + limit = limit ? Math.min(limit, this.size) : this.size; + + // Prepare the results container to hold up to the results limit + var resultSize = 0; + var result = new Array(limit); + + // Prepare a temporary array to hold items we'll traverse through and need to keep + var tmpSize = 0; + var tmp = new Array(this.size); + + while (resultSize < limit && !this.isEmpty()) { + // Dequeue items into either the results or our temporary array + var item = this.poll(); + if (callback(item)) { + result[resultSize++] = item; + } else { + tmp[tmpSize++] = item; + } + } + // Update the result array with the exact number of results + result.length = resultSize; + + // Re-add all the items we can keep + var i = 0; + while (i < tmpSize) { + const elem = tmp[i++]; + this.add(elem.value, elem.obj); + } + + return result; +}; + +// Look at the top of the queue (one of the smallest elements) without removing it +// executes in constant time +// +// Calling peek on an empty priority queue returns +// the "undefined" value. +// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined +// +FastPriorityQueue.prototype.peek = function() { + if (this.size == 0) return undefined; + return this.array[0]; +}; + +// remove the element on top of the heap (one of the smallest elements) +// runs in logarithmic time +// +// If the priority queue is empty, the function returns the +// "undefined" value. +// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined +// +// For long-running and large priority queues, or priority queues +// storing large objects, you may want to call the trim function +// at strategic times to recover allocated memory. +FastPriorityQueue.prototype.poll = function() { + if (this.size == 0) return undefined; + var ans = this.array[0]; + + if (this.size > 1) { + this.array[0] = this.array[--this.size]; + this._percolateDown(0); + } else { + this.size -= 1; + } + return ans; +}; + +// This function adds the provided value to the heap, while removing +// and returning one of the smallest elements (like poll). The size of the queue +// thus remains unchanged. +FastPriorityQueue.prototype.replaceTop = function(myval) { + if (this.size == 0) return undefined; + var ans = this.array[0]; + this.array[0] = myval; + this._percolateDown(0); + return ans; +}; + +// recover unused memory (for long-running priority queues) +FastPriorityQueue.prototype.trim = function() { + this.array = this.array.slice(0, this.size); +}; + +// Check whether the heap is empty +FastPriorityQueue.prototype.isEmpty = function() { + return this.size === 0; +}; + +FastPriorityQueue.prototype.forEach = function(callback) { + if (this.isEmpty() || typeof callback != 'function') return; + var i = 0; + var fpq = this.clone(); + let isRunning = true; + + while (!fpq.isEmpty() && isRunning) { + isRunning = callback(fpq.poll(), i++); + } +}; + +// return the k 'smallest' elements of the queue as an array, +// runs in O(k log k) time, the elements are not removed +// from the priority queue. +FastPriorityQueue.prototype.kSmallest = function(k) { + if ((this.size == 0) || (k<=0)) return []; + k = Math.min(this.size, k); + const newSize = Math.min(this.size, (2 ** (k - 1)) + 1); + if (newSize < 2) { return [this.peek()] } + + const fpq = new FastPriorityQueue(this.compare); + fpq.size = newSize; + fpq.array = this.array.slice(0, newSize); + + const smallest = new Array(k); + for (let i = 0; i < k; i++) { + smallest[i] = fpq.poll(); + } + return smallest; +} + +module.exports = FastPriorityQueue; \ No newline at end of file diff --git a/Utility/Router.js b/Utility/Router.js new file mode 100644 index 0000000..619f630 --- /dev/null +++ b/Utility/Router.js @@ -0,0 +1,71 @@ +class Router { + constructor(base) { + this.routes = []; + this.base = base; + }; + + addRoute(method, path, handler) { + const paramNames = []; + const regexPath = path + .replace(/:([^/]+)/g, (match, paramName) => { + paramNames.push(paramName); + return '([^/]+)'; + }) + .replace(/\*/g, '.*'); + + this.routes.push({ + method, + path, + regex: new RegExp(`^${regexPath}$`), + paramNames, + handler + }); + } + + get(path, handler) { + this.addRoute("GET", this.base ? this.base + path : path, handler); + console.info(this.base ? this.base + path : path) + } + + post(path, handler) { + this.addRoute("POST", this.base ? this.base + path : path, handler); + console.info("POST", this.base ? this.base + path : path) + + } + + put(path, handler) { + this.addRoute("PUT", this.base ? this.base + path : path, handler); + } + + delete(path, handler) { + this.addRoute("DELETE", this.base ? this.base + path : path, handler); + } + + handle(res, req) { + const method = req.getMethod().toUpperCase(); + const urlPrepare = req.getUrl().split("/"); + const urlRelevant = urlPrepare.slice(2, urlPrepare.length); + const url = "/" + urlRelevant.join("/"); + console.info("hm?", method) + for (const route of this.routes) { + if (route.method === method) { + const match = url.match(route.regex); + console.info(url, route, method) + if (!match) continue; + + const params = {}; + route.paramNames.forEach((name, index) => params[name] = match[index + 1]); + + req.params = params; + + return route.handler(res, req); + }; + }; + + res + .writeStatus("404 Not Found") + .end("Invalid route"); + } +}; + +module.exports = Router; \ No newline at end of file diff --git a/Utility/chooseWeightedRandom.js b/Utility/chooseWeightedRandom.js new file mode 100644 index 0000000..05481fe --- /dev/null +++ b/Utility/chooseWeightedRandom.js @@ -0,0 +1,15 @@ +function chooseWeightedRandom(items, weights) { + let i; + let length = weights.length; + weights = [...weights]; + + for (i = 0; i < length; i++) weights[i] += weights[i - 1] || 0; + + const random = Math.random() * weights[length - 1]; + + for (i = 0; i < length; i++) if (weights[i] > random) break; + + return items[i]; +}; + +module.exports = chooseWeightedRandom; \ No newline at end of file diff --git a/Utility/extractBehaviourFeatures.js b/Utility/extractBehaviourFeatures.js new file mode 100644 index 0000000..351336c --- /dev/null +++ b/Utility/extractBehaviourFeatures.js @@ -0,0 +1,76 @@ +function extractBehaviourFeatures(actions) { + if (!actions || actions.length < 2) return null; + + const actionCounts = {}; + const idleThreshold = 5000; + const windowSize = 5; + const intervals = []; + const sequenceBuffer = []; + let totalInterval = 0; + let minInterval = Infinity; + let maxInterval = -Infinity; + let idleTimeCount = 0; + let idleTimeTotal = 0; + let lastTimestamp = actions[0].timestamp; + let prevAction = actions[0].opcode; + let sequenceMap = {}; + + for (let i = 1; i < actions.length; i++) { + const current = actions[i]; + const interval = current.timestamp - lastTimestamp; + lastTimestamp = current.timestamp; + + intervals.push(interval); + + totalInterval += interval; + minInterval = Math.min(minInterval, interval); + maxInterval = Math.max(maxInterval, interval); + + actionCounts[current.opcode] = (actionCounts[current.opcode] || 0) + 1; + + if (interval > idleThreshold) { + idleTimeCount++; + idleTimeTotal += interval; + }; + + sequenceBuffer.push(current.opcode); + if (sequenceBuffer.length === windowSize) { + const key = sequenceBuffer.join(","); + sequenceMap[key] = (sequenceMap[key] || 0) + 1; + + sequenceBuffer.shift(); + }; + + prevAction = current.opcode; + }; + + const meanInterval = totalInterval / intervals.length; + const stdDevInterval = Math.sqrt(intervals.reduce((sum, val) => sum + (val - meanInterval) ** 2, 0) / intervals.length); + + const uniqueActionsCount = Object.keys(actionCounts).length; + const idleTimeRatio = idleTimeCount / (actions.length - 1); + const maxSequenceRepeats = Math.max(...Object.values(sequenceMap)) || 0; + + const totalActions = actions.length; + let entropy = 0; + + for (const count of Object.values(actionCounts)) { + const p = count / totalActions; + entropy -= p * Math.log2(p); + }; + + return { + timestamp: new Date(), + timestampStart: null, + meanInterval, + stdDevInterval, + minInterval, + maxInterval, + uniqueActionsCount, + actionEntropy: entropy, + maxSequenceRepeats, + idleTimeRatio + }; +}; + +module.exports = extractBehaviourFeatures; \ No newline at end of file diff --git a/Utility/getFixedDate.js b/Utility/getFixedDate.js new file mode 100644 index 0000000..f8e8f56 --- /dev/null +++ b/Utility/getFixedDate.js @@ -0,0 +1,5 @@ +function getFixedDate(seconds) { + return Date.now() - (Date.now() % (seconds * 1000) - seconds * 1000); +}; + +module.exports = getFixedDate; \ No newline at end of file diff --git a/Utility/isAuthenticated.js b/Utility/isAuthenticated.js new file mode 100644 index 0000000..62583d0 --- /dev/null +++ b/Utility/isAuthenticated.js @@ -0,0 +1,36 @@ +const redis = require("../Miscellaneous/redis"); + +async function isAuthenticated(res, req) { + const cookieHeader = req.getHeader("cookie"); + const cookie = cookieHeader.split("ps=")[1]; + if (!cookie) { + res.cork(() => res + .writeStatus("400") + .end("Unauthenticated")); + + return false; + }; + + const formattedCookie = cookie.split(".")[0].slice(4); + const cookieInDB = await redis.get(`sess:${formattedCookie}`); + if (!cookieInDB) { + res.cork(() => res + .writeStatus("400") + .end("Unauthenticated")); + + return false; + }; + + const parsedCookie = JSON.parse(cookieInDB); + if (!parsedCookie.isAdmin) { + res.cork(() => res + .writeStatus("400") + .end("Unauthorized")); + + return false; + }; + + return true; +}; + +module.exports = isAuthenticated; \ No newline at end of file diff --git a/Utility/setTimer.js b/Utility/setTimer.js new file mode 100644 index 0000000..2f1a19e --- /dev/null +++ b/Utility/setTimer.js @@ -0,0 +1,131 @@ +(function(w) { + + /** + * The main constructor function. + * @constructor + * @param {Function} onTick The tick function. + * @param {Number} interval The tick duration. + * @returns {Timer} The instance of timer. + */ + var Timer = function(onTick, interval) { + this.setInterval(interval); + this.setOnTick(onTick); + + this._timer = null; + this._active = false; + + this._startTime = 0; + }; + + + /** + * Start the timer. + * The tick function is called with one argument, the drift amount (int). + * @returns {Timer} this. + */ + Timer.prototype.start = function() { + + let drift = 0, + actual = 0, + ideal = 0, + counter = 0, + time = 0, + that = this, + now = performance.now; + + + function onTick() { + that._onTick(drift); + + ++counter; + + ideal = that._interval * counter; + actual = now() - that._startTime; + drift = actual - ideal; + time = that._interval - drift; + + + + if (that._active) setTimeout(onTick, time); + } + + that._active = true; + that._startTime = now(); + that._timer = setTimeout(onTick, that._interval); + + return this; + }; + + + /** + * Stop the timer. + * @returns {Timer} this. + */ + Timer.prototype.stop = function() { + this._active = false; + clearTimeout(this._timer); + return this; + }; + + + /** + * Set the tick duration. Can be called whilst timer is running. + * @param {Number} ms The tick duration. + * @returns {Timer} this. + */ + Timer.prototype.setInterval = function(ms) { + if(isNaN(ms)) { + throw new Error('Timer.setInterval requires a number'); + } + else if(ms < 10) { + ms = 10; + } + + this._interval = ms; + return this; + }; + + /** + * Allow for replacement of the tick function, even whilst the timer + * is running. + * @param {Function} onTick The new tick function to be called on each cycle. + * @returns {Timer} this. + */ + Timer.prototype.setOnTick = function(onTick) { + this._onTick = onTick; + return this; + }; + + + + /** + * Allow the `Timer` constructor to be used as a replacement for + * setInterval. + * @param {Function} tick The tick function to call every `interval` ms. + * @param {Number} interval The tick duration. + * @returns {Timer} A new instance of the `Timer` constructor. + */ + var setTimer = function(tick, interval) { + var timer = new Timer(tick, interval); + timer.start(); + return timer; + }; + + + /** + * Imitate the clearTimeout method... + * @param {Timer} + */ + var clearTimer = function(timer) { + timer.stop(); + }; + + + + w.Timer = Timer; + w.setTimer = setTimer; + + + + +}(global)); \ No newline at end of file diff --git a/Worker/NPC.js b/Worker/NPC.js new file mode 100644 index 0000000..7af4505 --- /dev/null +++ b/Worker/NPC.js @@ -0,0 +1,80 @@ +const { parentPort } = require("worker_threads"); + +const grids = require("../Game/Grids"); + +parentPort.on("message", data => { + const { position, mapID, id, preSelectedDestinationNode, predefinedArea } = data; + const grid = grids[mapID - 1]; + const obj = grid.getCell(position.x, position.y); + const open = []; + const close = {}; + + let desinationNode = preSelectedDestinationNode ? preSelectedDestinationNode : grid.randomNode(10); + + while (desinationNode.x === position.x && desinationNode.y === position.y) + desinationNode = predefinedArea ? grid.randomNodeInArea(predefinedArea) : grid.randomNode(10); + + const destination = grid.getCell(desinationNode.x, desinationNode.y); + + open.push({ + xw: obj.xw, + xh: obj.xh, + gCost: 0, + hCost: 0, + fCost: 0 + }); + + while (open.length > 0) { + let current = open.reduce((prev, curr) => prev.hCost < curr.hCost ? prev : curr); // uhhhh + open.splice(open.findIndex(s => s === current), 1); // uhh + + close[`${current.xw}-${current.xh}`] = true; + + if (current.xw === destination.xw && current.xh === destination.xh) { + const path = backtrace(current, grid); + path.shift(); + + return parentPort.postMessage({ + path, + id, + mapID, + messageId: 1 + }); + }; + + const neighbours = grid.getNodeNeighbours(current); + for (let i = 0; i < neighbours.length; i++) { + const n = neighbours[i]; + if (!grid.isValidNode(n)) continue; + + let closed = close[`${n.xw}-${n.xh}`]; + if (closed) continue; + + let alreadyIn = open.find(c => c.xw === n.xw && c.xh === n.xh); + if (alreadyIn) continue; + + n.gCost = current.gCost + 1.4; + n.hCost = Math.abs(n.xw - destination.xw) + Math.abs(n.xh - destination.xh); + n.fCost = n.gCost + n.hCost; + n.parent = current; + + open.push(n); + }; + }; + + return false; +}); + +function backtrace(node, map) { + let coords = map.getCoords(node.xw, node.xh); + let path = [[coords.x, coords.y]]; + + while (node.parent) { + node = node.parent; + coords = map.getCoords(node.xw, node.xh); + path.push([coords.x, coords.y]); + }; + + return path.reverse(); +}; + diff --git a/Worker/Player.js b/Worker/Player.js new file mode 100644 index 0000000..aaece55 --- /dev/null +++ b/Worker/Player.js @@ -0,0 +1,77 @@ +const { parentPort } = require("worker_threads"); + +const PriorityQueue = require("../Utility/PriorityQueue"); + +const grids = require("../Game/Grids"); + +const COUNT_NEIGHBOURS = 4; + +parentPort.on("message", d => { + const { destination, position, id, mapId, wasMoving, movementRequestTracker } = d; + const grid = grids[mapId - 1]; + + const start = grid.getCell(position.x, position.y); + const open = new PriorityQueue(); + const openRead = {}; + const close = {}; + + open.enqueue({ + xw: start.xw, + xh: start.xh, + gCost: 0, + hCost: 0, + fCost: 0 + }, 0); + + while (open.items.length > 0) { + const current = open.dequeue(); + if (current.xw === destination.xw && current.xh === destination.xh) { + const path = backtrace(current, grid); + const startingPosition = path.shift(); + + return parentPort.postMessage({ + startingPosition, + path, + id, + mapId, + wasMoving, + movementRequestTracker + }); + }; + + close[`${current.xw}-${current.xh}`] = current; + + const neighbours = grid.getNodeNeighbours(current); + for (let i = 0; i < COUNT_NEIGHBOURS; i++) { + const n = neighbours[i]; + if (!grid.isValidNode(n)) continue; + + let closed = close[`${n.xw}-${n.xh}`]; + if (closed) continue; + + let alreadyIn = openRead[`${n.xw}-${n.xh}`]; + if (alreadyIn) continue; + + n.hCost = Math.abs(n.xw - destination.xw) + Math.abs(n.xh - destination.xh); + n.gCost = current.gCost + 1; + n.fCost = n.gCost + n.hCost; + n.parent = current; + + open.enqueue(n, n.hCost); + openRead[`${n.xw}-${n.xh}`] = n; + }; + }; +}); + +function backtrace(node, map) { + let coords = map.getCoords(node.xw, node.xh); + let path = [[coords.x, coords.y]]; + + while (node.parent) { + node = node.parent; + coords = map.getCoords(node.xw, node.xh); + path.push([coords.x, coords.y]); + }; + + return path.reverse(); +}; \ No newline at end of file diff --git a/Worker/Statistics.js b/Worker/Statistics.js new file mode 100644 index 0000000..bd60b8b --- /dev/null +++ b/Worker/Statistics.js @@ -0,0 +1,89 @@ +const { parentPort } = require("worker_threads"); +const { MongoClient } = require("mongodb"); + +let mongoDB = null; +let collectionRewards = null; +let collectionFeatures = null + +async function connect() { + const client = new MongoClient('mongodb://localhost:27017'); + await client.connect({ + maxPoolSize: 100, + minPoolSize: 20, + maxConnecting: 20, + waitQueueTimeoutMS: 0, + socketTimeoutMS: 30000, + connectTimeoutMS: 10000, + retryWrites: false, + directConnection: true + }); + + return client.db("test"); +}; +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; +(async function() { + mongoDB = await connect(); + collectionRewards = mongoDB.collection("Rewards"); + collectionFeatures = mongoDB.collection("Features"); +})(); + +const rewardsBatch = []; +const featuresBatch = []; + +const fns = { + 1: async function (data) { + rewardsBatch.push(data); + }, + 2: function (data) { + featuresBatch.push(data); + }, + 3: async function (data) { + const n = performance.now(); + + await collectionFeatures.insertMany(data, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + console.info("Written!", performance.now() - n); + } +}; + +const queue = []; + +parentPort.on("message", data => { + const fn = fns[data.id]; + if (!fn) return; + + queue.push(data); +}); + +setInterval(async () => { + const n = performance.now(); + for (let i = 0; i < queue.length; i++) { + const item = queue[i]; + fns[item.id](item.data); + }; + + queue.length = 0; + + if (rewardsBatch.length) + await collectionRewards.insertMany(rewardsBatch, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + if (featuresBatch.length) + await collectionFeatures.insertMany(featuresBatch, { + ordered: false, + writeConcern: { w: 0 }, + bypassDocumentValidation: true + }); + + rewardsBatch.length = 0; + featuresBatch.length = 0; +}, 60000); \ No newline at end of file diff --git a/metrics.js b/metrics.js new file mode 100644 index 0000000..f4388c0 --- /dev/null +++ b/metrics.js @@ -0,0 +1,17 @@ +const client = require("prom-client"); + +const collectDefaultMetrics = client.collectDefaultMetrics; +collectDefaultMetrics(); + +const onlinePlayers = new client.Gauge({ + name: "game_online_players", + help: "Number of currently online players", + collect() { + this.set(Math.floor(Math.random() * 1000)); + } +}); + +module.exports = { + register: client.register, + onlinePlayers +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..231eddd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,895 @@ +{ + "name": "Server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "avl": "^1.5.3", + "dotenv": "^16.4.1", + "mongodb": "^6.18.0", + "mysql2": "^3.9.1", + "prom-client": "^15.1.3", + "redis": "^4.6.13", + "simple-statistics": "^7.8.8", + "stripe": "^14.14.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0", + "winston": "^3.17.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.14", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.14.tgz", + "integrity": "sha512-YGn0GqsRBFUQxklhY7v562VMOP0DcmlrHHs3IV1mFE3cbxe31IITUkqhBcIhVSI/2JqtWAJXg5mjV4aU+zD0HA==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", + "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/avl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/avl/-/avl-1.5.3.tgz", + "integrity": "sha512-1qcOJeREIosJxIBof2nefabU9+Y3wCxXBb3ElQmNVOHP+Hu2BLWAh8CpU7xFnJNDMVBt2H5UOE2S/B7MEqhTsg==" + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", + "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", + "dependencies": { + "es-errors": "^1.0.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz", + "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redis": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.13.tgz", + "integrity": "sha512-MHgkS4B+sPjCXpf+HfdetBwbRz6vCtsceTmw1pHNYJAsYxrfpOP6dz+piJWGos8wqG7qb3vj/Rrc5qOlmInUuA==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.14", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.6", + "@redis/search": "1.1.6", + "@redis/time-series": "1.0.5" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-statistics": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.8.tgz", + "integrity": "sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w==", + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/stripe": { + "version": "14.14.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.14.0.tgz", + "integrity": "sha512-P6lvKHxgDzZXto9VMstG1ucv4Ls0U9nOoQhVZABXJ33kRD7zMwkBy5Y4c3BO59O230uSTkOFPLh87YxVzkp0Mg==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uWebSockets.js": { + "version": "20.48.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#51ae1d1fd92dff77cbbdc7c431021f85578da1a6" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f4fad4 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "avl": "^1.5.3", + "dotenv": "^16.4.1", + "mongodb": "^6.18.0", + "mysql2": "^3.9.1", + "prom-client": "^15.1.3", + "redis": "^4.6.13", + "simple-statistics": "^7.8.8", + "stripe": "^14.14.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0", + "winston": "^3.17.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..00cde45 --- /dev/null +++ b/server.js @@ -0,0 +1,325 @@ +"use strict"; + +// console.info = () => null; +// console.trace = () => null; +console.log = () => null; + +global.socket = require("uWebSockets.js"); +global.app = socket.App(); +global.playerBehaviourContext = []; +socket.sockets = {}; + +const { register } = require("./metrics"); + +require("./Routes/Routes"); +require("./Utility/setTimer"); +require("./Miscellaneous/mysql"); + +const MainLoop = require("./Core"); +const LeaderboardLoop = require("./Timers/Leaderboard"); +const BehaviourLoop = require("./Timers/Behaviour"); +const ResourceLoop = require("./Timers/Resource"); + +const { Worker } = require("worker_threads"); +global.PlayerWorker = new Worker("./Worker/Player.js"); +global.NPCWorker = new Worker("./Worker/NPC.js"); +global.StatisticsWorker = new Worker("./Worker/Statistics.js"); + +const messageHandler = require("./Events/message"); +const closeHandler = require("./Events/close"); +const openHandler = require("./Events/open"); +const drainHandler = require("./Events/drain"); +const upgradeHandler = require("./Events/upgrade"); + +const Game = require("./Game/Game"); +const Guild = require("./Game/Guild"); +const GuildIsland = require("./Game/GuildIsland"); + +const leaderboards = require("./Data/leaderboard"); +const queries = require("./Data/queries"); +const BASE_SHOP = require("./Data/shop"); + +const { SHOP } = BASE_SHOP; + +const formatMemoryUsage = data => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`; + +function log(level, message) { + const LOG_LEVELS = { + "OK": "32", + "FATAL": "31" + }; + + console.info(`[\x1b[33m%s\x1b[0m] \x1b[${LOG_LEVELS[level]}m%s\x1b[0m`, level, message); +}; + +function scheduleGc() { + setTimeout(() => { + let pn = performance.now(); + global.gc(); + console.info(performance.now() - pn); +/* + const memoryData = process.memoryUsage(); + const memoryUsage = { + rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`, + heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`, + heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`, + external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`, + arrayBuffers: `${formatMemoryUsage(memoryData.arrayBuffers)} -> arraybuffers`, + };*/ + +// process.exit(1); + scheduleGc(); + }, 600000); +}; + +scheduleGc(); + +const promise = new Promise(async (resolve, reject) => { + leaderboards.forEach(async (leaderboard, i) => { + const databaseName = `leaderboard_${i + 1}`; + const leaderboardSizeFromDB = await execute(`SELECT COUNT(*) AS count FROM ${databaseName}`); + if (!leaderboardSizeFromDB.data) return reject(leaderboardSizeFromDB.message); + // I don't think this is good + leaderboard.size = leaderboardSizeFromDB.data[0].count; + }); + + const shopEntries = await execute(queries.QUERY_FETCH_SHOP_ENTRIES); + if (!shopEntries.data) return reject(shopEntries.error); + + shopEntries.data.forEach(entry => { + const shopEntry = { + id: entry.id, + itemID: entry.itemID, + categoryID: entry.categoryID, + currencyType: entry.currencyType, + currencyAmount: entry.currencyAmount, + unit: entry.unit, + shopCategoryID: 2, + isAvailable: () => true + }; + + SHOP.push(shopEntry); + }); + + console.info(SHOP) + + const guildResources = await execute(queries.QUERY_FETCH_GUILD_RESOURCES); + if (!guildResources.data) return reject(guildResources.error); + + const guildDiplomacies = await execute(queries.QUERY_FETCH_GUILD_DIPLOMACIES); + if (!guildDiplomacies.data) return reject(guildDiplomacies.error); + + const guildRequests = await execute(queries.QUERY_FETCH_GUILD_REQUESTS); + if (!guildRequests.data) return reject(guildRequests.error); + + const guildMembers = await execute(queries.QUERY_FETCH_GUILD_MEMBERS); + if (!guildMembers.data) return reject(guildMembers.error); + + const guildIslandsFromDB = await execute(queries.QUERY_FETCH_GUILD_ISLANDS); + if (!guildIslandsFromDB.data) return reject(guildIslandsFromDB.error); + + const guildIslandDamagesFromDB = await execute(queries.QUERY_FETCH_GUILD_ISLAND_DAMAGES); + if (!guildIslandDamagesFromDB.data) return reject(guildIslandDamagesFromDB.error); + + const islands = guildIslandsFromDB.data.reduce((acc, cur) => { + if (acc[cur.guildID]) acc[cur.guildID].push(cur); + else acc[cur.guildID] = [cur]; + + return acc; + }, {}) + + guildMembers.data.forEach(entry => { + let guild = Game.guilds[entry.id]; + if (!guild) { + console.info("Guild", entry.id, entry.name, entry.tag) + guild = Game.guilds[entry.id] = new Guild({ + id: entry.id, + name: entry.name, + tag: entry.tag, + description: entry.description, + ownerID: entry.createdBy, + experiencePoints: entry.experiencePoints, + taxRates: [entry.taxRateGold, entry.taxRateEmerald], + timestamps: { + taxRatesEditedAt: new Date(entry.taxRatesEditedAt).getTime() / 1000, + tagEditedAt: new Date(entry.tagEditedAt).getTime() / 1000, + nameEditedAt: new Date(entry.nameEditedAt).getTime() / 1000, + descriptionEditedAt: new Date(entry.descriptionEditedAt).getTime() / 1000 + } + }); + + const currentGuildIslands = islands[entry.id]; + if (currentGuildIslands) { + for (let i = 0, length = currentGuildIslands.length; i < length; i++) { + const currentIsland = currentGuildIslands[i]; + + const island = new GuildIsland({ + id: currentIsland.id, + locationMapID: currentIsland.mapID, + index: currentIsland.gindex, + position: { + x: currentIsland.positionX, + y: currentIsland.positionY + }, + guild + }); + + Game.guildIslands.push(island); + + const islandCaptureProgress = guildIslandDamagesFromDB.data.find(e => e.guildIslandID === currentIsland.id); + if (islandCaptureProgress) island.updateReceivedDamage(islandCaptureProgress.guildID, islandCaptureProgress.damage); + }; + }; + }; + + const member = { + id: entry.playerID, + permission: entry.permission + }; + + guild.addMember(member); + }); + + guildResources.data.forEach(entry => { + const guild = Game.guilds[entry.guildID]; + switch (entry.categoryID) { + case 1: + console.info("guild resource startup", entry) + guild.bank.depositTax(entry); + break; + + case 6: + if (entry.id === 1) { + guild.levelUpdate(entry.amount); + console.info("LEVEL UPDATE", entry, guild.level, guild.experiencePoints) + }; + break; + }; + }); + + guildDiplomacies.data.forEach(entry => { + // console.info(entry, Game.guilds[entry.guildID]) + const guild = Game.guilds[entry.guildID]; + guild.diplomacy[entry.targetGuildID] = entry.color; + }); + + guildRequests.data.forEach(entry => { + const guild = Game.guilds[entry.id]; + const arrayBufferMessage = new ArrayBuffer(entry.message.length); + const convertedMessage = new Uint8Array(arrayBufferMessage); + + for (let i = 0, strLen = entry.message.length; i < strLen; i++) { + arrayBufferMessage[i] = entry.message.charCodeAt(i); + }; + + guild.addRequest(entry.playerID, arrayBufferMessage); + }); + + resolve(); +}); + +promise.then(() => { + app.ws("/ws", { + idleTimeout: 50, + maxBackpressure: 1024, + maxPayloadLength: 512, + compression: socket.DISABLED, + upgrade: upgradeHandler, + close: closeHandler, + open: openHandler, + drain: drainHandler, + message: messageHandler + }); + + app.get("/selection/:playerID", async (res, req) => { + const playerID = req.getParameter(0); + + res.onAborted(() => { + console.info("ABORTED!!!"); + }); + + const playerQuery = await execute("SELECT designID, globalRank, levelRank, mapID FROM playerdata WHERE playerID = ?", [playerID]) + console.info(playerQuery) + if (!playerQuery.success) { + console.info("t", playerQuery) + res.cork(() => res.end(JSON.stringify({ ok: false }))); + + return; + }; + + if (!playerQuery.data[0]) + playerQuery.data[0] = { + server: process.env.GAME_SERVER + }; + else playerQuery.data[0].server = process.env.GAME_SERVER; + + res.cork(() => res.end(JSON.stringify(playerQuery.data[0]))); + }); + + app.get("/metrics", async (res, req) => { + res.writeHeader("Content-Type", register.contentType); + + const metrics = await register.metrics(); + res.end(metrics); + }); + + app.listen("0.0.0.0", 9001, listenSocket => { + if (!listenSocket) { + log("FATAL", "Coudln't initialize listening to connections!"); + + process.exit(); + }; + + require("./Handlers/Map"); + + MainLoop(); + LeaderboardLoop(); + BehaviourLoop(); + ResourceLoop(); + + Game.listeningServer = listenSocket; + + log("OK", "Server is successfully initialized and is listening on *:9001!"); + if (Game.state === 0) log("OK", `Game is running in restricted mode. To let players in, set game state to be 1.`); + }); +}).catch(e => { + log("FATAL", "Couldn't initialize server, application error!", e); + console.info(e); + + process.exit(); +}); + + +/* +process.on("exit", function (code) { + console.log("exit", code) +}); + +["beforeExit", "uncaughtException", "unhandledRejection", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT" ,"SIGBUS", "SIGFPE", "SIGUSR1", "SIGSEGV", "SIGUSR2", "SIGTERM"] +.forEach(sig => { + process.on(sig, function (reason) { + if (sig === "uncaughtException" || sig === "unhandledRejection") console.log(reason); + + terminator(sig); + console.log("signal: " + sig); + }); +}); + +function terminator(sig) { + if (typeof sig === "string") { + socket.us_listen_socket_close(listeningServer); + listeningServer = null; + + Game.disconnectAll(); + + setTimeout(() => { + console.log(process.argv[0], process.argv.slice(1)) + + process.exit(1); + }, 5000); + }; + + console.log("Server has been stopped!"); +};*/ + +