/* * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If * not, see <http://www.gnu.org/licenses/>. */ package silentium.scripts.ai; import javolution.util.FastList; import silentium.commons.utils.Rnd; import silentium.gameserver.ThreadPoolManager; import silentium.gameserver.ai.CtrlIntention; import silentium.gameserver.ai.DefaultMonsterAI; import silentium.gameserver.configs.NPCConfig; import silentium.gameserver.geo.GeoData; import silentium.gameserver.instancemanager.GrandBossManager; import silentium.gameserver.model.L2Object; import silentium.gameserver.model.L2Skill; import silentium.gameserver.model.actor.L2Attackable; import silentium.gameserver.model.actor.L2Character; import silentium.gameserver.model.actor.L2Npc; import silentium.gameserver.model.actor.L2Playable; import silentium.gameserver.model.actor.instance.L2GrandBossInstance; import silentium.gameserver.model.actor.instance.L2MonsterInstance; import silentium.gameserver.model.actor.instance.L2PcInstance; import silentium.gameserver.model.zone.type.L2BossZone; import silentium.gameserver.network.serverpackets.Earthquake; import silentium.gameserver.network.serverpackets.PlaySound; import silentium.gameserver.network.serverpackets.SocialAction; import silentium.gameserver.scripting.ScriptFile; import silentium.gameserver.tables.SkillTable; import silentium.gameserver.templates.StatsSet; import silentium.gameserver.utils.Util; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Following animations are handled in that time tempo : * <ul> * <li>wake(2), 0-13 secs</li> * <li>neck(3), 14-24 secs.</li> * <li>roar(1), 25-37 secs.</li> * </ul> * Waker's sacrifice is handled between neck and roar animation. */ public class Baium extends DefaultMonsterAI implements ScriptFile { private L2Character _target; private L2Skill _skill; private L2PcInstance _waker; private static final int STONE_BAIUM = 29025; private static final int LIVE_BAIUM = 29020; private static final int ARCHANGEL = 29021; // Baium status tracking public static final byte ASLEEP = 0; // baium is in the stone version, waiting to be woken up. Entry is unlocked. public static final byte AWAKE = 1; // baium is awake and fighting. Entry is locked. public static final byte DEAD = 2; // baium has been killed and has not yet spawned. Entry is locked. // Archangels spawns private static final int[][] ANGEL_LOCATION = { { 114239, 17168, 10080, 63544 }, { 115780, 15564, 10080, 13620 }, { 114880, 16236, 10080, 5400 }, { 115168, 17200, 10080, 0 }, { 115792, 16608, 10080, 0 }, }; private long _LastAttackVsBaiumTime = 0; private final List<L2Npc> _Minions = new ArrayList<>(5); private L2BossZone _Zone; public static void onLoad() { new Baium(-1, "Baium", "Baium", "ai"); } public Baium(final int scriptId, final String name, final String dname, final String path) { super(scriptId, name, dname, path); final int[] mob = { LIVE_BAIUM }; registerMobs(mob); // Quest NPC starter initialization addStartNpc(STONE_BAIUM); addTalkId(STONE_BAIUM); _Zone = GrandBossManager.getInstance().getZone(113100, 14500, 10077); final StatsSet info = GrandBossManager.getInstance().getStatsSet(LIVE_BAIUM); final int status = GrandBossManager.getInstance().getBossStatus(LIVE_BAIUM); if (status == DEAD) { // load the unlock date and time for baium from DB final long temp = info.getLong("respawn_time") - System.currentTimeMillis(); if (temp > 0) { // The time has not yet expired. Mark Baium as currently locked (dead). startQuestTimer("baium_unlock", temp, null, null); } else { // The time has already expired while the server was offline. Delete the saved time and // immediately spawn the stone-baium. Also the state need not be changed from ASLEEP addSpawn(STONE_BAIUM, 116033, 17447, 10104, 40188, false, 0); GrandBossManager.getInstance().setBossStatus(LIVE_BAIUM, ASLEEP); } } else if (status == AWAKE) { final int loc_x = info.getInteger("loc_x"); final int loc_y = info.getInteger("loc_y"); final int loc_z = info.getInteger("loc_z"); final int heading = info.getInteger("heading"); final int hp = info.getInteger("currentHP"); final int mp = info.getInteger("currentMP"); final L2Npc baium = addSpawn(LIVE_BAIUM, loc_x, loc_y, loc_z, heading, false, 0); GrandBossManager.getInstance().addBoss((L2GrandBossInstance) baium); baium.setCurrentHpMp(hp, mp); baium.setRunning(); // start monitoring baium's inactivity _LastAttackVsBaiumTime = System.currentTimeMillis(); startQuestTimer("baium_despawn", 60000, baium, null, true); startQuestTimer("skill_range", 500, baium, null, true); // Spawns angels for (final int[] element : ANGEL_LOCATION) { final L2Npc angel = addSpawn(ARCHANGEL, element[0], element[1], element[2], element[3], false, 0, true); ((L2Attackable) angel).setIsRaidMinion(true); angel.setRunning(); _Minions.add(angel); } // Angels AI startQuestTimer("angels_aggro_reconsider", 1000, null, null, true); } else addSpawn(STONE_BAIUM, 116033, 17447, 10104, 40188, false, 0); } @Override public String onAdvEvent(final String event, final L2Npc npc, final L2PcInstance player) { if ("baium_unlock".equalsIgnoreCase(event)) { GrandBossManager.getInstance().setBossStatus(LIVE_BAIUM, ASLEEP); addSpawn(STONE_BAIUM, 116033, 17447, 10104, 40188, false, 0); } else if ("skill_range".equalsIgnoreCase(event) && npc != null) { callSkillAI(npc); } else if ("clean_player".equalsIgnoreCase(event)) { _target = getRandomTarget(npc); } else if ("baium_neck".equalsIgnoreCase(event) && npc != null) { if (npc.getNpcId() == LIVE_BAIUM) npc.broadcastPacket(new SocialAction(npc, 3)); } else if ("sacrifice_waker".equalsIgnoreCase(event) && npc != null) { if (npc.getNpcId() == LIVE_BAIUM) { if (_waker != null) { // If player is far of Baium, teleport him back. if (!Util.checkIfInShortRadius(300, _waker, npc, true)) _waker.teleToLocation(115929, 17349, 10077); // 60% to die. if (Rnd.get(100) < 60) _waker.doDie(npc); } } } else if ("baium_roar".equalsIgnoreCase(event) && npc != null) { if (npc.getNpcId() == LIVE_BAIUM) { // Roar animation npc.broadcastPacket(new SocialAction(npc, 1)); // Spawn angels for (final int[] element : ANGEL_LOCATION) { final L2Npc angel = addSpawn(ARCHANGEL, element[0], element[1], element[2], element[3], false, 0, true); ((L2Attackable) angel).setIsRaidMinion(true); angel.setRunning(); _Minions.add(angel); } // Angels AI startQuestTimer("angels_aggro_reconsider", 1000, null, null, true); } } // despawn the live baium after 30 minutes of inactivity // also check if the players are cheating, having pulled Baium outside his zone... else if ("baium_despawn".equalsIgnoreCase(event) && npc != null) { if (npc.getNpcId() == LIVE_BAIUM) { // just in case the zone reference has been lost (somehow...), restore the reference if (_Zone == null) _Zone = GrandBossManager.getInstance().getZone(113100, 14500, 10077); if (_LastAttackVsBaiumTime + 1800000 < System.currentTimeMillis()) { // despawn the live-baium npc.deleteMe(); // Unspawn angels for (final L2Npc minion : _Minions) { if (minion != null) { minion.getSpawn().stopRespawn(); minion.deleteMe(); } } _Minions.clear(); addSpawn(STONE_BAIUM, 116033, 17447, 10104, 40188, false, 0); // spawn stone-baium GrandBossManager.getInstance().setBossStatus(LIVE_BAIUM, ASLEEP); // mark that Baium is not awake any more _Zone.oustAllPlayers(); cancelQuestTimer("baium_despawn", npc, null); } else if (_LastAttackVsBaiumTime + 300000 < System.currentTimeMillis() && npc.getCurrentHp() < npc.getMaxHp() * 3 / 4.0) { npc.setIsCastingNow(false); npc.setTarget(npc); final L2Skill skill = SkillTable.getInstance().getInfo(4135, 1); npc.doCast(skill); npc.setIsCastingNow(true); } else if (!_Zone.isInsideZone(npc)) npc.teleToLocation(116033, 17447, 10104); } } else if ("angels_aggro_reconsider".equalsIgnoreCase(event)) { boolean updateTarget = false; // Update or no the target for (final L2Npc minion : _Minions) { final L2Attackable angel = (L2Attackable) minion; if (angel == null) continue; final L2Character victim = angel.getMostHated(); if (Rnd.get(100) == 0) // Chaos time updateTarget = true; else { if (victim != null) // Target is a unarmed player ; clean aggro. { if (victim instanceof L2PcInstance && victim.getActiveWeaponInstance() == null) { angel.stopHating(victim); // Clean the aggro number of previous victim. updateTarget = true; } } else // No target currently. updateTarget = true; } if (updateTarget) { final L2Character newVictim = getRandomTarget(minion); if (newVictim != null && victim != newVictim) { angel.addDamageHate(newVictim, 0, 10000); angel.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, newVictim); } } } } return super.onAdvEvent(event, npc, player); } @Override public String onTalk(final L2Npc npc, final L2PcInstance player) { final int npcId = npc.getNpcId(); String htmltext = ""; if (_Zone == null) { _Zone = GrandBossManager.getInstance().getZone(113100, 14500, 10077); // If the zone is still null, it means the area is disabled / missing in DP. if (_Zone == null) return "<html><body>Angelic Vortex:<br>You may not enter while admin disabled this zone.</body></html>"; } if (npcId == STONE_BAIUM && GrandBossManager.getInstance().getBossStatus(LIVE_BAIUM) == ASLEEP) { if (_Zone.isPlayerAllowed(player)) { // once Baium is awaken, no more people may enter until he dies, the server reboots, or // 30 minutes pass with no attacks made against Baium. GrandBossManager.getInstance().setBossStatus(LIVE_BAIUM, AWAKE); npc.deleteMe(); final L2Npc baium = addSpawn(LIVE_BAIUM, npc, true); GrandBossManager.getInstance().addBoss((L2GrandBossInstance) baium); // Baium is stuck for the following time : 35secs ThreadPoolManager.getInstance().scheduleGeneral(new Runnable() { @Override public void run() { baium.setIsInvul(false); baium.setIsImmobilized(false); // Start monitoring baium's inactivity and activate the AI _LastAttackVsBaiumTime = System.currentTimeMillis(); startQuestTimer("baium_despawn", 60000, baium, null, true); startQuestTimer("skill_range", 500, baium, null, true); } }, 35000L); // First animation baium.setIsInvul(true); baium.setRunning(); baium.broadcastPacket(new SocialAction(baium, 2)); baium.broadcastPacket(new Earthquake(baium.getX(), baium.getY(), baium.getZ(), 40, 10)); _waker = player; // Second animation, waker sacrifice, followed by angels spawn and third animation. startQuestTimer("baium_neck", 13000, baium, null); startQuestTimer("sacrifice_waker", 24000, baium, null); startQuestTimer("baium_roar", 28000, baium, null); baium.setShowSummonAnimation(false); } else htmltext = "Conditions are not right to wake up Baium"; } return htmltext; } @Override public String onSpellFinished(final L2Npc npc, final L2PcInstance player, final L2Skill skill) { if (npc.isInvul()) { npc.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); return null; } else if (npc.getNpcId() == LIVE_BAIUM && !npc.isInvul()) callSkillAI(npc); return super.onSpellFinished(npc, player, skill); } @Override public String onSpawn(final L2Npc npc) { npc.disableCoreAI(true); return super.onSpawn(npc); } @Override public String onAttack(final L2Npc npc, final L2PcInstance attacker, final int damage, final boolean isPet) { if (!_Zone.isInsideZone(attacker)) { attacker.reduceCurrentHp(attacker.getCurrentHp(), attacker, false, false, null); return super.onAttack(npc, attacker, damage, isPet); } if (npc.isInvul()) { npc.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); return super.onAttack(npc, attacker, damage, isPet); } else if (npc.getNpcId() == LIVE_BAIUM && !npc.isInvul()) { if (attacker.getMountType() == 1) { final L2Skill skill = SkillTable.getInstance().getInfo(4258, 1); if (attacker.getFirstEffect(skill) == null) { npc.setTarget(attacker); npc.doCast(skill); } } // update a variable with the last action against baium _LastAttackVsBaiumTime = System.currentTimeMillis(); callSkillAI(npc); } return super.onAttack(npc, attacker, damage, isPet); } @Override public String onKill(final L2Npc npc, final L2PcInstance killer, final boolean isPet) { cancelQuestTimer("baium_despawn", npc, null); npc.broadcastPacket(new PlaySound(1, "BS01_D", 1, npc.getObjectId(), npc.getX(), npc.getY(), npc.getZ())); // spawn the "Teleportation Cubic" for 15 minutes (to allow players to exit the lair) addSpawn(29055, 115203, 16620, 10078, 0, false, 900000); // "lock" baium for 5 days + 1-8 hours final long respawnTime = (long) NPCConfig.SPAWN_INTERVAL_BAIUM + Rnd.get(NPCConfig.RANDOM_SPAWN_TIME_BAIUM); GrandBossManager.getInstance().setBossStatus(LIVE_BAIUM, DEAD); startQuestTimer("baium_unlock", respawnTime, null, null); // also save the respawn time so that the info is maintained past reboots final StatsSet info = GrandBossManager.getInstance().getStatsSet(LIVE_BAIUM); info.set("respawn_time", System.currentTimeMillis() + respawnTime); GrandBossManager.getInstance().setStatsSet(LIVE_BAIUM, info); // Unspawn angels. for (final L2Npc minion : _Minions) { if (minion != null) { minion.getSpawn().stopRespawn(); minion.deleteMe(); } } _Minions.clear(); // Clean Baium AI cancelQuestTimer("skill_range", npc, null); // Clean angels AI cancelQuestTimer("angels_aggro_reconsider", null, null); return super.onKill(npc, killer, isPet); } @Override public String onSkillSee(final L2Npc npc, final L2PcInstance caster, final L2Skill skill, final L2Object[] targets, final boolean isPet) { if (npc.isInvul()) { npc.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); return null; } npc.setTarget(caster); return super.onSkillSee(npc, caster, skill, targets, isPet); } /** * That method allows to select a random target. * * @param npc to check. * @return the random target. */ private L2Character getRandomTarget(final L2Npc npc) { final int npcId = npc.getNpcId(); final FastList<L2Character> result = FastList.newInstance(); final Collection<L2Object> objs = npc.getKnownList().getKnownObjects().values(); for (final L2Object obj : objs) { if (obj != null) { if (obj instanceof L2Playable) { if (obj instanceof L2PcInstance) { if (((L2PcInstance) obj).getAppearance().getInvisible()) continue; if (npcId == ARCHANGEL && ((L2PcInstance) obj).getActiveWeaponInstance() == null) continue; } if (obj.getZ() < npc.getZ() - 100 && obj.getZ() > npc.getZ() + 100 || !GeoData.getInstance().canSeeTarget(obj.getX(), obj.getY(), obj.getZ(), npc.getX(), npc.getY(), npc.getZ())) continue; if (Util.checkIfInRange(2000, npc, obj, true) && !((L2Character) obj).isDead()) result.add((L2Character) obj); } // Case of Archangels, they can hit Baium. if (npcId == ARCHANGEL && obj instanceof L2GrandBossInstance) result.add((L2Character) obj); } } // If there's no players available, Baium and Angels are hitting each other. if (result.isEmpty()) { if (npcId == LIVE_BAIUM) // Case of Baium. Angels should never be without target. { for (final L2Npc minion : _Minions) if (minion != null) result.add(minion); } } if (result.isEmpty()) { FastList.recycle(result); return null; } final Object[] characters = result.toArray(); cancelQuestTimer("clean_player", npc, null); startQuestTimer("clean_player", 20000, npc, null); final L2Character target = (L2Character) characters[Rnd.get(characters.length)]; FastList.recycle(result); return target; } /** * That method checks if angels are near. * * @param npc : baium. * @return the number of angels surrounding the target. */ private int getSurroundingAngelsNumber(final L2Npc npc) { int count = 0; final Collection<L2Object> objs = npc.getKnownList().getKnownObjects().values(); for (final L2Object obj : objs) { if (obj instanceof L2MonsterInstance) { if (((L2Npc) obj).getNpcId() == 29021) if (Util.checkIfInRange(600, npc, obj, true)) count++; } } return count; } /** * The personal casting AI for Baium. * * @param npc baium, basically... */ private synchronized void callSkillAI(final L2Npc npc) { if (npc.isInvul() || npc.isCastingNow()) return; if (_target == null || _target.isDead() || !_Zone.isInsideZone(_target)) { _target = getRandomTarget(npc); if (_target != null) _skill = SkillTable.getInstance().getInfo(getRandomSkill(npc), 1); } final L2Character target = _target; L2Skill skill = _skill; if (skill == null) skill = SkillTable.getInstance().getInfo(getRandomSkill(npc), 1); if (target == null || target.isDead() || !_Zone.isInsideZone(target)) { npc.setIsCastingNow(false); return; } // Adapt the skill range, because Baium is fat. if (Util.checkIfInRange(skill.getCastRange() + npc.getCollisionRadius(), npc, target, true)) { npc.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); npc.setTarget(skill.getId() == 4135 ? npc : target); npc.setIsCastingNow(true); _target = null; _skill = null; try { Thread.sleep(1000); npc.stopMove(null); npc.doCast(skill); } catch (Exception e) { e.printStackTrace(); } } else { npc.getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, target, null); npc.setIsCastingNow(false); } } /** * Pick a random skill through that list.<br> * If Baium feels surrounded, he will use AoE skills. Same behavior if he is near 2+ angels.<br> * * @param npc baium * @return a usable skillId */ private int getRandomSkill(final L2Npc npc) { // Baium's selfheal. It happens exceptionaly. if (npc.getCurrentHp() < npc.getMaxHp() / 10) { if (Rnd.get(10000) == 777) // His lucky day. return 4135; } int skill = 4127; // Default attack if nothing is possible. final int chance = Rnd.get(100); // Remember, it's 0 to 99, not 1 to 100. // If Baium feels surrounded or see 2+ angels, he unleashes his wrath upon heads :). if (Util.getPlayersCountInRadius(600, npc, true, false) >= 20 || getSurroundingAngelsNumber(npc) >= 2) { if (chance < 25) skill = 4130; else if (chance >= 25 && chance < 50) skill = 4131; else if (chance >= 50 && chance < 75) skill = 4128; else if (chance >= 75 && chance < 100) skill = 4129; } else { if (npc.getCurrentHp() > npc.getMaxHp() * 3 / 4) // > 75% { if (chance < 10) skill = 4128; else skill = chance >= 10 && chance < 20 ? 4129 : 4127; } else if (npc.getCurrentHp() > npc.getMaxHp() * 2 / 4) // > 50% { if (chance < 10) skill = 4131; else if (chance >= 10 && chance < 20) skill = 4128; else skill = chance >= 20 && chance < 30 ? 4129 : 4127; } else if (npc.getCurrentHp() > npc.getMaxHp() / 4) // > 25% { if (chance < 10) skill = 4130; else if (chance >= 10 && chance < 20) skill = 4131; else if (chance >= 20 && chance < 30) skill = 4128; else skill = chance >= 30 && chance < 40 ? 4129 : 4127; } else // < 25% { if (chance < 10) skill = 4130; else if (chance >= 10 && chance < 20) skill = 4131; else if (chance >= 20 && chance < 30) skill = 4128; else skill = chance >= 30 && chance < 40 ? 4129 : 4127; } } return skill; } }