테스트

aion-server 4.8

Gitteol
최고관리자 · 1 · 💬 0 클론/새로받기
 4.8 61f661d · 1 commits 새로받기(Pull)
game-server/data/handlers/ai/instance/drakenspire/Lv1HumanBeritraAI.java
package ai.instance.drakenspire;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.commons.utils.Rnd;
import com.aionemu.gameserver.ai.AIName;
import com.aionemu.gameserver.ai.event.AIEventType;
import com.aionemu.gameserver.controllers.attack.AggroInfo;
import com.aionemu.gameserver.model.Race;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.Npc;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.skill.NpcSkillEntry;
import com.aionemu.gameserver.model.templates.spawns.SpawnTemplate;
import com.aionemu.gameserver.skillengine.SkillEngine;
import com.aionemu.gameserver.skillengine.model.Effect;
import com.aionemu.gameserver.skillengine.model.SkillTemplate;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.PositionUtil;
import com.aionemu.gameserver.utils.ThreadPoolManager;

import ai.AggressiveNoLootNpcAI;

/**
 * //moveto 301390000 170 530 1750
 * <br/>
 * <br/>
 * Guide:
 * The battle always begins with Beritra applying three buffs to himself, with "Everlasting Life" is always being the first.
 * The battle is effectively divided into three phases, which are triggered either by reaching a certain HP threshold, or
 * after a set interval.
 * Note: The longer the battle lasts, the more difficult it becomes.
 * <br/>
 * <br/>
 * Seal Mark:
 * - 15s buff applied when killing a Drakenspire Protector (Seal Guardian)
 * - Explodes at the end, dealing ~ 20-22k damage to all targets in range
 * - Removes one of Beritra's buffs if hit by the explosion
 * <br/>
 * <br/>
 * Drakenspire Protector (Seal Guardian):
 * - Spawns on one of the four outer platforms {@link #spawnSealGuardian()}
 * - Spawns every 60s if killed
 * - If not attacked for 60s, he will spawn on a new position
 * - When reset (BACK_HOME), he will also teleport
 * - Spawns a "Ghostly" version of himself in the arena that can respawn after 30s, if killed
 * - Applies "Seal Mark" on the player doing the last hit, if killed
 * <br/>
 * <br/>
 * Skill Pattern:
 * Phase 1: HP 100% - 71% || fight time < 6min
 * - Forgiving loop to introduce the standard skills
 * - Ends with "Pulse Wave" the first more complex mechanic (see below)
 * <br/>
 * <br/>
 * Phase 2: HP 70% - 36% || fight time 6min - 11min
 * - Introduces "Soul Extinction Field" (see below)
 * - Introduces "Dimensional Wave" (see below)
 * <br/>
 * <br/>
 * Phase 3: HP < 36% || fight time >= 11min
 * - Introduces a 4-skill chain right after "Dimensional Wave" that can easily cause a team wipe if not handled carefully
 * <br/>
 * <br/>
 * Pulse Wave ({@link #handlePulseWave()}:
 * - A Y-shaped AoE that deals ~10k damage
 * - Can be executed in one of four patterns
 * - Attacks from the center of the arena to its outer perimeter
 * <br/>
 * <br/>
 * Soul Extinction Field ({@link #handleSoulExtinctionFields()}):
 * - Targets up to three random players
 * - Spawns a field at the player's current location
 * - Activates after 4s, dealing massive damage to the players still standing inside
 * <br/>
 * <br/>
 * Dimensional Wave ({@link #handleDimensionalWave()}):
 * - A 15-second channel skill that plunges parts of the arena into dark fields.
 * - Deals up to 10k damage
 * - Always sequencing the same areas: Northern half -> Southern half -> Platforms
 * - Players should avoid getting hit twice
 * <br/>
 * <br/>
 * Lv 1 (Easy Mode) specifics:
 * - Players will receive help from NPCs after 10s of combat
 * - NPCs will sacrifice themselves and remove all buffs, causing Beritra to use "Rending Shadow" twice
 *
 * @author Estrayl
 */
@AIName("drakenspire_lv1_human_beritra")
public class Lv1HumanBeritraAI extends AggressiveNoLootNpcAI {

	private static final Logger log = LoggerFactory.getLogger("INSTANCE_LOG");
	protected final AtomicBoolean isActivated = new AtomicBoolean();
	private Future<?> spawnTask;
	private long fightStartTime;

	public Lv1HumanBeritraAI(Npc owner) {
		super(owner);
	}

	protected void handleFightStarted() {
		ThreadPoolManager.getInstance().schedule(() -> {
			if (isActivated.get()) // just in case he resets immediately
				spawnFactionHelpers(getAttackingPlayerRace() == Race.ELYOS ? List.of(209736, 209737, 209737) : List.of(209801, 209802, 209802));
		}, 10, TimeUnit.SECONDS);
	}

	@Override
	protected void handleCreatureAggro(Creature creature) {
		super.handleCreatureAggro(creature);
		if (isActivated.compareAndSet(false, true)) {
			fightStartTime = System.currentTimeMillis();
			applyBuffs();
			scheduleNewSealGuardianSpawn();
			handleFightStarted(); // Only relevant for Lv1 Beritra
		}
	}

	private void applyBuffs() {
		SkillEngine.getInstance().applyEffectDirectly(21612, getOwner(), getOwner());
		SkillEngine.getInstance().applyEffectDirectly(21611, getOwner(), getOwner());
		SkillEngine.getInstance().applyEffectDirectly(21610, getOwner(), getOwner());
	}

	private void scheduleNewSealGuardianSpawn() {
		if (isActivated.get())
			spawnTask = ThreadPoolManager.getInstance().schedule(this::spawnSealGuardian, 60, TimeUnit.SECONDS);
	}

	/**
	 * Retail sequence
	 */
	private void spawnSealGuardian() {
		if (Rnd.chance() < 25) {
			spawn(855460, 128.621f, 461.719f, 1754.576f, (byte) 15);
		} else if (Rnd.chance() < 33) {
			spawn(855461, 207.780f, 496.081f, 1754.524f, (byte) 40);
		} else if (Rnd.chance() < 50) {
			spawn(855462, 208.671f, 542.410f, 1754.609f, (byte) 67);
		} else {
			spawn(855463, 127.028f, 574.691f, 1754.681f, (byte) 103);
		}
	}

	@Override
	protected void handleCustomEvent(int eventId, Object... args) {
		switch (eventId) {
			case 1 -> scheduleNewSealGuardianSpawn();
			case 2 -> spawnSealGuardian();
		}
	}

	@Override
	public void onEffectApplied(Effect effect) {
		switch (effect.getSkillId()) {
			case 20834, 20835 -> {
				PacketSendUtility.broadcastMessage(getOwner(), 1501272); // You insects think you have a chance against me?
				handleBuffsRemovedByNpc();
			}
		}
	}

	/**
	 * Retail sequence => Beritra will immediately execute 21604 + 21603 (Rending Shadow);
	 */
	protected void handleBuffsRemovedByNpc() {
		int chainId = getOwner().getGameStats().getLastSkill().getNextChainId();
		NpcSkillEntry entry = getOwner().getSkillList().getNpcSkills().stream().filter(nse -> nse.getChainId() == chainId).findAny().orElse(null);

		getOwner().queueSkill(21604, 56, 0);
		getOwner().queueSkill(21603, 56, entry == null ? -1 : 0);
		if (entry != null)
			getOwner().getQueuedSkills().offer(entry);
	}

	/**
	 * Retail: Use one of the following patterns:
	 * 25% => 1 3 6
	 * 33% => 2 5 7
	 * 50% => 3 5 8
	 * Fix => 1 4 7
	 */
	private void handlePulseWave() {
		// 855541
		Npc skill1 = (Npc) spawn(855742, 151.9f, 518.6f, 1749.6f, (byte) 0);
		Npc skill2 = (Npc) spawn(855742, 151.9f, 518.6f, 1749.6f, (byte) 0);
		Npc skill3 = (Npc) spawn(855742, 151.9f, 518.6f, 1749.6f, (byte) 0);

		Npc slave1, slave2, slave3;
		if (Rnd.chance() < 25) {
			slave1 = (Npc) spawn(855745, 136.6f, 496.7f, 1749.9f, (byte) 0); // Pos 1
			slave2 = (Npc) spawn(855747, 137.5f, 534.4f, 1749.9f, (byte) 0); // Pos 3
			slave3 = (Npc) spawn(855750, 180.9f, 515.4f, 1749.9f, (byte) 0); // Pos 6
		} else if (Rnd.chance() < 33) {
			slave1 = (Npc) spawn(855746, 129.6f, 516.2f, 1749.9f, (byte) 0); // Pos 2
			slave2 = (Npc) spawn(855749, 173.7f, 534.0f, 1749.9f, (byte) 0); // Pos 5
			slave3 = (Npc) spawn(855751, 174.3f, 498.2f, 1749.9f, (byte) 0); // Pos 7
		} else if (Rnd.chance() < 50) {
			slave1 = (Npc) spawn(855747, 137.5f, 534.4f, 1749.9f, (byte) 0); // Pos 3
			slave2 = (Npc) spawn(855749, 173.7f, 534.0f, 1749.9f, (byte) 0); // Pos 5
			slave3 = (Npc) spawn(855752, 156.4f, 490.2f, 1749.9f, (byte) 0); // Pos 8
		} else {
			slave1 = (Npc) spawn(855745, 136.6f, 496.7f, 1749.9f, (byte) 0); // Pos 1
			slave2 = (Npc) spawn(855748, 154.6f, 540.7f, 1749.9f, (byte) 0); // Pos 4
			slave3 = (Npc) spawn(855751, 174.3f, 498.2f, 1749.9f, (byte) 0); // Pos 7
		}

		ThreadPoolManager.getInstance().schedule(() -> {
			int skillId = getPulseWaveSkillId();
			SkillEngine.getInstance().getSkill(skill1, skillId, 1, slave1).useNoAnimationSkill();
			SkillEngine.getInstance().getSkill(skill2, skillId, 1, slave2).useNoAnimationSkill();
			SkillEngine.getInstance().getSkill(skill3, skillId, 1, slave3).useNoAnimationSkill();
		}, 500);
		ThreadPoolManager.getInstance().schedule(() -> despawnNpcs(skill1, skill2, skill3, slave1, slave2, slave3), 8000);
	}

	private int getPulseWaveSkillId() {
		return getNpcId() == 236247 ? 21623 : 21828;
	}

	/**
	 * Spawn soul extinction fields on three random players within 50m.
	 */
	private void handleSoulExtinctionFields() {
		List<AggroInfo> playersInRange = getAggroList().getList().stream()
			.filter(ai -> ai.getAttacker() instanceof Player && PositionUtil.isInRange(getOwner(), (Player) ai.getAttacker(), 50))
			.collect(Collectors.toList());

		Collections.shuffle(playersInRange);
		playersInRange.stream().limit(3).forEach(ai -> {
			Player p = (Player) ai.getAttacker();
			spawn(855450, p.getX(), p.getY(), p.getZ(), (byte) 0);
		});
	}

	/**
	 * Retail: NPCs are spawned solely to display effects.
	 * There is a new zone mechanic "activate_skillarea" which should handle the calc, which player needs to be affected.
	 * <p>
	 * To keep it simple, we just calculate the effects within the NPC's AI.
	 */
	private void handleDimensionalWave() {
		// equals area_id="10"
		ThreadPoolManager.getInstance().schedule(() -> spawn(855435, 151.991f, 518.583f, 1749.5945f, (byte) 90), 3000);
		// equals area_id="20"
		ThreadPoolManager.getInstance().schedule(() -> spawn(855435, 152.002f, 518.548f, 1749.5945f, (byte) 30), 7500);
		// equals area_id="30"
		ThreadPoolManager.getInstance().schedule(() -> {
			spawn(856300, 126.977f, 574.736f, 1754.6809f, (byte) 0);
			spawn(856300, 208.552f, 542.472f, 1754.6082f, (byte) 0);
			spawn(856300, 177.031f, 458.650f, 1759.8838f, (byte) 0);
			spawn(856300, 176.057f, 579.624f, 1760.0452f, (byte) 0);
			spawn(856300, 207.733f, 496.071f, 1754.5236f, (byte) 0);
			spawn(856300, 128.722f, 461.584f, 1754.5775f, (byte) 0);
		}, 12000);
	}

	@Override
	public void onStartUseSkill(SkillTemplate st, int level) {
		switch (st.getSkillId()) {
			case 21601 -> { // Pulse Wave
				handlePulseWave();
				addHateToRandomTarget();
			}
			case 21602 -> handleDimensionalWave(); // Dimensional Wave
		}
	}

	private void addHateToRandomTarget() {
		List<AggroInfo> attackingPlayers = getAggroList().getList().stream().filter(ai -> ai.getAttacker() instanceof Player player && !player.isDead())
			.toList();
		AggroInfo aggroInfo = Rnd.get(attackingPlayers);
		if (aggroInfo != null)
			aggroInfo.addHate(100000);
	}

	@Override
	public void onEndUseSkill(SkillTemplate st, int level) {
		switch (st.getSkillId()) {
			case 21609 -> { // Soul Extinction Field
				switch (level) {
					case 57 -> PacketSendUtility.broadcastMessage(getOwner(), 1501269); // You're not too bad for an insect! | Could also be 1501274
					case 58 -> PacketSendUtility.broadcastMessage(getOwner(), 1501270); // I'm not playing anymore! | Could also be 1501275
				}
				handleSoulExtinctionFields();
			}
		}
	}

	/**
	 * Deviation to retail implementation:
	 * Guards should spawn with a distance of 8m relative to Beritra's current position.
	 * To avoid spawning them outside the arena, we will spawn them relative to the center of the arena.
	 */
	protected void spawnFactionHelpers(List<Integer> npcIds) {
		SpawnTemplate st = getSpawnTemplate();
		for (int npcId : npcIds) {
			float distance = Rnd.nextFloat() * 5;
			double angleRadians = Math.toRadians(Rnd.nextFloat(360f));
			float x = st.getX() + (float) (Math.cos(angleRadians) * distance);
			float y = st.getY() + (float) (Math.sin(angleRadians) * distance);
			Npc helper = (Npc) spawn(npcId, x, y, st.getZ(), (byte) 0);
			ThreadPoolManager.getInstance().schedule(() -> {
				helper.setTarget(getOwner());
				helper.getAggroList().addHate(getOwner(), 100000);
				helper.getMoveController().moveToTargetObject();
			}, 500);
		}
	}

	private void despawnNpcs(Npc... npcs) {
		for (Npc npc : npcs)
			if (npc != null)
				npc.getController().deleteIfAliveOrCancelRespawn();
	}

	private void despawnNpcsById(int... npcIds) {
		getPosition().getWorldMapInstance().getNpcs(npcIds).forEach(npc -> npc.getController().deleteIfAliveOrCancelRespawn());
	}

	private boolean isTargetInsideArena() {
		SpawnTemplate st = getSpawnTemplate();
		return PositionUtil.isInRange(getTarget(), st.getX(), st.getY(), st.getZ(), 28);
	}

	@Override
	protected void handleTargetTooFar() {
		if (!isTargetInsideArena()) {
			getAggroList().stopHating(getTarget());
			Creature mostHated = getAggroList().getMostHated();
			if (mostHated != null)
				onCreatureEvent(AIEventType.TARGET_CHANGED, mostHated);
			else
				onGeneralEvent(AIEventType.TARGET_GIVEUP);
			return;
		}
		super.handleTargetTooFar();
	}

	@Override
	protected void handleBackHome() {
		super.handleBackHome();
		despawnNpcsById(209734, 209735, 209736, 209737, 209799, 209800, 209801, 209802, 855446, 855460, 855461, 855462, 855463);
		cancelTasks();
		fightStartTime = 0;
		isActivated.set(false);
	}

	@Override
	protected void handleDied() {
		cancelTasks();
		logMetrics();
		super.handleDied();
	}

	@Override
	protected void handleDespawned() {
		cancelTasks();
		isActivated.set(false);
		despawnNpcsById(209734, 209735, 209736, 209737, 209799, 209800, 209801, 209802, 855446, 855460, 855461, 855462, 855463);
		super.handleDespawned();
	}

	protected Race getAttackingPlayerRace() {
		return getKnownList().getKnownPlayers().values().stream().filter(p -> !p.isStaff()).map(Player::getRace).findAny().orElse(Race.ELYOS);
	}

	private void cancelTasks() {
		if (spawnTask != null && !spawnTask.isDone())
			spawnTask.cancel(true);
	}

	private void logMetrics() {
		long fullFightTime = (System.currentTimeMillis() - fightStartTime) / 1000;
		String damageDealt = getAggroList().getFinalDamageList(false).stream().sorted((Comparator.comparingInt(AggroInfo::getDamage).reversed()))
			.map(ai -> String.format("%s (ID: %d, Dmg: %d)", ai.getAttacker().getName(), ai.getAttacker().getObjectId(), ai.getDamage()))
			.collect(Collectors.joining(", "));

		log.info("[{}] {} (ID:{}) was killed in {}s. Damage List: {}", getPosition().getWorldMapInstance().getTemplate().getName(), getOwner().getName(),
			getNpcId(), fullFightTime, damageDealt);
	}
}

📎 첨부파일

댓글 작성 권한이 없습니다.
🏆 포인트 랭킹 TOP 10
순위 닉네임 포인트
1 no_profile 타키야겐지쪽지보내기 자기소개 아이디로 검색 전체게시물 102,949
2 no_profile 동가리쪽지보내기 자기소개 아이디로 검색 전체게시물 63,733
3 no_profile 라프텔쪽지보내기 자기소개 아이디로 검색 전체게시물 51,771
4 no_profile 불멸의행복쪽지보내기 자기소개 아이디로 검색 전체게시물 36,923
5 서번트쪽지보내기 자기소개 아이디로 검색 전체게시물 35,011
6 no_profile 닥터스쪽지보내기 자기소개 아이디로 검색 전체게시물 29,470
7 no_profile 검은고양이쪽지보내기 자기소개 아이디로 검색 전체게시물 29,077
8 no_profile Revolution쪽지보내기 자기소개 아이디로 검색 전체게시물 28,199
9 no_profile 보거스쪽지보내기 자기소개 아이디로 검색 전체게시물 26,731
10 no_profile 호롤롤로쪽지보내기 자기소개 아이디로 검색 전체게시물 17,020
알림 0