1537 lines
44 KiB
JavaScript
1537 lines
44 KiB
JavaScript
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;
|