Hey community, I’m sharing another customization for my server l2friends.co ultra status. I took the mini boss event that Bazookarpm posted and reworked a lot of it. I added an auto-teleport with confirmation—big thanks to Mobius for showing me which method sends those confirmations to users. Since I’m not a Java developer, I struggled with these changes for three days 😅 but now I’ve reached a very good result. Over time, I’ll make more adjustments as needed. Feel free to modify anything and share it with the community.
1. Add a New Enumerator:
In the file \gameserver\enums\PlayerAction.java, add a new enumerator called USER_CONFIRM_BOSS_TELEPORT. The updated section should look like this:
public enum PlayerAction
{
...
OFFLINE_PLAY,
USER_CONFIRM_BOSS_TELEPORT;
}
2. Register the New Action:
In the file gameserver\network\clientpackets\DlgAnswer.java, register the new enumerator in the alert manager. Look for the following section:
else if (player.removeAction(PlayerAction.USER_ENGAGE))
{
if (Config.ALLOW_WEDDING)
{
player.engageAnswer(_answer);
}
}
Add a new validation for user interaction:
else if (player.removeAction(PlayerAction.USER_CONFIRM_BOSS_TELEPORT))
{
player.bossTeleportAnswer(_answer);
}
3. Add Methods in Player.java:
In the file /model/actor/Player.java, add the following code. Place it below:
private Map<Stat, Double> _servitorShare;
The updated section should look like this:
private Map<Stat, Double> _servitorShare;
private int _bossTeleportAnswer = -1; // -1 (no response), 0 (declined), 1 (accepted).
public void setBossTeleportAnswer(int answer)
{
// Stores the response
_bossTeleportAnswer = answer;
}
public void resetBossTeleportAnswer()
{
_bossTeleportAnswer = -1;
}
public int getBossTeleportAnswer()
{
// Returns the response
return _bossTeleportAnswer;
}
public void bossTeleportAnswer(int answer)
{
LOGGER.info("Setting boss teleport answer to: " + answer + " for player: " + getName());
setBossTeleportAnswer(answer);
}
4. Directory and File Setup:
Create a new folder and files in /data/scripts/custom/events/. The folder name should be boss, and inside the boss folder, create the following files:
config.xml
<?xml version="1.0" encoding="UTF-8"?>
<event name="boss" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../xsd/event.xsd">
<!--
The pattern attribute specifies the schedule time of the event.
- Minutes (0-59)
- Hours (0-23)
- Day of the month (1-31)
- Month (1-12)
- Day of the week (0-7)
You can use * as a wildcard value.
You can use multiple schedule elements.
-->
<!-- Run at specific times throughout the day. -->
<schedule pattern="03 09 * * *" />
<schedule pattern="36 10 * * *" />
<schedule pattern="28 11 * * *" />
<schedule pattern="59 12 * * *" />
<!-- Add more schedules as needed -->
</event>
boss.java
For now, include your logic and code here as needed. Start with:
/*
* This file is part of the L2J Mobius project.
*
* 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 custom.events.boss;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.l2jmobius.commons.threads.ThreadPool;
import org.l2jmobius.commons.time.SchedulingPattern;
import org.l2jmobius.commons.util.IXmlReader;
import org.l2jmobius.commons.util.TimeUtil;
import org.l2jmobius.gameserver.enums.ChatType;
import org.l2jmobius.gameserver.enums.PlayerAction;
import org.l2jmobius.gameserver.model.StatSet;
import org.l2jmobius.gameserver.model.World; // Importar World
import org.l2jmobius.gameserver.model.actor.Npc;
import org.l2jmobius.gameserver.model.actor.Player;
import org.l2jmobius.gameserver.model.actor.instance.EventMonster;
import org.l2jmobius.gameserver.model.quest.Event;
import org.l2jmobius.gameserver.network.serverpackets.ConfirmDlg;
import org.l2jmobius.gameserver.network.serverpackets.CreatureSay;
import org.l2jmobius.gameserver.util.Broadcast;
public class boss extends Event
{
// NPC
private static final List<Integer> BOSS_IDS = List.of(900103, 900104, 900105, 900106, 900107, 900108, 900109); // Substitua pelos IDs dos bosses que deseja usar.
// Amount of BOSS to spawn when the event starts
private static final int BOSS_AMOUNT = 1;
// Event duration in minutes
private static final int EVENT_DURATION_MINUTES = 10;
// @formatter:off
private static final int[][] DROPLIST_CONSUMABLES =
{
{ 6656, 80, 5, 10 }, // Normal Boss Jewelery
{ 6657, 80, 5, 10 }, // Normal Boss Jewelery
{ 6658, 80, 5, 10 }, // Normal Boss Jewelery
{ 6659, 80, 5, 10 }, // Normal Boss Jewelery
{ 6660, 80, 5, 10 }, // Normal Boss Jewelery
{ 6661, 80, 5, 10 }, // Normal Boss Jewelery
{ 6662, 80, 5, 10 }, // Normal Boss Jewelery
};
private static final int[][] DROPLIST_CRYSTALS =
{
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 }, // Adena
{ 57, 100, 200000000, 600000000 } // Adena
};
// @formatter:on
// Non-final variables
private static boolean EVENT_ACTIVE = false;
private ScheduledFuture<?> _eventTask = null;
private final Set<Npc> _boss = ConcurrentHashMap.newKeySet(BOSS_AMOUNT);
private boss()
{
BOSS_IDS.forEach(this::addSpawnId);
BOSS_IDS.forEach(this::addKillId);
loadConfig();
}
private void loadConfig()
{
new IXmlReader()
{
@Override
public void load()
{
parseDatapackFile("data/scripts/custom/events/boss/config.xml");
}
@Override
public void parseDocument(Document doc, File f)
{
final AtomicInteger count = new AtomicInteger(0);
forEach(doc, "event", eventNode ->
{
final StatSet att = new StatSet(parseAttributes(eventNode));
final String name = att.getString("name");
for (Node node = doc.getDocumentElement().getFirstChild(); node != null; node = node.getNextSibling())
{
switch (node.getNodeName())
{
case "schedule":
{
final StatSet attributes = new StatSet(parseAttributes(node));
final String pattern = attributes.getString("pattern");
final SchedulingPattern schedulingPattern = new SchedulingPattern(pattern);
final StatSet params = new StatSet();
params.set("Name", name);
params.set("SchedulingPattern", pattern);
final long delay = schedulingPattern.getDelayToNextFromNow();
getTimers().addTimer("Schedule" + count.incrementAndGet(), params, delay + 5000, null, null); // Added 5 seconds to prevent overlapping.
LOGGER.info("Event " + name + " scheduled at " + TimeUtil.getDateTimeString(System.currentTimeMillis() + delay));
break;
}
}
}
});
}
}.load();
}
@Override
public void onTimerEvent(String event, StatSet params, Npc npc, Player player)
{
if (event.startsWith("Schedule"))
{
eventStart(null);
final SchedulingPattern schedulingPattern = new SchedulingPattern(params.getString("SchedulingPattern"));
final long delay = schedulingPattern.getDelayToNextFromNow();
getTimers().addTimer(event, params, delay + 5000, null, null); // Added 5 seconds to prevent overlapping.
LOGGER.info("Event " + params.getString("Name") + " scheduled at " + TimeUtil.getDateTimeString(System.currentTimeMillis() + delay));
}
}
@Override
public boolean eventBypass(Player player, String bypass)
{
return false;
}
@Override
public boolean eventStart(Player eventMaker)
{
if (EVENT_ACTIVE)
{
return false;
}
EVENT_ACTIVE = true;
final EventLocation randomLoc = getRandomEntry(EventLocation.values());
final long despawnDelay = EVENT_DURATION_MINUTES * 60000;
// Corrigindo o uso de randomLoc para coordenadas X, Y, Z
for (int i = 0; i < BOSS_AMOUNT; i++)
{
int randomBossId = BOSS_IDS.get(getRandom(0, BOSS_IDS.size() - 1)); // Escolhe um boss aleatório.
_boss.add(addSpawn(randomBossId, randomLoc.getRandomX(), randomLoc.getRandomY(), randomLoc.getZ(), 0, true, despawnDelay));
}
List<String> messages = List.of("*I will end everyone around me*", "A mighty boss has spawned in " + randomLoc.getName(), "Help us exterminate them!", "You have " + EVENT_DURATION_MINUTES + " minutes!");
// Send each message as a separate announcement.
for (String message : messages)
{
Broadcast.toAllOnlinePlayers(new CreatureSay(null, ChatType.ANNOUNCEMENT, "Event Manager", message));
}
_eventTask = ThreadPool.schedule(() ->
{
Broadcast.toAllOnlinePlayers("Time is up!");
eventStop();
}, despawnDelay);
return true;
}
@Override
public boolean eventStop()
{
if (!EVENT_ACTIVE)
{
return false;
}
EVENT_ACTIVE = false;
if (_eventTask != null)
{
_eventTask.cancel(true);
_eventTask = null;
}
for (Npc npc : _boss)
{
npc.deleteMe();
}
_boss.clear();
Broadcast.toAllOnlinePlayers("*They have managed to defeat the invader*");
Broadcast.toAllOnlinePlayers("Kill the Boss Event finished!");
return true;
}
private void teleportPlayerToBoss(Player player, Npc boss)
{
final int bossX = boss.getX();
final int bossY = boss.getY();
final int bossZ = boss.getZ();
final int teleportRadius = 800;
if ((player != null) && player.isOnline() && (player.getBossTeleportAnswer() == 1))
{
final int offsetX = getRandom(-teleportRadius, teleportRadius);
final int offsetY = getRandom(-teleportRadius, teleportRadius);
player.teleToLocation(bossX + offsetX, bossY + offsetY, bossZ);
player.sendMessage("You have been teleported near the boss!");
}
}
private boolean sendTeleportConfirmation(Player player, Npc boss)
{
ConfirmDlg dlg = new ConfirmDlg("Do you want to teleport to fight the boss?");
player.addAction(PlayerAction.USER_CONFIRM_BOSS_TELEPORT);
dlg.addTime(10000);
player.sendPacket(dlg);
return true;
}
@Override
public String onKill(Npc npc, Player killer, boolean isSummon)
{
if (EVENT_ACTIVE)
{
_boss.remove(npc);
dropItem(npc, killer, DROPLIST_CONSUMABLES); // Dropa todos os consumíveis
dropItem(npc, killer, DROPLIST_CRYSTALS); // Dropa todos os cristais
if (_boss.isEmpty())
{
Broadcast.toAllOnlinePlayers("Mighty" + npc.getName() + " has been killed!");
eventStop();
}
}
return super.onKill(npc, killer, isSummon);
}
@Override
public String onSpawn(Npc npc)
{
if (BOSS_IDS.contains(npc.getId())) // Se o NPC estiver na lista de bosses
{
Broadcast.toAllOnlinePlayers("A mighty boss has spawned: " + npc.getName() + "! Prepare for battle!");
List<Player> players = new ArrayList<>(World.getInstance().getPlayers());
// Paralelizar o envio de confirmações
ExecutorService executor = Executors.newFixedThreadPool(10);
for (Player player : players)
{
executor.submit(() -> sendTeleportConfirmation(player, npc));
}
executor.shutdown();
// Consolidar teletransportes após 20 segundos
ThreadPool.schedule(() ->
{
for (Player player : players)
{
if (player.getBossTeleportAnswer() == 1)
{
teleportPlayerToBoss(player, npc);
}
}
}, 20000);
}
if (npc instanceof EventMonster)
{
((EventMonster) npc).eventSetDropOnGround(true);
((EventMonster) npc).eventSetBlockOffensiveSkills(true);
}
return super.onSpawn(npc);
}
private enum EventLocation
{
ADEN("Aden", 146558, 148341, 26622, 28560, -2200),
DION("Dion", 18564, 19200, 144377, 145782, -3081),
GLUDIN("Gludin", -84040, -81420, 150257, 151175, -3125),
HV("Hunters Village", 116094, 117141, 75776, 77072, -2700),
GIRAN("Giran", 90730, 90730, 131034, 131034, -3629),
OREN("Oren", 82048, 82940, 53240, 54126, -1490);
private final String _name;
private final int _minX;
private final int _maxX;
private final int _minY;
private final int _maxY;
private final int _z;
EventLocation(String name, int minX, int maxX, int minY, int maxY, int z)
{
_name = name;
_minX = minX;
_maxX = maxX;
_minY = minY;
_maxY = maxY;
_z = z;
}
public String getName()
{
return _name;
}
public int getRandomX()
{
return getRandom(_minX, _maxX);
}
public int getRandomY()
{
return getRandom(_minY, _maxY);
}
public int getZ()
{
return _z;
}
}
private void dropItem(Npc mob, Player player, int[][] droplist)
{
for (int[] drop : droplist) // Percorre todos os itens da lista
{
int chance = getRandom(100); // Gera um número aleatório entre 0 e 100
if (chance < drop[1]) // Verifica se o item será dropado com base na chance
{
int itemId = drop[0]; // ID do item
int minAmount = drop[2]; // Quantidade mínima
int maxAmount = drop[3]; // Quantidade máxima
int amount = getRandom(minAmount, maxAmount); // Gera a quantidade entre o mínimo e o máximo
mob.dropItem(player, itemId, amount); // Realiza o drop
}
}
}
public static void main(String[] args)
{
new boss();
}
}