Chapter 13: L2JMobius Learning – Player attacks monsters

In this chapter, we learn about the refresh of monsters around the player. In the previous chapter, we mentioned this matter. When the player finishes moving, the surrounding game objects will be displayed, including NPC monsters. Of course, when the player "spawns" himself (calls the spawnMe method), surrounding game objects will also be displayed. Let's first take a look at the spawnMe method of WorldObject when the player "hatches" himself. The important code in this method is:

World.getInstance().addVisibleObject(this, getWorldRegion(), null);

Let's continue to the World class to view the addVisibleObject method, as shown below

// 从地图上查找附近的游戏对象
final List<WorldObject> visibleObjects = getVisibleObjects(object, 2000);
for (int i = 0; i < visibleObjects.size(); i++)
{
    // 周围的对象把 当前角色"我" 加入到 _knownObjects 列表中
    wo.getKnownList().addKnownObject(object, dropper);
	
    // 当前角色"我" 把 周围对象加入到 _knownObjects 列表中
    object.getKnownList().addKnownObject(wo, dropper);
}

Let’s focus on the last line of code: object.getKnownList().addKnownObject(wo, dropper); That is, the current player adds surrounding game objects (NPC monsters) to his own _knownObjects list. What needs to be noted here is that the game player's getKnownList() method returns the PlayerKnownList class, and its addKnownObject method is as follows:

else if (object.isNpc())
{
    activeChar.sendPacket(new NpcInfo((Npc) object, activeChar));
}

This code will send different data to the player client based on the type of game object. The NpcInfo here is the data packet information corresponding to the NPC monster. Next, let’s look at the operation after the game character is moved, which is the final code part of the updatePosition method in the game character’s Creature class.

// 到达目标点之后,更新周围游戏对象
if (distFraction > 1)
{
    getKnownList().updateKnownObjects();
    ThreadPool.execute(() -> getAI().notifyEvent(CtrlEvent.EVT_ARRIVED));
    return true;
}

Let's continue to look at the updateKnownObjects method of the PlayerKnownList class. In fact, this method is located in the parent class WorldObjectKnownList. The code is as follows

if (_activeObject instanceof Creature)
{
    findCloseObjects();
    forgetObjects();
}

Let's continue to look at the findCloseObjects method, the code is as follows

if (_activeObject.isPlayable()){
for (WorldObject object : World.getInstance().getVisibleObjects(_activeObject))
{
    addKnownObject(object);
}}

Here everyone must not forget the polymorphism of Java. What we instantiate is the subclass PlayerKnownList. Even if we call the addKnownObject method in WorldObjectKnownList, it will still call the overridden addKnownObject method in PlayerKnownList. We have already introduced this method above, which is to send NpcInfo packets to the player client.

Now that there are NPC monsters around our players, we can attack them. First, we should click to select the object we want to attack (NPC monster). At this time, an Action packet will be sent to the server. This Action data package has a wide range of applications, and we will encounter it later. Let's check this Action packet.

	private int _objectId;	// 鼠标点击选中的游戏对象ID
	private int _originX;	// 玩家当前位置
	private int _originY;	// 玩家当前位置
	private int _originZ;	// 玩家当前位置

Next, we continue to look at the run method

		// 鼠标点击选中的游戏对象(根据ID查询)
		final WorldObject obj = World.getInstance().findObject(_objectId);
		obj.onAction(player);

We first find the game object instance based on the game object ID, and then call the onAction method of the game object. What should be noted here is that the onAction method of the NPC monster is called, not the onAction method of the player. Next, we go to the onAction method of the monster class Monster. In fact, this method is in its parent class Npc. Let's go to the parent class Npc to check. The parameter of this onAction method is the current player. In this method, there are two situations. One is that the Npc monster is not the target object _target of the current player Player, and the other is that the Npc monster is the target object _target of the current player Player. When we select the NPC monster for the first time, it is of course not the current player's target object, so the code in the first case is executed.

if (this != player.getTarget())
{
    // 设置当前玩家的选择目标
    player.setTarget(this);
    // 发送 MyTargetSelected 数据包
    player.sendPacket(new MyTargetSelected(getObjectId(), 0));
	// 设置开始攻击时间
	player.setTimerToAttack(System.currentTimeMillis());
    // 校验玩家当前位置
    player.sendPacket(new ValidateLocation(this));
}

The above code is to set the game object (Npc monster) that the current player has selected by mouse click, and then send the MyTargetSelected data packet to the client. In fact, it tells the client that the server has selected it and can proceed to the next step. Next, we can continue to click on the game object (Npc monster) selected by our mouse. Then, the client still sends Action data packets to the server, and still calls the onAction method of the monster class Monster. At that time, since we have set the player's target object in the previous operation, it is time to perform the second case here.

// 校验玩家当前位置
player.sendPacket(new ValidateLocation(this));
// 设置玩家为攻击状态
player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this);

The player's PlayerAI class will be called here to put it into the AI_INTENTION_ATTACK attack state. This setIntention method is actually located in the parent class AbstractAI.

case AI_INTENTION_ATTACK:
{
    onIntentionAttack((Creature) arg0);
    break;
}

The onIntentionAttack method above is actually located in the CreatureAI class, and the parameter is the attack object. There are two situations here, one is that the current player is already in an attack state (to prevent the user from clicking multiple times to attack the same target), and the other is that the current player is not in an attack state. Obviously, we belong to the latter, let's look at the corresponding code

// 改变玩家的状态
changeIntention(AI_INTENTION_ATTACK, target, null);
// 设置攻击目标
setAttackTarget(target);
// 停止移动
stopFollow();
// 执行 EVT_THINK
notifyEvent(CtrlEvent.EVT_THINK, null);

Here, we focus on the last sentence of code: notifyEvent(CtrlEvent.EVT_THINK, null); This notifyEvent method is located in the parent class AbstractAI, and the code is as follows

case EVT_THINK:
{
    onEvtThink();
    break;
}

The above onEvtThink is in the PlayerAI class. It will perform different behaviors according to different states. This onEvtThink method is actually executed in a loop. Because the player's automatic attack is executed cyclically by the AI. Then the starting position of the loop is the onEvtThink method here. So where is the code for the loop? We will understand when we look back.

if (getIntention() == AI_INTENTION_ATTACK)
{
    // 自动攻击
    thinkAttack();
}

Needless to say here, the thinkAttack method must be executed, and this method will eventually call Player's doAttack method. The code logic of this method is not much, mainly in the doAttack method in its parent class Creature, and its parameters are to be attacked. Object, we briefly introduce this method.

// 获取手持武器
final Weapon weaponItem = getActiveWeaponItem();
final Item weaponInst = getActiveWeaponInstance();

// 检查灵魂蛋使用
boolean wasSSCharged;

// 根据武器计算攻击时间
final int timeAtk = calculateTimeBetweenAttacks(target, weaponItem);

// 攻击到一半的时候,给与目标伤害
final int timeToHit = timeAtk / 2;

// 本次攻击结束时间
_attackEndTime = GameTimeTaskManager.getInstance().getGameTicks();
_attackEndTime += (timeAtk / GameTimeTaskManager.MILLIS_IN_TICK);
_attackEndTime -= 1;

// 武器的等级
int ssGrade = 0;

// 发送给客户端的攻击数据包
final Attack attack = new Attack(this, wasSSCharged, ssGrade);

// 计算下次攻击时间
final int reuse = calculateReuseTime(target, weaponItem);

// 是否产生伤害(可能miss哦)
hitted = doAttackHitSimple(attack, target, timeToHit);

// 更新玩家PVP状态
player.updatePvPStatus(target);

// miss效果
if (!hitted){
    sendPacket(new SystemMessage(SystemMessageId.YOU_HAVE_MISSED));
    abortAttack();
}

// 如果命中造成伤害就广播Attack数据包
if (attack.hasHits())
{
    broadcastPacket(attack);
}

// 定时任务执行 NotifyAITask 任务(就是执行L2PlayerAI 中的 onEvtThink 方法)
ThreadPool.schedule(new NotifyAITask(CtrlEvent.EVT_READY_TO_ACT), timeAtk + reuse);

Please note that the above NotifyAITask task will execute the onEvtThink method in L2PlayerAI. In the above description, we have said that this onEvtThink method is actually executed in a loop. When will it end? Either the player cancels the attack, or the monster dies, etc. It is sent. In fact, just cancel the player's AI_INTENTION_ATTACK state. Next, let’s briefly talk about the doAttackHitSimple method above.

// 攻击是否miss
final boolean miss1 = Formulas.calcHitMiss(this, target);

// 计算伤害值
damage1 = (int) Formulas.calcPhysDam(this, target, null, shld1, crit1, false, attack.soulshot);

// timeToHit 时间后执行 HitTask 伤害任务。攻击动作到一半的时候造成伤害。
ThreadPool.schedule(new HitTask(target, damage1, crit1, miss1, attack.soulshot, shld1), sAtk);

// 攻击数据包中添加伤害值
attack.addHit(target, damage1, miss1, crit1, shld1);

The above HitTask damage task is to execute:

onHitTimer(_hitTarget, _damage, _crit, _miss, _soulshot, _shld);

We can directly introduce the onHitTimer method.

// 发送伤害信息,就是SystemMessage 数据包。
sendDamageMessage(target, damage, false, crit, miss);

// 计算吸血(增加玩家HP)
final double absorbPercent = getStat().calcStat(Stat.ABSORB_DAMAGE_PERCENT, 0, null, null);
setCurrentHp(getStatus().getCurrentHp() + absorbDamage);

// 计算反射伤害(减少玩家HP)
final double reflectPercent = target.getStat().calcStat(Stat.REFLECT_DAMAGE_PERCENT, 0, null, null);
getStatus().reduceHp(reflectedDamage, target, true);

// 减少怪物目标HP(怪物死亡后掉落物品最为奖励)
target.reduceCurrentHp(damage, this);

// 设置怪物开始反击玩家
target.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this);

// 发送开始自动攻击数据包,就是AutoAttackStart 数据包
getAI().clientStartAutoAttack();

What everyone needs to pay attention to is that the main attack codes above are concentrated in the Creature class. We have explained this class before. It is the parent class of Player and Monster, and the movement code in it is shared. Of course, the same goes for attacks, which are shared between players and monsters. In other words, the code for the monster above to start counterattacking the player is also in the Creature class. The difference between the two is that the AI ​​categories are different. However, the AI ​​class ultimately calls the doAttack method of the Creature class. In this doAttack method, different code logic judgments will be made based on the current role instance (Player or Monster). It will not be introduced in detail here.

When the battle between the player and the monster ends, the first is the distance between the two, and the second is the death of one party. The first distance issue involves the two chasing each other. If the player escapes, the player will automatically give up the active attack state and switch to the moving state; the monster may pursue (still in a combat state). If it can be pursued, it will launch an attack. If it cannot be pursued, it will switch to the normal state (return to the birth point and enter the patrol state). The second is that if one party dies, both parties will stop automatic attacks. If a monster dies, it will drop an item. If the player dies, a pop-up box will prompt whether to resurrect in place or return to a nearby village. If one party dies, the other party will change state. For example, the player will stop the automatic attack state; the monster will also stop the automatic attack state and enter the normal state. Both sides of the battle use timers during the "automatic battle" process. To end the battle, you need to cancel the timer.

Resurrection of monsters after death is managed in the RespawnTaskManager class, which is a singleton class and also a thread. In this thread class, there is a Map<Npc, Long> PENDING_RESPAWNS collection. The Key of this set is the dead NPC, and the Long value is the time to resurrect. This thread will continuously obtain dead NPCs from the PENDING_RESPAWNS collection, and then determine whether they need to be resurrected based on the time.

// 当前时间
final long time = System.currentTimeMillis();

// 循环死亡的npc
for (Entry<Npc, Long> entry : PENDING_RESPAWNS.entrySet())
{
    // 如果到了复活的时间就复活
    if (time > entry.getValue().longValue())
    {
        // 复活npc
        spawn.respawnNpc(npc);
	}
}

The resurrection code is to call the respawnNpc method of the Spawn class. In this method, the initializeNpc(oldNpc) method is directly called to reinitialize the current npc object. We have already explained the initializeNpc method before and will not describe it here. So where is this RespawnTaskManager class called? It is in the decreaseCount method in this Spawn class,

RespawnTaskManager.getInstance().add(oldNpc, System.currentTimeMillis() + _respawnDelay);

This respawnDelay time comes from the respawn_delay field value in the spawnlist of the hatching data table. Just need to make a small setting in the Spawn class: _respawnDelay = value < 10 ? 10000 : value * 1000;

In other words, if the respawn_delay field value is less than 10, change it to 10000 (10 seconds), otherwise multiply it by 1000 (convert to millisecond units). So, who calls the decreaseCount method in this Spawn class? It's in the onDecay method of the npc class. This method is called in DecayTaskManager, which is also a singleton thread class. The call of DecayTaskManager is called in the doDie method of the npc class. After seeing the doDie method, everyone should be very clear that it is the method called when the monster dies.

The content involved in this chapter has been uploaded to Baidu Netdisk:

https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwd=avd4

Welcome to add Penguin Communication Skirt: 874700842 (you can also download all the content in the skirt file).

Guess you like

Origin blog.csdn.net/konkon2012/article/details/134000049