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;