L2JMobius

Interlude Repack Custom Mini Boss Event

northon · 4 · 2441

Offline northon

  • Vassal
  • *
    • Posts: 7
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:
Code: [Select]
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:
Code: [Select]
else if (player.removeAction(PlayerAction.USER_ENGAGE))
{
    if (Config.ALLOW_WEDDING)
    {
        player.engageAnswer(_answer);
    }
}

Add a new validation for user interaction:
Code: [Select]
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:
Code: [Select]
private Map<Stat, Double> _servitorShare;
The updated section should look like this:
Code: [Select]
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
  • boss.java
config.xml
Code: [Select]
<?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:
Code: [Select]
/*
 * 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();
}
}


Online Bazookarpm

  • Knight
  • ***
    • Posts: 95
  • Lineage II - lover - newbie
Hello, it is a pleasure to see that small contributions, no matter how simple one makes, can be shared and improved.

I'm going to try this improvement since it would be a version 2.0 hahaha

a round of applause, friend.
Atte BazooKa.RPM


Online GuruGel

  • Heir
  • **
    • Posts: 47
Problem with confirmation of teleport to the boss. If you participate in the event once, then the subsequent appearance of the invitation to click is not necessary ... since you are still teleported to the boss, if you click cancel, then you will not be moved. Well, if you do not click anything, then you are teleported to the boss in any case.


Offline northon

  • Vassal
  • *
    • Posts: 7
Problem with confirmation of teleport to the boss. If you participate in the event once, then the subsequent appearance of the invitation to click is not necessary ... since you are still teleported to the boss, if you click cancel, then you will not be moved. Well, if you do not click anything, then you are teleported to the boss in any case.

to resolve search "player.sendMessage("You have been teleported near the boss!");" in the code and add player.resetBossTeleportAnswer(); after.