763 lines
20 KiB
JavaScript
763 lines
20 KiB
JavaScript
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; |