Система временных эффектов (Замена Potion)

Система временных эффектов (Замена Potion)

Нет прав для скачивания
Версия(и) Minecraft
1.7.10, 1.12.2
Предисловие

Здравствуйте. В данном туториале я опишу процесс создания собственной системы временных эффектов (баффов), которая будет работать со всеми существами(наследниками EntityLivingBase). Работают они так же, как и ванильные эффекты зелий, однако имеют ряд улучшений и оптимизаций. В первую очередь статья будет полезна тем, кому требуется возможность создавать множество эффектов с полным контролем над ними. Туториал написан для версии 1.7.10 и адаптирован для 1.12.2. Исходники доступны на github.

Данный туториал не содержит информации по настройке
пакетной системы, так как это не является его темой.

1.7.10

Основа

Начнём! Первым делом создадим объект-основу для регистрации наших временных эффектов и описания общих свойств. Расположим тут всё необходимое для применения эффектов. Cоздаём поля для описания общих свойств: идентификатор, название, индекс координат иконки и т.п., создаём сеттеры и геттеры для них. Объявляем массив объектов, в который будем добавлять эффекты. Наследники класса должны переопределять методы onActive() и isReady(), определяющие поведение эффекта, эти методы вызываются каждый тик. Здесь же создаём методы, которые будут единожды вызываться при добавлении и удалении временного эффекта, их будем использовать для активности, которую требуется вызывать один раз.

Этого достаточно для работы с любой сущностью в рамках ванильной игры. Этот объект будет тесно связан с классом, реализующим EEP и общим для всех EntityLivingBase. Такой класс должен содержать только информацию об эффектах. Для остальных EEP создавайте отдельные классы. В Buff для работыс эффектами в методы передаётся EntityLivingBase и если, например, вы создали ещё EEP для игрока и что то вроде "магии", то для создания эффектов, работающих с вашей "магией" просто получите ваши EEP прямо в методах обработки.

Java:
public class Buff {

    public final int id;

    private String name;

    //Число, определяющее координаты иконки баффа в файле с текстурами.
    private int iconIndex;

    //keepOnDeath определяет, сохранять ли бафф при смерти.
    //isPersistent определяет бессрочность эффекта, эффект не исчезнет ни при смерти, ни со временем.
    private boolean keepOnDeath, isPersistent;

    //Создаём массив объектов.
    public static List<Buff> buffs = new ArrayList<Buff>();

    public Buff() {

        //Присваеваем идентификатор.
        this.id = buffs.size();

        //Добавляем созданный объект в массив ArrayList.
        this.buffs.add(this);
    }

    public static Buff of(int buffId) {

        return buffs.get(buffId);
    }

    //Метод, который применяет эффект баффа если isReady() возвращает true.
    protected void onActive(EntityLivingBase entityLivingBase, World world, ActiveBuff buff) {

        int
        tier = buff.getTier(),
        duration = buff.getDuration();
    }

    //Служебный метод, определяющий возможность применить эффект в onActive().
    protected boolean isReady(ActiveBuff buff) {

        int
        tier = buff.getTier(),
        duration = buff.getDuration();

        return false;
    }

    //Добавляем эффект баффа на сервере.
    public void applyBuffEffect(EntityLivingBase entityLivingBase, World world, ActiveBuff buff) {

        if (!world.isRemote) {

            int
            id = buff.getId(),
            tier = buff.getTier();
        }
    }


    //Удаляем эффект баффа на сервере.
    public void removeBuffEffect(EntityLivingBase entityLivingBase, World world, ActiveBuff buff) {

        if (!world.isRemote) {

            int
            id = buff.getId(),
            tier = buff.getTier();
        }
    }

    //Сеттеры для описания свойств и геттеры.

    protected Buff setName(String name) {

        this.name = name;

        return this;
    }

    public String getName() {

        return this.name;
    }

    protected Buff setIconIndex(int x, int y) {

        this.iconIndex = x + y * 8;

        return this;
    }

    public int getIconIndex() {

        return this.iconIndex;
    }

    protected Buff keepOnDeath() {

        this.keepOnDeath = true;

        return this;
    }

    public boolean shouldKeepOnDeath() {

        return this.keepOnDeath;
    }

    protected Buff setPersistent() {

        this.isPersistent = true;

        return this;
    }

    public boolean isPersistent() {

        return this.isPersistent;
    }

    public String getDurationForDisplay(ActiveBuff buff) {

        //Возвращает оставшееся время до истечения действия баффа в формате "мм:сс".
        //Если флаг isPersistent = true, то вернёт строку "-:-", так как бафф постоянен.

        int
        i = buff.getDuration(),
        j = i / 20,
        k = j / 60;

        j %= 60;

        return Buff.of(buff.getId()).isPersistent ? "-:-" : j < 10 ? String.valueOf(k) + ":0" + String.valueOf(j) : String.valueOf(k) + ":" + String.valueOf(j);
    }
}

Отдельно обращаю внимание на поле iconIndex, оно хранит координаты иконки эффекта в виде одной переменной. Кодируются координаты так:
Java:
iconIndex = x + y * 8


Считываются так:
Java:
x = iconIndex % 8 * 16, y = iconIndex / 8 * 16//где 16 - размер стороны иконки в пикселях.


Активный эффект

Теперь требуется создать объект, описывающий индивидуальные свойства каждого эффекта.
Он будет содержать идентификатор, уровень эффекта и время действия. От уровня будет зависеть активность эффекта, он позволит одному эффекту назначить разную активность (например, эффект регенерации в зависимости от уровня может восстанавливать разное кол-во здоровья или восстанавливать здоровье через разные промежутки времени и т.д.). Время действия будем декрементить каждый тик и перезаписывать. Так же создаём методы записи и чтения объекта из NBTTagCompound.

Java:
public class ActiveBuff {

    private int id, duration, tier;

    public ActiveBuff(int buffId, int buffTier, int buffDuration) {

        this.id = buffId;
        this.tier = buffTier;
        this.duration = buffDuration;
    }

    public ActiveBuff(int buffId, int buffDuration) {

        this.id = buffId;
        this.tier = 0;
        this.duration = buffDuration;
    }

    //Для удобства наложения постоянных баффов.
    public ActiveBuff(int buffId) {

        this.id = buffId;
        this.tier = 0;
        this.duration = 0;
    }


    //Метод, обновляющий бафф путём декремента продолжительности действия (duration), возвращает true если время действия не истекло.
    public boolean updateBuff(EntityLivingBase entityLivingBase, World world) {

        if (Buff.of(this.id).isPersistent() || this.duration > 0) {

            //Проверка допустимости применения эффекта (для эффектов с периодическим действием (прим. регенерация, отравление и т.д.).
            if (Buff.of(this.id).isReady(this)) {
 
                //Применение эффекта.
                Buff.of(this.id).onActive(entityLivingBase, world, this);
            }

            //Уменьшаем оставшееся время действия.
            if (!Buff.of(this.id).isPersistent()) {
            
                this.duration--;
            }
        }

        //true - время действия ещё не истекло, false - истекло, удаляем бафф.
        return Buff.of(this.id).isPersistent() || this.duration > 0;
    }

    //Комбинирование бафов с одинаковыми уровнями.
    public void combineBuffs(ActiveBuff buff) {

        //Обновляем время действия только если новый бафф будет активен дольше.
        if (buff.duration > this.duration) {
 
            this.duration = buff.duration;
        }
    }

    //Геттеры свойств.

    public int getId() {

        return this.id;
    }

    public int getDuration() {

        return this.duration;
    }

    public int getTier() {

        return this.tier;
    }

    public String getBuffName() {

        return Buff.of(this.id).getName();
    }

    //Упаковка баффа в NBTTagCompound.
    public NBTTagCompound saveBuffToNBT(ActiveBuff buff) {

        NBTTagCompound tagCompound = new NBTTagCompound();

        tagCompound.setByte("id", (byte) buff.getId());
        tagCompound.setByte("tier", (byte) buff.getTier());
        tagCompound.setInteger("duration", buff.getDuration());

        return tagCompound;
    }

    //Распаковка баффа из NBTTagCompound.
    public static ActiveBuff readBuffFromNBT(NBTTagCompound tagCompound) {

        return new ActiveBuff(tagCompound.getByte("id"), tagCompound.getByte("tier"), tagCompound.getInteger("duration"));
    }
}

Хранение и использование

Все служебные методы разместим в отдельно созданном классе EEP для EntityLivingBase. Это позволит накладывать наши баффы на любые сущности, унаследованные от EntityLivingBase (игрок, животные, монстры). Данный класс будет использоваться для хранения информации об активных эффектах и содержать все служебные методы для работы с ними.

Создаём класс с интерфейсом IEEP:

Java:
public class BuffsLivingBase implements IExtendedEntityProperties {

    public final static String EEP_NAME = "BuffsLivingEEP";

    private EntityLivingBase livingBase;//Создаём поле EntityLivingBase.

    private World world;//Создаём поле World для удобства.

    public BuffsLivingBase() {}

    //Для регистрации.
    public static final void register(EntityLivingBase entity) {

        entity.registerExtendedProperties(BuffsLivingBase.EEP_NAME, new BuffsLivingBase());
    }

    //Для доступа к EEP.
    public static final BuffsLivingBase get(EntityLivingBase entityLiving) {

        return (BuffsLivingBase) entityLiving.getExtendedProperties(EEP_NAME);
    }

    @Override
    public void saveNBTData(NBTTagCompound mainCompound) {

        //Во избежание конфликтов с другими модами создаём собственный NBTTagCompound.
        NBTTagCompound buffsCompound = new NBTTagCompound();

        //Все наши данные храним в нашем компаунде.

        //Сохраняем его в общий NBTTagCompound, используя наш уникальный ключ - имя EEP.
        mainCompound.setTag(EEP_NAME, buffsCompound);
    }

    @Override
    public void loadNBTData(NBTTagCompound mainCompound) {

        //Загружаем наш NBTTagCompound.
        NBTTagCompound buffsCompound = (NBTTagCompound) mainCompound.getTag(EEP_NAME);
    }

    //Инициализируем наши поля
    @Override
    public void init(Entity entity, World world) {

        this.livingBase = (EntityLivingBase) entity;

        this.world = world;
    }

    //Создаем для них геттеры.
    public EntityLivingBase getEntityLiving() {

        return this.livingBase;
    }

    public World getWorld() {

        return this.world;
    }
}

Регистрируем его в EntityJoinWorldEvent для всех EntityLivingBase. Конечно вы можете зарегистрировать его только для конкретных сущностей, например только для игрока.
Java:
    @SubscribeEvent
    public void onEntityConstructing(EntityConstructing event) {

        //Регистрируем EEP для всех entity, которые наследуются от EntityLivingBase.
        if (event.entity instanceof EntityLivingBase) {

            if (BuffsLivingBase.get((EntityLivingBase) event.entity) == null) {

                BuffsLivingBase.register((EntityLivingBase) event.entity);
            }
        }
    }


Создаём карту (HashMap) активных эффектов. В неё будем помещать эффекты (ActiveBuff) в качестве значений и идентификаторы эффектов в качестве ключей. HashMap выбрана потому, что не может содержать два одинаковых ключа-идентификатора. что не позволит нам иметь два одинаковых активных эффекта. Все служебные методы будут работать с ней. Карту необходимо синхронизировать с клиентом, когда игрок заходит в мир (EntityJoinWorldEvent).
Java:
   private final Map<Integer, ActiveBuff> activeBuffs = new HashMap<Integer, ActiveBuff>();


Инкапсулируем её для надёжности. Создаём вспомогательные функции для безопасной работы с ней.
Java:
    //Проверка наличия эффектов.
    public boolean haveActiveBuffs() {

        return !this.activeBuffs.isEmpty();
    }

    //Быстрая проверка на наличие указанного баффа.
    public boolean isBuffActive(int buffId) {

        return this.activeBuffs.containsKey(buffId);
    }

    //Возвращает сет ключей-идентификаторов.
    public Set<Integer> activeBuffsIdSet() {

        return this.activeBuffs.keySet();
    }

    //Возвращает коллекцию активных баффов (ActiveBuff).
    public Collection<ActiveBuff> activeBuffsCollection() {

        return this.activeBuffs.values();
    }

    //Вспомогательный метод для синхронизационного пакета.
    @SideOnly(Side.CLIENT)
    public void putActiveBuffToMap(ActiveBuff buff) {

        this.activeBuffs.put(buff.getId(), buff);
    }

    //Вспомогательный метод для пакета удаления эффекта.
    @SideOnly(Side.CLIENT)
    public void removeActiveBuffFromMap(int buffId) {

        this.activeBuffs.remove(buffId);
    }

    //Получение активного баффа.
    public ActiveBuff getActiveBuff(int buffId) {

        return this.activeBuffs.get(buffId);
    }


Для хранения данных об эффектах между сессиями нам потребуется использование NBT. Класс с EEP имеет необходимые методы для хранения и загрузки данных, воспользуемся ими.
Java:
    @Override
    public void saveNBTData(NBTTagCompound mainCompound) {

        //Во избежание конфликтов с другими модами создаём собственный NBTTagCompound.
        NBTTagCompound buffsCompound = new NBTTagCompound();

        //Все наши данные храним в нашем компаунде.

        //Проверка карты на наличие баффов.
        if (this.haveActiveBuffs()) {

            //Создаём NBTTagList, в который запишем все активные баффы.
            NBTTagList tagList = new NBTTagList();

            //Перебор элементов коллекции активных эффектов.
            for (ActiveBuff buff : this.activeBuffsCollection()) {
 
                //Добавляем бафф в NBTTagList. Для этого предварительно упаковываем бафф в NBTTagCompound.
                tagList.appendTag(buff.saveBuffToNBT(buff));
            }

            //Сохраняем NBTTagList в NBTTagCompound.
            buffsCompound.setTag("buffs", tagList);
        }

        //Сохраняем его в общий NBTTagCompound, используя наш уникальный ключ - имя EEP.
        mainCompound.setTag(EEP_NAME, buffsCompound);
    }

    @Override
    public void loadNBTData(NBTTagCompound mainCompound) {

        //Загружаем наш NBTTagCompound.
        NBTTagCompound buffsCompound = (NBTTagCompound) mainCompound.getTag(EEP_NAME);

        //Проверяем, содержит ли NBT данные с указанным ключём. Второй параметр, цифра 9 - идентификатор типа NBTBase, в данном случае NBTTagList.
        if (buffsCompound.hasKey("buffs", 9)) {

            //Достаём NBTTagList из NBT.
            NBTTagList tagList = buffsCompound.getTagList("buffs", 10);//10 для NBTTagCompound.

            //Цикл, длиной равный кол-ву элементов в NBTTagList.
            for (int i = 0; i < tagList.tagCount(); ++i) {
 
                //Получаем NBTTagCompound по текущему номеру операции в цикле.
                NBTTagCompound tagCompound = tagList.getCompoundTagAt(i);
 
                //Распаковываем бафф из NBTTagCompound.
                ActiveBuff buff = ActiveBuff.readBuffFromNBT(tagCompound);

                if (buff != null) {
     
                    //Добавляем в карту активных баффов используя идентификатор как ключ и распакованный бафф как значение.
                    this.activeBuffs.put(buff.getId(), buff);
                }
            }
        }
    }


Напишем функцию, обрабатывающую активные эффекты. Она вызывает метод updateBuff() из класса активного эффекта (ActiveBuff), который в свою очередь вызывает метод onActive() из основного класса (Buff) и декрементит оставшееся время действия. Его необходимо вызывать из цикла обновления сущностей для обеих сторон (LivingUpdateEvent). Обновление эффектов будет происходить для обеих сторон, удалять истёкшие эффекты будем на сервере, игроку дополнительно будем отправлять пакет для удаления его эффектов.
Java:
    public void updateBuffs() {

        //Проверка наличия активных баффов.
        if (this.haveActiveBuffs()) {

            //Итератор по идентификаторам.
            Iterator buffsIterator = this.activeBuffsIdSet().iterator();

            while (buffsIterator.hasNext()) {

                int buffId = (Integer) buffsIterator.next();

                //Достаём бафф из карты используя идентификатор.
                ActiveBuff buff = this.getActiveBuff(buffId);

                //Вызов метода обновления баффа и одновременно проверка на истечения времени действия.
                if (!buff.updateBuff(this.livingBase, this.world)) {
 
                    if (!this.world.isRemote) {

                        //Снимаем эффект.
                        Buff.of(buffId).removeBuffEffect(this.livingBase, this.world, buff);
         
                        if (this.livingBase instanceof EntityPlayer) {
             
                            //Удаляем бафф с игрока на клиенте.
                            NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) this.livingBase);
                        }
         
                        //Удаляем на сервере.
                        buffsIterator.remove();
                    }
                }
            }
        }
    }


В пакете отправляем идентификатор эффекта для удаления и на клиенте удаляем:
Java:
        IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);

        if (buffs.isBuffActive(this.buffId)) {
 
            buffs.removeActiveBuffFromMap(this.buffId);;
        }


Добавление эффекта ентити. Для вызова получаем EEP. В качестве параметра передаём новый экземпляр активного эффекта (ActiveBuff). Если эффект с таким же идентификатором уже активен, происходит комбинирование эффектов.
Java:
     public void addBuff(ActiveBuff buff) {

        //Проверка наличия баффа с идентичным идентификатором.
        if (this.isBuffActive(buff.getId())) {

            //Если такой есть, достаем бафф из карты и сверяем уровни.
            //Если уровни совпадают, комбинируем (просто присваеваем активному баффу время действия добовляемого).
            //Если не совпадают, то во избежание сбоев при удалении эффекта (к примеру с атрибутами) по истечению времени
            //действия удаляем активный бафф и добавляем новый другого уровня.

            ActiveBuff activeBuff = this.getActiveBuff(buff.getId());

            if (activeBuff.getTier() == buff.getTier()) {
 
                activeBuff.combineBuffs(buff);

                if (livingBase instanceof EntityPlayer) {
         
                    if (!livingBase.world.isRemote) {
                 
                        //Уедомляем игрока если бафф был добавлен на сервере.
                        NetworkHandler.sendTo(new SyncBuff(activeBuff), (EntityPlayerMP) livingBase);
                    }
                }
            }

            else {
 
                this.removeBuff(activeBuff.getId());
 
                this.activeBuffs.put(buff.getId(), buff);
 
                Buff.of(buff.getId()).applyBuffEffect(this.livingBase, this.world, buff);

                if (livingBase instanceof EntityPlayer) {
             
                    if (!livingBase.world.isRemote) {
                 
                        //Синхронизируем бафф игрока с клиентом если бафф был добавлен на сервере.
                        NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) livingBase);
                    }
                }
            }
        }

        else {

            //Если баффа нет, добавляем в карту.
            this.activeBuffs.put(buff.getId(), buff);

            //Применяем эффект баффа.
            Buff.of(buff.getId()).applyBuffEffect(this.livingBase, this.world, buff);

             if (livingBase instanceof EntityPlayer) {
             
                 if (!livingBase.world.isRemote) {
                 
                     //Синхронизируем бафф игрока с клиентом если бафф был добавлен на сервере.
                     NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) livingBase);
                 }
             }
        }
    }

    //Удаляем бафф и его эффект. Нужен так же для внешнего удаления баффа с флагом isPersistent.
    public void removeBuff(int buffId) {
 
        if (this.isBuffActive(buffId)) {

            ActiveBuff activeBuff = this.getActiveBuff(buffId);

            Buff.of(buffId).removeBuffEffect(this.livingBase, this.world, activeBuff);

            this.activeBuffs.remove(buffId);

            if (livingBase instanceof EntityPlayer) {
         
                if (!livingBase.world.isRemote) {
             
                    //Уведомляем игрока об удалении баффа если удаление произошло на сервере.
                    NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) livingBase);
                }
            }
        }
    }


Дополнительно создадим метод для удаления всех активных эффектов. Добавим ему параметр, который будет отвечать за "режим" очистки. Предположим мы хотим удалять эффекты при смерти (только для игрока), но некоторые могут иметь флаг сохранения keepOnDeath или постоянства isPersistent, такие эффекты не удаляются. Вызов этого метода с параметром true удалит только удаляемые эффекты, false очистит активные эффекты полностью.
Java:
public void clearBuffs(boolean onDeath) {

        if (this.haveActiveBuffs()) {

            Iterator buffsIterator = this.activeBuffsIdSet().iterator();

            while (buffsIterator.hasNext()) {

                int buffId = (Integer) buffsIterator.next();

                ActiveBuff buff = this.getActiveBuff(buffId);

                if (!this.world.isRemote) {                                    
     
                    //Сохранение баффов при смерти доступно только для игрока.
                    if (this.livingBase instanceof EntityPlayer) {
         
                        //В зависимости от переданного параметра удаляются либо все эффекты, либо только те, которые не сохраняются при смерти.
                        if (onDeath) {
                               
                            //Удаляем баффы без флага keepOnDeath и isPersistent.
                            if (!Buff.of(buffId).shouldKeepOnDeath() && !Buff.of(buffId).isPersistent()) {
                         
                                //Удаляем эффект баффа.
                                Buff.of(buffId).removeBuffEffect(this.livingBase, this.world, buff);
             
                                //Удаляем бафф на серверной стороне.
                                buffsIterator.remove();
                            }
                        }
     
                        else {                                
         
                            //Если очистка производится по иным причинам, удаляем всё.
             
                            Buff.of(buffId).removeBuffEffect(this.livingBase, this.world, buff);
     
                            NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) this.livingBase);
         
                            buffsIterator.remove();
                        }
                    }
     
                    else {
         
                        //С других ентити снимаем все в любом случае.
         
                        Buff.of(buffId).removeBuffEffect(this.livingBase, this.world, buff);
                             
                        buffsIterator.remove();
                    }
                }
            }
        }
    }


События

Для игрока требуется синхронизация активных эффектов (например для визуализации). При смерти игрока эффекты должны сбрасываться и наконец необходимо впихнуть главный метод updateBuffs() в цикл обновления LivingUpdateEvent. Кроме того эффекты должны переносится в новую сущность игрока при смерти и перемещении между мирами. Нам потребуется использовать события.

Создаём класс, в котором будем работать с событиями.

Java:
public class BuffsEvents {

    //Регистрация EEP.
    @SubscribeEvent
    public void onEntityConstructing(EntityConstructing event) {

        //Регистрируем EEP для всех entity, которые наследуются от EntityLivingBase.
        if (event.entity instanceof EntityLivingBase) {

            if (BuffsLivingBase.get((EntityLivingBase) event.entity) == null) {

                BuffsLivingBase.register((EntityLivingBase) event.entity);
            }
        }
    }

    //Синхронизация баффов игрока при входе в мир.
    @SubscribeEvent
    public void onEntityJoinWorld(EntityJoinWorldEvent event) {

        if (event.entity instanceof EntityPlayer) {
         
            if (!event.entity.worldObj.isRemote) {
 
                EntityPlayer player = (EntityPlayer) event.entity;
 
                BuffsLivingBase eLivingBase = BuffsLivingBase.get(player);//Получении EEP с эффектами для игрока.
                 
                //Проверка карты на наличие баффов.
                if (eLivingBase.haveActiveBuffs()) {
     
                    //Перебор элементов коллекции активных эффектов.
                    for (ActiveBuff buff : eLivingBase.activeBuffsCollection()) {
                     
                        //Синхронизируем бафф с клиентом.
                        NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) player);
                    }
                }
            }
        }
    }

    //Вызов метода обновления эффектов всех ентити.
    @SubscribeEvent
    public void onPlayerUpdate(LivingUpdateEvent event) {

        if (event.entityLiving instanceof EntityLivingBase) {

            BuffsLivingBase.get(event.entityLiving).updateBuffs();
        }
    }

    //В случае смерти игрока снимаем все удаляемые эффекты.
    @SubscribeEvent
    public void onLivingDeath(LivingDeathEvent event) {

        if (event.entityLiving instanceof EntityPlayer) {

            if (!event.entityLiving.worldObj.isRemote) {

                //Вызываем метод удаления баффов.
                BuffsLivingBase.get(event.entityLiving).clearBuffs(true);
            }
        }
    }

    //В случае смерти или переходе между мирами загружаем наши эффекты для новой сущности игрока.
    @SubscribeEvent
    public void onPlayerClone(PlayerEvent.Clone event) {

        BuffsLivingBase eLivingBase = BuffsLivingBase.get(event.entityPlayer);

        NBTTagCompound tagCompound = new NBTTagCompound();

        BuffsLivingBase.get(event.original).saveNBTData(tagCompound);
        eLivingBase.loadNBTData(tagCompound);
    }
}

В пакете синхронизации на сервере получаем описание эффекта:
Java:
    private byte buffId, buffTier;

    private int buffDuration;
 
    public SyncBuff(EntityPlayer player, ActiveBuff buff) {

        //Разбираем бафф на сервере и отправляем описание на клиент.            
        this.buffId = (byte) buff.getId();
        this.buffTier = (byte) buff.getTier();

        this.buffDuration = buff.getDuration();
    }


На клиентской создаём и добавляем игроку:
Java:
IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);

buffs.putActiveBuffToMap(new ActiveBuff(this.buffId, this.buffTier, this.buffDuration));


Не забываем зарегистрировать наш класс с событиями в главном классе вашего проекта в FMLInitializationEvent.
Java:
    @EventHandler
    public void init(FMLInitializationEvent event) {

        MinecraftForge.EVENT_BUS.register(new BuffsEvents());
    }


Визуализация активных эффектов

Отлично, проделано много работы, а потрогать посмотреть всё ещё не на что. Прежде чем мы начнём создавать свои эффекты давайте добавим визуализацию для игрока. Класс, представленный ниже отвечает за отрисовку эффектов в оверлее. Иконки баффов, оставшееся время и уровень будут рисоваться в правом нижнем углу снизу вверх.

Java:
public class BuffsOverlay {

    private static BuffsOverlay instance = new BuffsOverlay();

    private static Minecraft mc = Minecraft.getMinecraft();

    //Указываем путь к файлу иконок и действительное название.
    private static final ResourceLocation buffIcons = new ResourceLocation(BuffsMain.MODID, "textures/gui/buffIcons.png");

    private BuffsOverlay() {}

    public static BuffsOverlay getInstance() {

        return instance;
    }

    public void renderBuffs() {

        if (this.mc.inGameHasFocus) {

            EntityPlayer player = this.mc.thePlayer;

            BuffsLivingBase eLivingBase = BuffsLivingBase.get(player);

            ScaledResolution scaledResolution = new ScaledResolution(this.mc, this.mc.displayWidth, this.mc.displayHeight);

            int
            i = scaledResolution.getScaledWidth() / 2 + 240,
            j = scaledResolution.getScaledHeight() - 30,
            counter = 0,
            index = 0;

            if (eLivingBase.haveActiveBuffs()) {
 
                //Перебор элементов коллекции активных эффектов.
                for (ActiveBuff buff : eLivingBase.activeBuffsCollection()) {
 
                    index = Buff.of(buff.getId()).getIconIndex();
 
                    counter++; 
 
                    GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
                    GL11.glDisable(GL11.GL_LIGHTING); 
                    GL11.glEnable(GL11.GL_BLEND);
                                 
                    GL11.glPushMatrix();
 
                    GL11.glTranslatef(i + 5, j + 25 - 24 * counter, 0.0F);
                    GL11.glScalef(0.5F, 0.5F, 0.5F);
 
                    this.mc.getTextureManager().bindTexture(buffIcons);
                    this.drawTexturedRect(0, 0, index % 8 * 32, index / 8 * 32, 32, 32, 32, 32); //Рендерим иконку.
 
                    GL11.glPopMatrix();
 
                    GL11.glPushMatrix();
 
                    GL11.glTranslatef(i + 15, j + 42 - 24 * counter, 0.0F);               
                    GL11.glScalef(0.7F, 0.7F, 0.7F);
     
                    int durLength = Buff.of(buff.getId()).getDurationForDisplay(buff).length();
                 
                    this.mc.fontRenderer.drawStringWithShadow(Buff.of(buff.getId()).getDurationForDisplay(buff), - durLength * 3, 0, 8421504);//Рендерим оставшееся время действия.
 
                    String tier = "";
 
                    if (buff.getTier() == 0) {
     
                        tier = "I";
                    }
 
                    else if (buff.getTier() == 1) {
     
                        tier = "II";
                    }
 
                    else if (buff.getTier() == 2) {
     
                        tier = "III";
                    }
 
                    this.mc.fontRenderer.drawStringWithShadow(tier, 7 - tier.length() * 3, - 23, 8421504);//И уровень эффекта.
     
                    GL11.glPopMatrix();
 
                    GL11.glDisable(GL11.GL_BLEND);                                
                    GL11.glEnable(GL11.GL_LIGHTING);
                }
            }
        }
    }

    private void drawTexturedRect(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {

        float f = 1.0F / (float) textureWidth;
        float f1 = 1.0F / (float) textureHeight;

        Tessellator tessellator = Tessellator.instance;

        tessellator.startDrawingQuads();

        tessellator.addVertexWithUV((double) (x), (double) (y + height), 0, (double) ((float) (u) * f), (double) ((float) (v + height) * f1));
        tessellator.addVertexWithUV((double) (x + width), (double) (y + height), 0, (double) ((float) (u + width) * f), (double) ((float) (v + height) * f1));
        tessellator.addVertexWithUV((double) (x + width), (double) (y), 0, (double) ((float) (u + width) * f), (double) ((float) (v) * f1));
        tessellator.addVertexWithUV((double) (x), (double) (y), 0, (double) ((float) (u) * f), (double) ((float) (v) * f1));

        tessellator.draw();
    }
}

Вызывать рендер будем с помощью события RenderGameOverlay.Post. Добавим это в наш класс с событиями.
Java:
    @SubscribeEvent
    public void onRenderOverlay(RenderGameOverlayEvent.Post event) {

        if (event.type == ElementType.CHAT) {

            BuffsOverlay.getInstance().renderBuffs();
        }
    }


Пример текстуры, добавьте её в нужную директорию. Или нарисуйте свою.
buff.png


Вы можете реализовать визуализацию как угодно и где угодно.

Создание тестового эффекта

Ну вот, надеюсь вы скопипастили написали всё правильно. Теперь можно проверить работоспособность. Создадим пустой эффект и добавим его игроку.

Объявляем и создаём экземпляр объекта Buff в нём же. Опишем его свойства.
Java:
    public static final Buff
    testBuff = new Buff().setName("buff.testBuff").setIconIndex(0, 0).keepOnDeath();


Допустим мы хотим добавлять игроку этот эффект когда он прыгает. В класс с событиями добавим следующий метод:
Java:
    @SubscribeEvent
    public void onJump(LivingJumpEvent event) {

        if (event.entityLiving instanceof EntityPlayer) {

        BuffsLivingBase eLivingBase = BuffsLivingBase.get((EntityPlayer) event.entityLiving);

            //Добавляем наш эффект длительностью 300 тиков (15 секунд).
            eLivingBase .addBuff(new ActiveBuff(Buff.testBuff.id, 300));
        }
    }


Ничего сложного, да? Заходим в игру и... прыгаем. Если в правом нижнем углу что то появилось - это успех! Эффект будет обновлять время действия (комбинироваться) при каждом прыжке и сохраняться при смерти. И хоть он пользы от него никакой, этот пример наглядно демонстрирует результат проделанной работы.

Примечание: искать справа снизу.

2018-01-21_00.15.50.png


P.S. Я не настроил должным образом положение иконок при изменении размера интерфейса в рендере. На скрине крупный интерфейс (недоступен для русского языка, на скрине выбран английский). С другим размером иконки будут смещаться.

Тест эффекта на мобе
Так, с игроком вроде всё ясно. А что насчёт мобов? Добавление эффектов мобам ничем не отличается.

Давайте создадим эффект, который будет отравлять любую сущность (EntityLivingBase) на 1 единицу здоровья каждю секунду. В Buff:
Java:
    public static final Buff
    intoxicationBuff = new Buff().setName("buff.intoxicationBuff").setIconIndex(5, 0);

    protected void onActive(EntityLivingBase entityLivingBase, World world, ActiveBuff buff) {

        int
        tier = buff.getTier(),
        duration = buff.getDuration();

        if (this.id == intoxicationBuff.id) {

            if (entityLivingBase.getHealth() > 1.0F) {
 
                entityLivingBase.attackEntityFrom(DamageSource.magic, 1.0F);
            }
        }
    }

    protected boolean isReady(ActiveBuff buff) {

        int
        tier = buff.getTier(),
        duration = buff.getDuration();

        if (this.id == intoxicationBuff.id) {

            return duration % 20 == 0;
        }

        return false;
    }


Обновим файл с иконками:
bufficons.png


Меняем размеры изображения (BuffOverlay#renderBuffs()):
Java:
//Размер изображения с иконками теперь 64*32.
this.drawTexturedRect(i + 5, j + 25 - 22 * counter, index % 8 * 32, index / 8 * 32, 32, 32, 64, 32);


Пусть эффект накладывается на мобов (и игроков) при атаках гнилой плотью:
Java:
     @SubscribeEvent
    public void onPlayerAttack(AttackEntityEvent event) {
 
        if (!event.entity.worldObj.isRemote) {

            if (event.target instanceof EntityLivingBase) {
 
                if (event.entityPlayer.getCurrentEquippedItem() != null && event.entityPlayer.getCurrentEquippedItem().getItem() == Items.rotten_flesh) {
 
                    BuffsLivingBase eEntityLiving = BuffsLivingBase.get((EntityLivingBase) event.target);
     
                    //Отравление на 5 секунд.
                    eEntityLiving.addBuff(new ActiveBuff(Buff.intoxicationBuff.id, 100));
                }
            }
        }
    }


Заходим и лупим по кому попадём. Эффект покажет себя характерным покраснением моба и звуком каждую секунду.


1.12.2
Для этой версии нам придётся заменить EEP на Capabilities и подправить пару функций и ещё по мелочам.

Изменения в основе
Основа в виде Buff и ActiveBuff осталась без изменений, смотрите для 1.7.10. Дублировать их тут не буду.

Изменения в использовании

Для добавления сущностям кастомных свойств теперь используются Capabilities. Для хранения и работы снашей системой нам придётся создать капу.

Первым делом создаём интерфейс и объявляем методы, которые нам потребуются. Почти все они аналогичны 1.7.10. Изменения коснулись только функций, работающих с объектом сущности. Если раньше EEP предоставляло им сущность, получаемую при регистрации, теперь нам предётся передовать её вручную.

Java:
public interface IBuffs {

    //Проверка наличия эффектов.
    boolean haveActiveBuffs();

    //Быстрая проверка на наличие указанного баффа.
    boolean isBuffActive(int buffId);

    //Возвращает сет ключей-идентификаторов.
    Set<Integer> activeBuffsIdSet();

    //Возвращает коллекцию активных баффов (ActiveBuff).
    Collection<ActiveBuff> activeBuffsCollection();

    //Вспомогательный метод для синхронизационного пакета.
    void putActiveBuffToMap(ActiveBuff buff);

    //Вспомогательный метод для пакета удаления эффекта.
    void removeActiveBuffFromMap(int buffId);

    //Копирование баффов из коллекции.
    void copyActiveBuffs(Collection<ActiveBuff> collection);

    //Получение активного баффа.
    ActiveBuff getActiveBuff(int buffId);

    //Добавление баффа ентити.
    void addBuff(ActiveBuff buff, EntityLivingBase livingBase);

    //Удаляем бафф и его эффект. Нужен так же для внешнего удаления баффа с флагом isPersistent.
    void removeBuff(EntityLivingBase livingBase, int buffId);

    //Метод для очищения активных баффов. Вызывается либо для полной очистки эффектов, либо при смерти.
    void clearBuffs(EntityLivingBase livingBase, boolean onDeath);

    //Метод обновления активных баффов. Вызывается в LivingUpdateEvent для EntityLivingBase на обеих сторонах.
    void updateBuffs(EntityLivingBase livingBase);
}

Теперь объект, предоставляющий реализацию. Реализация ничуть не изменилась. Подробности смотрите для 1.7.10.

Java:
public class Buffs implements IBuffs {

    //Карта активных баффов, которые имеет ентити. В качестве ключа используется идентификатор баффа,
    //а значением является экземпляр класса ActiveBuff, описывающий идентификатор, уровень и продолжительность эффекта.
    private final Map<Integer, ActiveBuff> activeBuffs = new HashMap<Integer, ActiveBuff>();

    @Override
    public boolean haveActiveBuffs() {

        return !this.activeBuffs.isEmpty();
    }

    @Override
    public boolean isBuffActive(int buffId) {

        return this.activeBuffs.containsKey(buffId);
    }

    @Override
    public Set<Integer> activeBuffsIdSet() {

        return this.activeBuffs.keySet();
    }

    @Override
    public Collection<ActiveBuff> activeBuffsCollection() {

        return this.activeBuffs.values();
    }

    @Override
    public void putActiveBuffToMap(ActiveBuff buff) {

        this.activeBuffs.put(buff.getId(), buff);
    }

    @Override
    public void removeActiveBuffFromMap(int buffId) {

        this.activeBuffs.remove(buffId);
    }

    @Override
    public void copyActiveBuffs(Collection<ActiveBuff> collection) {

        for (ActiveBuff buff : collection) {

            this.activeBuffs.put(buff.getId(), buff);
        }
    }

    @Override
    public ActiveBuff getActiveBuff(int buffId) {

        return this.activeBuffs.get(buffId);
    }

    @Override
    public void addBuff(ActiveBuff buff, EntityLivingBase livingBase) {

        //Проверка наличия баффа с идентичным идентификатором.
        if (this.isBuffActive(buff.getId())) {

            //Если такой есть, достаем бафф из карты и сверяем уровни.
            //Если уровни совпадают, комбинируем (просто присваеваем активному баффу время действия добовляемого).
            //Если не совпадают, то во избежание сбоев при удалении эффекта (к примеру с атрибутами) по истечению времени
            //действия удаляем активный бафф и добавляем новый другого уровня.

            ActiveBuff activeBuff = this.getActiveBuff(buff.getId());

            if (activeBuff.getTier() == buff.getTier()) {
    
                activeBuff.combineBuffs(buff);

                if (livingBase instanceof EntityPlayer) {
         
                    if (!livingBase.world.isRemote) {
                 
                        //Уедомляем игрока если бафф был добавлен на сервере.
                        NetworkHandler.sendTo(new SyncBuff(activeBuff), (EntityPlayerMP) livingBase);
                    }
                }
            }

            else {
    
                this.removeBuff(livingBase, activeBuff.getId());
    
                this.activeBuffs.put(buff.getId(), buff);
    
                Buff.of(buff.getId()).applyBuffEffect(livingBase, livingBase.world, buff);

               if (livingBase instanceof EntityPlayer) {
         
                   if (!livingBase.world.isRemote) {
             
                       //Синхронизируем бафф игрока с клиентом если бафф был добавлен на сервере.
                       NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) livingBase);
                   }
               }
            }
        }

        else {

            //Если баффа нет, добавляем в карту.
            this.activeBuffs.put(buff.getId(), buff);

            //Применяем эффект баффа.
            Buff.of(buff.getId()).applyBuffEffect(livingBase, livingBase.world, buff);

            if (livingBase instanceof EntityPlayer) {
         
                if (!livingBase.world.isRemote) {
             
                    //Синхронизируем бафф игрока с клиентом если бафф был добавлен на сервере.
                    NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) livingBase);
                }
            }
        }
    }

    @Override
    public void removeBuff(EntityLivingBase livingBase, int buffId) {

        if (this.isBuffActive(buffId)) {

            ActiveBuff activeBuff = this.getActiveBuff(buffId);

            Buff.of(buffId).removeBuffEffect(livingBase, livingBase.world, activeBuff);

            this.activeBuffs.remove(buffId);

            if (livingBase instanceof EntityPlayer) {
         
                if (!livingBase.world.isRemote) {
             
                    //Уведомляем игрока об удалении баффа если удаление произошло на сервере.
                    NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) livingBase);
                }
            }
        }
    }

    @Override
    public void clearBuffs(EntityLivingBase livingBase, boolean onDeath) {

        if (this.haveActiveBuffs()) {

            Iterator buffsIterator = this.activeBuffsIdSet().iterator();

            while (buffsIterator.hasNext()) {

                int buffId = (Integer) buffsIterator.next();

                ActiveBuff buff = this.getActiveBuff(buffId);

                if (!livingBase.world.isRemote) {                                       
        
                    //Сохранение баффов при смерти доступно только для игрока.
                    if (livingBase instanceof EntityPlayer) {
            
                        //В зависимости от переданного параметра удаляются либо все эффекты, либо только те, которые не сохраняются при смерти.
                        if (onDeath) {
                                  
                            //Удаляем баффы без флага keepOnDeath и isPersistent.
                            if (!Buff.of(buffId).shouldKeepOnDeath() && !Buff.of(buffId).isPersistent()) {
                            
                                //Удаляем эффект баффа.
                                Buff.of(buffId).removeBuffEffect(livingBase, livingBase.world, buff);
        
                                //Удаляем бафф на клиентской стороне.
                                NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) livingBase);
                
                                //Удаляем бафф на серверной стороне.
                                buffsIterator.remove();
                            }
                        }
        
                        else {                                   
            
                            //Если очистка производится по иным причинам, удаляем всё.
                
                            Buff.of(buffId).removeBuffEffect(livingBase, livingBase.world, buff);
        
                            NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) livingBase);
            
                            buffsIterator.remove();
                        }
                    }
        
                    else {
            
                        //С других ентити снимаем все в любом случае.
            
                        Buff.of(buffId).removeBuffEffect(livingBase, livingBase.world, buff);
                                
                        buffsIterator.remove();
                    }
                }
            }
        }
    }

    @Override
    public void updateBuffs(EntityLivingBase livingBase) {

        //Проверка наличия активных баффов.
        if (this.haveActiveBuffs()) {

            //Итератор по идентификаторам.
            Iterator buffsIterator = this.activeBuffsIdSet().iterator();

            while (buffsIterator.hasNext()) {

                int buffId = (Integer) buffsIterator.next();

                //Достаём бафф из карты используя идентификатор.
                ActiveBuff buff = this.getActiveBuff(buffId);

                //Вызов метода обновления баффа и одновременно проверка на истечения времени действия.
                if (!buff.updateBuff(livingBase, livingBase.world)) {
    
                    if (!livingBase.world.isRemote) {

                        //Снимаем эффект.
                        Buff.of(buffId).removeBuffEffect(livingBase, livingBase.world, buff);
            
                        if (livingBase instanceof EntityPlayer) {
                
                            //Удаляем бафф с игрока на клиенте.
                            NetworkHandler.sendTo(new RemoveBuff(buffId), (EntityPlayerMP) livingBase);
                        }
            
                        //Удаляем на сервере.
                        buffsIterator.remove();
                    }
                }
            }
        }
    }
}

В пакетах действуем аналогично 1.7.10.

Хранение между сессиями теперь реализуется в отдельном классе с интерфейсом IStorage, сама процедура чтения/записи данных в NBT аналогична 1.7.10. Создаём объект, реализуем IStorage для IBuffs.

Java:
public class BuffsStorage  implements IStorage<IBuffs> {

    @Override
    public NBTBase writeNBT(Capability<IBuffs> capability, IBuffs instance, EnumFacing side) {
    
        NBTTagCompound buffsCompound = new NBTTagCompound();
    
        //Проверка карты на наличие баффов.
        if (instance.haveActiveBuffs()) {

            //Создаём NBTTagList, в который запишем все активные баффы.
            NBTTagList tagList = new NBTTagList();

            //Перебор элементов коллекции активных эффектов.
            for (ActiveBuff buff : instance.activeBuffsCollection()) {
    
                //Добавляем бафф в NBTTagList. Для этого предварительно упаковываем бафф в NBTTagCompound.
                tagList.appendTag(buff.saveBuffToNBT(buff));
            }

            //Сохраняем NBTTagList в NBTTagCompound.
            buffsCompound.setTag("buffs", tagList);
        }

        return buffsCompound;
    }

    @Override
    public void readNBT(Capability<IBuffs> capability, IBuffs instance, EnumFacing side, NBTBase nbt) {

        //Загружаем наш NBTTagCompound.
        NBTTagCompound buffsCompound = (NBTTagCompound) nbt;

        //Проверяем, содержит ли NBT данные с указанным ключём. Второй параметр, цифра 9 - идентификатор типа NBTBase, в данном случае NBTTagList.
        if (buffsCompound.hasKey("buffs", 9)) {

            //Достаём NBTTagList из NBT.
            NBTTagList tagList = buffsCompound.getTagList("buffs", 10);//10 для NBTTagCompound.

            //Цикл, длиной равный кол-ву элементов в NBTTagList.
            for (int i = 0; i < tagList.tagCount(); ++i) {
    
                //Получаем NBTTagCompound по текущему номеру операции в цикле.
                NBTTagCompound tagCompound = tagList.getCompoundTagAt(i);
    
                //Распаковываем бафф из NBTTagCompound.
                ActiveBuff buff = ActiveBuff.readBuffFromNBT(tagCompound);

                if (buff != null) {
        
                    //Добавляем в карту активных баффов используя идентификатор как ключ и распакованный бафф как значение.
                    instance.putActiveBuffToMap(buff);
                }
            }
        }
    }
}

Для получения и работы с капой нам нужен класс с интерфейсом ICapabilitySerializable.

Java:
public class BuffsProvider implements ICapabilitySerializable<NBTBase> {

    @CapabilityInject(IBuffs.class)
    public static final Capability<IBuffs> BUFFS_CAP = null;

    private IBuffs instance = BUFFS_CAP.getDefaultInstance();

    @Override
    public boolean hasCapability(Capability<?> capability, EnumFacing facing) {

        return capability == BUFFS_CAP;
    }

    @Override
    public <T> T getCapability(Capability<T> capability, EnumFacing facing) {

        return capability == BUFFS_CAP ? BUFFS_CAP.<T> cast(this.instance) : null;
    }

    @Override
    public NBTBase serializeNBT() {

        return BUFFS_CAP.getStorage().writeNBT(BUFFS_CAP, this.instance, null);
    }

    @Override
    public void deserializeNBT(NBTBase nbt) {

        BUFFS_CAP.getStorage().readNBT(BUFFS_CAP, this.instance, null, nbt);
    }
}

Для добавления Capabilities сущностям используем AttachCapabilitiesEvent. Вы можете создать отдельный класс для этого эффекта. Опять же, вы можете реализовать систему эффектов лишь для некоторых существ.
Java:
public class BuffsCupRegistrationEvent {

    public static final ResourceLocation BUFFS_CAP = new ResourceLocation(BuffsMain.MODID, "Buffs");

    @SubscribeEvent
    public void attachCapability(AttachCapabilitiesEvent event) {

        if (event.getObject() instanceof EntityLivingBase){

            event.addCapability(BUFFS_CAP, new BuffsProvider());           
        }
    }
}


Не забываем зарегистрировать его в CommonProxy в фазе FMLInitializationEvent. Там же регистрируем нашу капу.
Java:
    public void init(FMLInitializationEvent event) {

        CapabilityManager.INSTANCE.register(IBuffs.class, new BuffsStorage(), Buffs.class);

        MinecraftForge.EVENT_BUS.register(new BuffsCupRegistrationEvent());
    }


Далее нам потребуется использовать ряд событий для выполнения важных функций.

Внедряем вызов функции обновления (тика) активных эффектов в LivingUpdateEvent.
Java:
    @SubscribeEvent
    public void onLivingUpdate(LivingUpdateEvent event) {

        if (event.getEntityLiving() instanceof EntityLivingBase) {

            EntityLivingBase livingBase = event.getEntityLiving();

            livingBase.getCapability(BuffsProvider.BUFFS_CAP, null).updateBuffs(livingBase);
        }
    }


Синхронизация эффектов с клиентом при входе в мир/смерти/перемещении между мирами. Как оказалось EntityJoinWorldEvent для этого теперь не подходит, так как при его срабатывании на сервере (и отправке пакетов) клиентский игрок ещё не заспавнен. Но хрен там.
Java:
    @SubscribeEvent
    public void onPlayerJoinWorld(EntityJoinWorldEvent event) {

        if (event.getEntity() instanceof EntityPlayer) {
                        
            EntityPlayer player = (EntityPlayer) event.getEntity();

            if (player.world.isRemote) {
    
                //Дожидаемся когда игрок будет полностью загружен на клиенте и шлём запрос
                //на синхронизацию активных эффектов.
    
                if (player != null) {
        
                    NetworkHandler.sendToServer(new BuffsSyncRequest());
                }
            }
        }
    }


Пакет-запрос не передаёт на сервер ничего, лишь запускает процесс синхронизации:
Java:
        IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);

        //Проверка на наличие активных баффов.
        if (buffs.haveActiveBuffs()) {
             
            for (ActiveBuff buff : buffs.activeBuffsCollection()) {
                 
                //Синхронизируем бафф с клиентом.
                NetworkHandler.sendTo(new SyncBuff(buff), (EntityPlayerMP) player);
            }
        }


Пакет синхронизации идентичен 1.7.10, смотрите там.

Элегантно ;). По крайней мере все отлично синхронизируется.

Очистка активных эффектов при смерти.
Java:
    @SubscribeEvent
    public void onPlayerDeath(LivingDeathEvent event) {

        if (event.getEntityLiving() instanceof EntityPlayer) {
            
            if (!event.getEntityLiving().world.isRemote) {
    
                EntityPlayer player = (EntityPlayer) event.getEntityLiving();
    
                IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);

                buffs.clearBuffs(player, true);
            }
        }
    }


Перенос капы для новой сущности игрока.
Java:
    @SubscribeEvent
    public void onPlayerClone(PlayerEvent.Clone event) {
            
        EntityPlayer player = event.getEntityPlayer();

        IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);//Капа новой сущности.

        IBuffs oldBuffs = event.getOriginal().getCapability(BuffsProvider.BUFFS_CAP, null);//Капа старой сушности.

        buffs.copyActiveBuffs(oldBuffs.activeBuffsCollection());//Копируем коллекцию эффектов.
    }


Изменения визуализации
Их немного. Класс для рендера идентичен версии 1.7.10, за исключением того, что нужно убрать функции с GL11.GL_LIGHTING и заменить GL11.glColor4f() на GlStateManager.color(), а так же использовать метод Gui#drawModalRectWithCustomSizedTexture() для отрисовки иконки. Имя файла иконок не должно содержать буквы верхнего регистра, поправьте это.

Ну и работа с коллекцией эффектов:
Java:
            IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);

            if (buffs.haveActiveBuffs()) {
     
                for (ActiveBuff buff : buffs.activeBuffsCollection()) {}
            }

Тестовый эффект
Самое время испытать наши возможности. В качестве примера создадим эффект вывиха, который игрок может получить при падении. Этот эффект будет значительно замедлять игрока и для того, что бы исцелить его, добавим необходимость поспать (халтура).

Создаём объект Buff, описываем его. Реализуем эффект умножением entityLivingBase#motionX и entityLivingBase#motionZ на коэффициент замедления (пусть будет 0.4F - потеря 60% скорости) в Buff#onActive, в Buff#isReady() вернём true для применения эффекта каждый тик.
Java:
public static final Buff
    sprain = new Buff().setName("buff.sprain").setIconIndex(1, 0).setPersistent();

    protected void onActive(EntityLivingBase entityLivingBase, World world, ActiveBuff buff) {
  
        int
        tier = buff.getTier(),
        duration = buff.getDuration();
  
        if (this.id == sprain.id) {
      
            entityLivingBase.motionX *= 0.4F;
            entityLivingBase.motionZ *= 0.4F;
        }
    }

    protected boolean isReady(ActiveBuff buff) {
  
        int
        tier = buff.getTier(),
        duration = buff.getDuration();
  
        if (this.id == sprain.id) {
      
            return true;
        }
  
        return false;
    }


Добавим текстуры для иконок. Вот пример:
bufficons.png


Воспользуемся событиями LivingFallEvent для добавления и PlayerWakeUpEvent для удаления эффекта:
Java:
    @SubscribeEvent
    public void onPlayerFall(LivingFallEvent event) {
    
        if (!event.getEntity().world.isRemote) {
        
            if (event.getEntityLiving() instanceof EntityPlayer) {
            
                EntityPlayer player = (EntityPlayer) event.getEntityLiving();
            
                if (!player.capabilities.isCreativeMode) {
            
                    if (event.getDistance() > 5.0F) {//При падении с высоты больше пяти блоков.
                
                        IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);
                
                        if (!buffs.isBuffActive(Buff.sprain.id)) {
                
                            //Добавляем эффект вывиха.
                            buffs.addBuff(new ActiveBuff(Buff.sprain.id), player);
                        }
                    }
                }
            }
        }
    }

    @SubscribeEvent
    public void onWakeUp(PlayerWakeUpEvent event) {
     
        EntityPlayer player = event.getEntityPlayer();
                                             
        IBuffs buffs = player.getCapability(BuffsProvider.BUFFS_CAP, null);
             
        if (buffs.isBuffActive(Buff.sprain.id)) {
                 
            buffs.removeBuff(player, Buff.sprain.id);
        }
    }


Тестируем. Что бы получить эффект перелома вам нужно упасть с высоты больше пяти блоков. В правом нижнем углу появится иконка эффекта, время действия "-:-" означает что эффект бессрочный, вы не сможете избавиться от него (даже умерев) пока не поспите. При этом требуется действительно лечь в кровать (спам ПКМ по кровати днём вам не поможет). Как то так.

2018-01-24_23.07.38.png

Заключение

На этом пока всё, спасибо за внимание. Теперь у вас есть возможность создавать свои собственные временные эффекты для любой сущности. Я очень надеюсь что этот туториал пригодится кому-нибудь. Прошу прощения за возможные орфографические ошибки. Оцените статью и оставьте свои замечания.
Автор
AustereTony
Скачивания
18
Просмотры
4,684
Первый выпуск
Обновление
Оценка
4.38 звёзд 8 оценок

Другие ресурсы пользователя AustereTony

Последние обновления

  1. Обновление #3

    Туториал адаптирован под 1.12.2, а так же добавлена ссылка на исходники. Множественные правки...
  2. Обновление #2

    Туториал переписан для использования со всеми EntityLivingBase. Вся работа с эффектами...
  3. Обновление #1

    Обновлён BuffManager. Заменил обычный массив на ArrayList, добавил метод getBuff() для упрощения...

Последние рецензии

Годнота! Ресурс очень пригодился.
Полезно, когда нужен эффект без зелья.
Сам туториал хороший, но практического применения в упор не вижу.
Всё это можно сделать гораздо проще и на обычных Potion'ах - вечность длительностью в Integer.MAX_VALUE; сохранение при смерти - копирование зелья в эвенте клонирования.
Давно пользуюсь чем-то подобным. Очень удобно и дает полную свободу действий. Не плохо было бы еще для ентитей сделать, а не только для игрока
AustereTony
AustereTony
Обязательно напишу!
Зачем делать массив и костылить с размером, если есть ArrayList с тем же функционалом?
AustereTony
AustereTony
А ведь точно... Обновил.
Так и не понял, в чем отличие от ванильных?
(к слову extends Potion, я тоже могу сделать эффект и при его применении в эвенте тик плеера делать все что мне требуется).
AustereTony
AustereTony
В эвенте не советую) В наследнике Potion можно переопределить все необходимые методы для применения чего угодно. А если по существу, то создание собственной системы позволяет слепить всё что угодно в зависимости от потребностей и не добавлять ничего лишнего.
Сверху