Энное количество лет назад на множестве серверов была такая фича - по достижении 20, 40 и 76 уровней появляется всплывающее окно с предложением выбрать профессию. Один человек спросил как это сделать (нет, это не Аури) а я даже зачем-то пообещал ему рассказать. Положим, человек лезет в ядрышко если не в первый, то в полуторный раз и нужных навыков он не имеет. Значит, будем опираться на логику.
Все приведенное ниже относится к оверовскому попилу. Для лыжи есть решение из коробки.
Для начала нужно смекнуть, что раз речь идет о срабатывании триггера по достижению уровня, то и копать нужно возле анимации получения нового уровня - пусть сие будет для нас ориентиром. Мы находим это в классе Player.java, в методе levelSet. Просматривая метод, мы видим вызов других методов, которые устанавливают CP, MP и HP на максимум. Сюда-то мы и вклинимся:
Свернуть ↑
private void levelSet(final int levels)
{
if (levels > 0)
{
final int level = getLevel();
checkLevelUpReward();
sendPacket(SystemMsg.YOUR_LEVEL_HAS_INCREASED);
broadcastPacket(new SocialActionPacket(getObjectId(), 2122));
setCurrentHpMp(getMaxHp(), getMaxMp());
setCurrentCp(getMaxCp());
// Метка!
...
}
}
Code: Java
Свернуть ↑Развернуть ↓
Будет хорошим тоном завести конфиг под это все и вынести в соответствующий файл. Сделать это можно по образу и подобию любого булева конфига в принципе, если посмотреть Config.java
Дальше нам нужно описать условия срабатывания. На обычном языке это можно описать так: "ну, если у тебя 20 уровень но ты без первой профы, то должно появляться окно. Если ты 40 и без второй профы, то тоже. И если ты 76 без третьей - аналогично." К старшим хроникам актуально еще несколько требований - 85 уровень и четвертая профессия + условие для новой расы, Артеи. У Артей профессии смещены - третья профессия для них является четвертой, вторая - третьей ну и далее по тексту. Опишем же это:
Свернуть ↑
if(Config.ALTERNATE_CLASS_MASTER) // Использовать конфиги - это норма
{
if(!getClassId().isOfRace(Race.ERTHEIA)) // Не Артея
{
if((level>=20 && getClassId().isOfLevel(ClassLevel.NONE)) ||
(level>=40 && getClassId().isOfLevel(ClassLevel.FIRST)) ||
(level>=76 && getClassId().isOfLevel(ClassLevel.SECOND))||
(level>=85 && getClassId().isOfLevel(ClassLevel.THIRD)))
{
AltClassMaster(); // Это наш метод, который еще предстоит написать
}
}
else // Артея
{
if((level>=40 && getClassId().isOfLevel(ClassLevel.NONE)) ||
(level>=76 && getClassId().isOfLevel(ClassLevel.FIRST)) ||
(level>=85 && getClassId().isOfLevel(ClassLevel.SECOND)))
{
AltClassMaster();
}
}
}
Code: Java
Свернуть ↑Развернуть ↓
Отлично! Мы описали условия и теперь все сказанное про срабатывание на раличных уровнях и профессия стало реальностью. Но что будет срабатывать, если условия будут выполнены? Давайте опять подумаем - должно всплывать окно, где будут кнопки с доступными профессиями. Для кнопок должен быть свой байпасс, по которому при срабатывании должна присваиваться очередная профессия. Опишем пока все это более схематично - мол, вызывается окно и часть текста меняется на нужное нам. Давайте же запилим вызов окна. Для этого мы прокрутим класс Player.java ближе к концу и запилим свой метод сразу после public void sendAlchemySkillList(), например, так:
Свернуть ↑
public void AltClassMaster()
{
final NpcHtmlMessagePacket html = new NpcHtmlMessagePacket(this, null);
html.setFile("custom/altclassmaster.htm"); // путь к нашему окну
html.replace("%name%", getClassId().getName(this));
html.replace("%list%", generateAvailClassList(getClassId()));
SendPacket(html);
}
Code: Java
Свернуть ↑Развернуть ↓
Тут даже ежикам должно быть понятно, что мы задаем путь к html'ке, в которой меняем текст %name% на имя игрока, а %list% будет генерировать список доступных профессий для указанного класса. Все это для того, чтобы можно было сделать свое вырвиглазное оформление этого окошка, главное, чтобы там была фраза %list%. Но как же сгенерировать этот лист? Вот у нас список профессий. Можно просто идти по нему и в случае успеха добавлять кнопку с байпассом. Учтем, что класс Инспектора придется пропустить (ибо берется только как саб-класс), да и не будет лишним игнорировать раскукоженные в линдвиоре четвертые профессии аля Sigel Knight, Tyr Warrior, Othel Rogue и подобные. В описании будем использовать метод сверки принадлежности дочернего класса к основному (т.е. к наследующему) и метод возвращающий название класса по его ID. Все это примет примерно вот такой вид:
Свернуть ↑
public String generateAvailClassList(ClassId classId)
{
StringBuilder classList = new StringBuilder();
for(ClassId cid : ClassId.VALUES) // Идем по списку классов
{
if((cid == ClassId.INSPECTOR) || (cid == ClassId.SIGEL_KNIGHT) || (cid == ClassId.TYR_WARRIOR) || (cid == ClassId.OTHELL_ROGUE) || (cid == ClassId.YR_ARCHER) || (cid == ClassId.FEOH_WIZARD) || (cid == ClassId.ISS_ENCHANTER) || (cid == ClassId.WYNN_SUMMONER) || (cid == ClassId.EOLH_HEALER))
{
continue; // Если условия выполняются, то крутим барабан
}
if(cid.childOf(classId) && cid.getClassLevel().ordinal() == classId.getClassLevel().ordinal() + 1)
{
classList.append("<button ALIGN=LEFT ICON=\"NORMAL\" action=\"bypass -h ");
classList.append(encodeBypasses("legacy_setclass "+cid.getId(), false));
classList.append("alt_setclass "+cid.getId());
classList.append("\">");
classList.append(HtmlUtils.htmlClassName(cid.getId())).append("</button>");
}
}
return classList.toString();
}
Code: Java
Свернуть ↑Развернуть ↓
В итоге, вместо слова %list% у нас будет список профессий, которые мы можем выбрать для нашего класса. В очередной раз время подумать - где, внедренная нами фича может дать сбой исходя из логики работы сервера? Таких моментов несколько. Первый из них - это отсылка html-приветственного сообщения при заходе на сервер. Давайте исправим это в классе EnterWorld.java. Находим булево значение из конфига Config.SHOW_HTML_WELCOME и вписываем туда следующую логику: если условия для взятия класса выполняются, то нужно показать наше окно; при других условиях пусть показывается окно приветствия. Будет это выглядеть так:
Свернуть ↑
if(Config.ALTERNATE_CLASS_MASTER || Config.SHOW_HTML_WELCOME)
{
final int level = activeChar.getLevel();
final ClassId classID = activeChar.getClassId();
if(!activeChar.getClassId().isOfRace(Race.ERTHEIA))
{
if((level>=20 && classID.isOfLevel(ClassLevel.NONE)) ||
(level>=40 && classID.isOfLevel(ClassLevel.FIRST)) ||
(level>=76 && classID.isOfLevel(ClassLevel.SECOND))||
(level>=85 && classID.isOfLevel(ClassLevel.THIRD)))
{
activeChar.AltClassMaster();
}
}
else
{
if ((level>=40 && classID.isOfLevel(ClassLevel.NONE)) ||
(level>=76 && classID.isOfLevel(ClassLevel.FIRST)) ||
(level>=85 && classID.isOfLevel(ClassLevel.SECOND)))
{
activeChar.AltClassMaster();
}
}
else
{
activeChar.sendPacket(new NpcHtmlMessagePacket(activeChar, null));
}
}
Code: Java
Свернуть ↑Развернуть ↓
Таким образом, наше злосчастное окно настигнет игрока даже если тот попробует спастить бегством - при следующем заходе на сервер. Еще один момент заключается в том, что если игрок закроет окно, то ему придется либо делать релогин, либо ждать очередного повышения уровня, чтобы сработали проверки условий и вызвался наш свежезапиленный метод. По совету одного товарища можно поступить проще - создать голосовую команду, например, .class и при ее вызове выкидывать окошко с выбором профессии - ну, или не выкидывать и слать на фиг, если условия не соблюдены. Есть множество мануалов по запилке голосовых команд, их можно сделать и без мануала вообще, по образу и подобию уже существующих. Условия срабатывания можно скопи-пастить из EnterWorld.java, убрав оттуда показ приветственного html-окна.
И вот, мы все это дело собираем, запускам сборку и качаемся до срабатывания окна - и вот окно появляется. Но, при нажатии на него почему-то ничего не происходит. Еще бы - байпасс, который мы вписывали при формировании списка доступных профессий для класса не существует! Время это исправить. Для этого мы проследуем в класс RequestBypassToServer и добавим наш байпасс "alt_class" к остальной гоп-компании. Предположим, что вы уже начали осмысливать происходящее и с первого раза нормально скопипастите нижеописанное:
Свернуть ↑
if (bypass.startsWith("alt_setclass") && Config.ALTERNATE_CLASS_MASTER)
{
try
{
final String val = fullString.substring(13);
final int id = Integer.parseInt(val.trim());
if (id > ClassId.VALUES.length - 1)
{
activeChar.sendMessage("Вы не можете выбрать ID класса выше 136");
return;
}
if((activeChar.getClassLevel()==ClassLevel.NONE) || (activeChar.getClassLevel()==ClassLevel.FIRST)) // Кейс при первой и второй профе
{
activeChar.sendPacket(SystemMsg.CONGRATULATIONS__YOUVE_COMPLETED_A_CLASS_TRANSFER);
activeChar.setClassId(id, true);
return;
}
if(activeChar.getClassLevel()==ClassLevel.SECOND) // Кейс при третьей профе
{
activeChar.sendPacket(SystemMsg.CONGRATULATIONS__YOUVE_COMPLETED_YOUR_THIRDCLASS_TRANSFER_QUEST);
activeChar.setClassId(id, true);
return ;
}
else // Если класс перерождения
{
if (activeChar.isTransformed() || activeChar.isMounted())
{
activeChar.sendPacket(SystemMsg.YOU_CANNOT_AWAKEN_WHILE_YOURE_TRANSFORMED_OR_RIDING);
return;
}
activeChar.setClassId(id, true); // Смена класса
activeChar.sendMessage("Вы достигли самой вершины! Получен класс перерождения!");
activeChar.broadcastPacket(new SocialActionPacket(activeChar.getObjectId(), 20)); // Анимация прыжка
}
activeChar.rewardSkills(true, false, true);
activeChar.sendSkillList();
activeChar.notifyNewSkills();
activeChar.broadcastUserInfo(true);
return;
}
catch (StringIndexOutOfBoundsException e)
{
activeChar.sendMessage("К сожалению, вы не профпригодны");
return;
}
}
Code: Java
Свернуть ↑Развернуть ↓
Теперь наш байпасс корректно отработает и сменить профессию. Но нужно ли было это все делать, когда можно включить котов-NPC или воспользоваться готовыми решениями для Community Board (тысячи их)?