Версия(и) Minecraft
1.12+
Здравствуйте. В этой статье речь пойдёт о мылозельеварении и всем что с этим связано. Будут рассмотрены процессы создания эффектов, добавления зелий как предметов и рецептов для них, а так же некоторые связанные с этим хитрости. Исходники доступны в моём репозитории.

Зельеварение
В последних версиях работа с зельями стала проще, и здесь я опишу полный алгоритм создания собственных крафтовых зелий.

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

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

Примеры: зелье лечения, мгновенный урон.

Чтобы создать такой эффект необходимо в его классе переопределить
Potion#isInstant() и вернуть в нём true и Potion#affectEntity(). Метод affectEntity() будет вызван единожды при применении эффекта.
Java:
    @Override
    public boolean isInstant() {
     
        return true;
    }
 
    @Override
    public void affectEntity(@Nullable Entity source, @Nullable Entity indirectSource, EntityLivingBase entityLivingBase, int amplifier, double health) {

        //Применение
    }


Периодический эффект

Примеры: регенерация, отравление.

Для такого эффекта требуется переопределение Potion#isReady() и Potion#performEffect().
Метод performEffect() буден вызван только если isReady() вернёт true. Так как вызов isReady() происходит каждый игровой тик можно организовать применение эффекта с необходимой частотой используя нехитрую логическую конструкцию с делением по модулю.
Java:
    @Override
    public void performEffect(EntityLivingBase entityLivingBase, int amplifier) {
     
        //Применение
    }
 
    @Override
    public boolean isReady(int duration, int amplifier) {
     
        return duration % 20 == 0;//true каждую секунду.
    }


Эффект с модификацией атрибутов

Примеры: скорость, спешка, повышение урона.

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

Potion#applyAttributesModifiersToEntity() вызывается при применении, а Potion#removeAttributesModifiersFromEntity()
при удалении.

Java:
    @Override
    public void applyAttributesModifiersToEntity(EntityLivingBase entityLivingBase, AbstractAttributeMap attributeMap, int amplifier) {
        //Добавление
    }
 
    @Override
    public void removeAttributesModifiersFromEntity(EntityLivingBase entityLivingBase, AbstractAttributeMap attributeMap, int amplifier) {
        //Удаление
    }


Эффект с действием вне класса

Примеры: невидимость.

Бывают случаи когда применить эффект в его классе не получается.
Для этого для отслеживания активности эффекта в классе EntityLivingBase содержится несколько методов:
EntityLivingBase#isPotionActive(Potion) - проверка наличия эффекта,
EntityLivingBase#getActivePotionEffect(Potion) - получение эффекта (можно получить описание с помощью геттеров).

Ну а теперь перейдём к практике. В качестве примера я опишу процесс создания эффекта с периодическим действием, который будет лечить игрока в обмен на очки опыта (два уровня - лечение раз в секунду или раз пол секунды). Примеры остальных эффектов смотрите на GitHub.

В первую очередь для эффекта требуется создать класс-наследник Potion, я назвал его PotionEquilibrium:
Java:
public class PotionEquilibrium extends Potion {
 
    public PotionEquilibrium(String potionName, boolean isBadEffect, int liquidColor) {
     
        super(isBadEffect, liquidColor);
     
        this.setName(potionName);
    }
 
    public void setName(String potionName) {
     
        this.setRegistryName(PotionsMain.MODID, potionName);//Ваш modid первым параметром.
        this.setPotionName("effect." + this.getRegistryName().toString());
    }
}


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

Локализация названия эффекта. Имя будет задано при инициализации эффекта чуть позже.
effect.potions:equilibrium=Равноценный обмен

Переопределение isReady() и performEffect(), реализация эффекта. Как сразу становится видно наш эффект будет иметь два уровня (применение каждую секунду и применение раз в секунду):
Java:
    @Override
    public void performEffect(EntityLivingBase entityLivingBase, int amplifier) {
     
        if (entityLivingBase instanceof EntityPlayer) {
         
            EntityPlayer player = (EntityPlayer) entityLivingBase;
         
            if (player.getHealth() < player.getMaxHealth()) {
             
                if (player.experienceLevel >= 1) {
                 
                    player.heal(2.0F);//Хил 2 пункта (1 сердце).
                 
                    player.experienceLevel -= 1;//За один уровень опыта.
                }
            }
        }
    }
 
    @Override
    public boolean isReady(int duration, int amplifier) {
     
        return duration % (amplifier == 0 ? 20 : 10) == 0;
    }


Эффекту необходима иконка. Что бы её добавить создаем константу для пути к иконке, инициализируем в конструкторе, переопределяем и наполняем методы рендера. В данном случае название иконки совпадает с названием эффекта:
Java:
public class PotionEquilibrium extends Potion {
 
    private final ResourceLocation icon;

    public PotionEquilibrium(String potionName, boolean isBadEffect, int liquidColor) {
     
        super(isBadEffect, liquidColor);
             
        this.icon = new ResourceLocation(PotionsMain.MODID, "textures/potions/" + potionName + ".png");
    }
 
    @Override
    public boolean hasStatusIcon() {
     
        return false;
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void renderInventoryEffect(int x, int y, PotionEffect potionEffect, Minecraft mc) {
     
        if (mc.currentScreen != null) {
         
            mc.getTextureManager().bindTexture(this.icon);
         
            Gui.drawModalRectWithCustomSizedTexture(x + 6, y + 7, 0, 0, 18, 18, 18, 18);
        }
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void renderHUDEffect(int x, int y, PotionEffect potionEffect, Minecraft mc, float alpha) {
     
        mc.getTextureManager().bindTexture(this.icon);
     
        Gui.drawModalRectWithCustomSizedTexture(x + 3, y + 3, 0, 0, 18, 18, 18, 18);
    }
}


Разместите иконку в папке "src\main\resources\assets\ваш modid\textures\potions":
equilibrium.png


В итоге класс должен выглядеть как то так:
Java:
public class PotionEquilibrium extends Potion {
 
    private final ResourceLocation icon;

    public PotionEquilibrium(String potionName, boolean isBadEffect, int liquidColor) {
     
        super(isBadEffect, liquidColor);
     
        this.setName(potionName);
     
        this.icon = new ResourceLocation(PotionsMain.MODID, "textures/potions/" + potionName + ".png");
    }
 
    public void setName(String potionName) {
     
        this.setRegistryName(PotionsMain.MODID, potionName);
        this.setPotionName("effect." + this.getRegistryName().toString());
    }
 
    @Override
    public void performEffect(EntityLivingBase entityLivingBase, int amplifier) {
     
        if (entityLivingBase instanceof EntityPlayer) {
         
            EntityPlayer player = (EntityPlayer) entityLivingBase;
         
            if (player.getHealth() < player.getMaxHealth()) {
             
                if (player.experienceLevel >= 1) {
                 
                    player.heal(2.0F);//Хил 2 пункта (1 сердце).
                 
                    player.experienceLevel -= 1;//За один уровень опыта.
                }
            }
        }
    }
 
    @Override
    public boolean isReady(int duration, int amplifier) {
     
        return duration % (amplifier == 0 ? 20 : 10) == 0;
    }
 
    @Override
    public boolean hasStatusIcon() {
     
        return false;
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void renderInventoryEffect(int x, int y, PotionEffect potionEffect, Minecraft mc) {
     
        if (mc.currentScreen != null) {
         
            mc.getTextureManager().bindTexture(this.icon);
         
            Gui.drawModalRectWithCustomSizedTexture(x + 6, y + 7, 0, 0, 18, 18, 18, 18);
        }
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void renderHUDEffect(int x, int y, PotionEffect potionEffect, Minecraft mc, float alpha) {
     
        mc.getTextureManager().bindTexture(this.icon);
     
        Gui.drawModalRectWithCustomSizedTexture(x + 3, y + 3, 0, 0, 18, 18, 18, 18);
    }
}

Регистрация... Теперь никакой рефлексии для расширения массивов, всё как у людей, спасибо команде Forge. Для объявления и регистрации эффектов создадим отдельный класс PotionsRegistry. Так как регистрация эффектов происходит через эвент RegistryEvent.Register, пометим класс аннотацией @Mod.EventBusSubscriber(modid = Main.MODID), которая обеспечит автоматическое подключение методов-эвентов (с аннотацией @SubscribeEvent) при загрузке мода:
Java:
@Mod.EventBusSubscriber(modid = PotionsMain.MODID)
public class PotionsRegistry {

    /** Эффекты */
    public static final Potion  
    EQUILIBRIUM = new PotionEquilibrium("equilibrium", false, 0x77FFA9);//Периодический эффект (обмен опыта на здоровье).
 
    @SubscribeEvent
    public static void registerPotions(RegistryEvent.Register<Potion> event) {
             
        event.getRegistry().registerAll(
             
                EQUILIBRIUM        
        );
    }
}


Фордж подгрузит класс и метод с регистрацией самостоятельно.

Ну вот и всё. Используя EntityLivingBase#addPotionEffect(),
можно добавить игроку этот эффект, предварительно обернув его в PotionEffect. Стандартные эффекты перенесены из Potion в MobEffects.

Создание зелья

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

Таким образом всё что нужно сделать для создания зелий это создать новый PotionType и зарегистрировать его. Создадим зелья с нашим эффектом, которые будут иметь каноничные модификации в виде увеличенного времени действия и силы. Делать всё это будем в нашем PotionsRegistry:
Java:
@Mod.EventBusSubscriber(modid = PotionsMain.MODID)
public class PotionsRegistry {

    /** Эффекты */
    public static final Potion
    EQUILIBRIUM = new PotionEquilibrium("equilibrium", false, 0x77FFA9);//Периодический эффект (обмен опыта на здоровье).
     
    /** "Типы", представляющие зелья как предметы */
    public static final PotionType
    EQUILIBRIUM_TYPE_STANDARD = createPotionType(null, new PotionEffect(EQUILIBRIUM, 600)),
    EQUILIBRIUM_TYPE_LONG = createPotionType("long", new PotionEffect(EQUILIBRIUM, 900)),
    EQUILIBRIUM_TYPE_STRONG = createPotionType("strong", new PotionEffect(EQUILIBRIUM, 600, 1));

    /**
     * Возвращает новый экземпляр PotionType с именем эффекта.
     *
     * @param namePrefix
     * @param potionEffect
     *
     * @return PotionType
     */
    private static PotionType createPotionType(String namePrefix, PotionEffect potionEffect) {    
         
        ResourceLocation potionName = potionEffect.getPotion().getRegistryName();

        ResourceLocation potionTypeName;
     
        if (namePrefix != null)    
            potionTypeName = new ResourceLocation(potionName.getResourceDomain(), namePrefix + "_" + potionName.getResourcePath());                
        else    
            potionTypeName = potionName;

        return new PotionType(potionName.toString(), potionEffect).setRegistryName(potionTypeName);
    }
 
    /**
     * Возвращает новый экземпляр PotionType с указанным именем. Используется для зелий с несколькими эффектами.
     *
     * @param typeName
     * @param namePrefix
     * @param potionEffects
     *
     * @return PotionType
     */
    private static PotionType createCompositePotionType(String typeName, String namePrefix, PotionEffect... potionEffects) {    
     
        ResourceLocation potionTypeName;
     
        if (namePrefix != null)
            potionTypeName = new ResourceLocation(PotionsMain.MODID, namePrefix + "_" + typeName);                
        else    
            potionTypeName = new ResourceLocation(PotionsMain.MODID, typeName);

        return new PotionType(typeName, potionEffects).setRegistryName(potionTypeName);
    }
 
    @SubscribeEvent
    public static void registerPotions(RegistryEvent.Register<Potion> event) {
             
        event.getRegistry().registerAll(
         
                EQUILIBRIUM
        );
    }
 
    @SubscribeEvent
    public static void registerPotionTypes(RegistryEvent.Register<PotionType> event) {
     
        event.getRegistry().registerAll(
                         
                EQUILIBRIUM_TYPE_STANDARD,
                EQUILIBRIUM_TYPE_LONG,
                EQUILIBRIUM_TYPE_STRONG
        );
    }
}


Стоит отметить, что если использовать расширенный конструктор PotionEffect и отключить рендер частиц зелья вокруг ентити (false последним параметром) при создании PotionType, то цвет колбы такого зелья будет чёрным.

Локализация зелий:
Код:
potion.effect.potions:equilibrium=Зелье Равноценного обмена
splash_potion.effect.potions:equilibrium=Взрывное Зелье Равноценного обмена
lingering_potion.effect.potions:equilibrium=Оседающее Зелье Равноценного обмена


Регистрация аналогична эффектам, происходит автоматом.

Рецепты

Осталось добавить рецепты зельям. Есть два способа:

Для добавления рецептов аналогичных стандартным рекомендую использовать PotionHelper#addMix(PotionType, Item, PotionType), где параметры: основа, ингредиент, результат соответственно. В качестве основы можно задать как стандартные основы (PotionTypes#AWKWARD, PotionTypes#MUNDANE и PotionTypes#THICK) так и любые другие зелья. Результат так же может быть любым зельем. Минус этого способа состоит в типе ингредиента, который должен быть инстансом Item, что не позволит использовать в качестве ингредиентов другие зелья и проверять NBT. Все ванильные зелья зарегистрированы таким образом.

Второй способ это использование BrewingRecipeRegistry#addRecipe(ItemStack, ItemStack, ItemStack). Однако этот метод не сверяет NBT переданных стаков и поэтому адекватной работы от него не ждите. Для добавления собственных рецептов с зельями в качестве ингредиентов в частности единственным решением является создание собственного рецепта с реализацией IBrewingRecipe и использование перегруженного BrewingRecipeRegistry#addRecipe(IBrewingRecipe) для его регистрации. Пример вы можете найти в исходниках в моём репозитории.

Для зелья из примера создадим рецепт первым способом. В классе PotionsRegistry:
Java:
    public static void registerSimpleRecipes() {
             
        addStandardRecipes(EQUILIBRIUM_TYPE_STANDARD, EQUILIBRIUM_TYPE_LONG, EQUILIBRIUM_TYPE_STRONG, Items.EXPERIENCE_BOTTLE);
    }
 
    /**
     * Создание стандартных рецептов для зелий.
     *
     * @param standardPotionType
     * @param longPotionType
     * @param strongPotionType
     * @param ingredient
     */
    private static void addStandardRecipes(PotionType standardPotionType, PotionType longPotionType, PotionType strongPotionType, Item ingredient) {
     
        PotionHelper.addMix(PotionTypes.AWKWARD, ingredient, standardPotionType);//Аргументы: основа, ингредиент, результат.
        if (longPotionType != null)
        PotionHelper.addMix(standardPotionType, Items.REDSTONE, longPotionType);    
        if (strongPotionType != null)
        PotionHelper.addMix(standardPotionType, Items.GLOWSTONE_DUST, strongPotionType);
    }
 
    /**
     * Добавление своего рецепта.
     *
     * @param basePotionType
     * @param ingredient
     * @param outputPotiontype
     */
    private static void addRecipe(PotionType inputPotionType, Item ingredient, PotionType outputPotiontype) {
     
        PotionHelper.addMix(inputPotionType, ingredient, outputPotiontype);
    }


Java:
@Mod.EventBusSubscriber(modid = PotionsMain.MODID)
public class PotionsRegistry {

    /** Эффекты */
    public static final Potion
    EQUILIBRIUM = new PotionEquilibrium("equilibrium", false, 0x77FFA9);//Периодический эффект (обмен опыта на здоровье).
     
    /** "Типы", представляющие зелья как предметы */
    public static final PotionType  
    EQUILIBRIUM_TYPE_STANDARD = createPotionType(null, new PotionEffect(EQUILIBRIUM, 600)),
    EQUILIBRIUM_TYPE_LONG = createPotionType("long", new PotionEffect(EQUILIBRIUM, 900)),
    EQUILIBRIUM_TYPE_STRONG = createPotionType("strong", new PotionEffect(EQUILIBRIUM, 600, 1));
     
    public static void registerSimpleRecipes() {
             
        addStandardRecipes(EQUILIBRIUM_TYPE_STANDARD, EQUILIBRIUM_TYPE_LONG, EQUILIBRIUM_TYPE_STRONG, Items.EXPERIENCE_BOTTLE);    
    }

    /**
     * Возвращает новый экземпляр PotionType с именем эффекта.
     *
     * @param namePrefix
     * @param potionEffect
     *
     * @return PotionType
     */
    private static PotionType createPotionType(String namePrefix, PotionEffect potionEffect) {    
         
        ResourceLocation potionName = potionEffect.getPotion().getRegistryName();

        ResourceLocation potionTypeName;
     
        if (namePrefix != null)    
            potionTypeName = new ResourceLocation(potionName.getResourceDomain(), namePrefix + "_" + potionName.getResourcePath());                
        else    
            potionTypeName = potionName;

        return new PotionType(potionName.toString(), potionEffect).setRegistryName(potionTypeName);
    }
 
    /**
     * Возвращает новый экземпляр PotionType с указанным именем. Используется для зелий с несколькими эффектами.
     *
     * @param typeName
     * @param namePrefix
     * @param potionEffects
     *
     * @return PotionType
     */
    private static PotionType createCompositePotionType(String typeName, String namePrefix, PotionEffect... potionEffects) {    
     
        ResourceLocation potionTypeName;
     
        if (namePrefix != null)
            potionTypeName = new ResourceLocation(PotionsMain.MODID, namePrefix + "_" + typeName);                
        else    
            potionTypeName = new ResourceLocation(PotionsMain.MODID, typeName);

        return new PotionType(typeName, potionEffects).setRegistryName(potionTypeName);
    }
 
    /**
     * Создание стандартных рецептов для зелий.
     *
     * @param standardPotionType
     * @param longPotionType
     * @param strongPotionType
     * @param ingredient
     */
    private static void addStandardRecipes(PotionType standardPotionType, PotionType longPotionType, PotionType strongPotionType, Item ingredient) {
     
        PotionHelper.addMix(PotionTypes.AWKWARD, ingredient, standardPotionType);//Аргументы: основа, ингредиент, результат.
        if (longPotionType != null)
        PotionHelper.addMix(standardPotionType, Items.REDSTONE, longPotionType);    
        if (strongPotionType != null)
        PotionHelper.addMix(standardPotionType, Items.GLOWSTONE_DUST, strongPotionType);
    }
 
    /**
     * Добавлние своего рецепта.
     *
     * @param basePotionType
     * @param ingredient
     * @param outputPotiontype
     */
    private static void addRecipe(PotionType inputPotionType, Item ingredient, PotionType outputPotiontype) {
     
        PotionHelper.addMix(inputPotionType, ingredient, outputPotiontype);
    }
 
    @SubscribeEvent
    public static void registerPotions(RegistryEvent.Register<Potion> event) {
             
        event.getRegistry().registerAll(
         
                EQUILIBRIUM
        );
    }
 
    @SubscribeEvent
    public static void registerPotionTypes(RegistryEvent.Register<PotionType> event) {
     
        event.getRegistry().registerAll(
                         
                EQUILIBRIUM_TYPE_STANDARD,
                EQUILIBRIUM_TYPE_LONG,
                EQUILIBRIUM_TYPE_STRONG
        );
    }
}

Java:
public class CommonProxy {

    public void preInit(FMLPreInitializationEvent event) {}

    public void init(FMLInitializationEvent event) {
     
        PotionsRegistry.registerSimpleRecipes();    
    }
}

Зелье можно будет сварить из основы в виде неловкого зелья и склянки с опытом, продлить редстоуном или усилить светопылью - как в ванили. Регистрируются рецепты в CommonProxy в процессе инициализации.

Дополнительно: зелья в качестве компонента при крафте

Вы можете добавить для варочной стойки рецепт, ингредиентом в котором будет зелье. В исходниках есть рабочий пример, но тут приведу метод регистрации такого рецепта и класс рецепта:
Java:
        //Продвинутый рецепт, определённый в собственном классе RecipeRetreatPotion. Используется для создания сложных рецептов, требующих проверки NBT.
        BrewingRecipeRegistry.addRecipe(new RecipeRetreatPotion(PotionUtils.addPotionToItemStack(new ItemStack(Items.POTIONITEM, 1), VANISH_TYPE_STANDARD), PotionUtils.addPotionToItemStack(new ItemStack(Items.POTIONITEM, 1), IRON_SKIN_TYPE), PotionUtils.addPotionToItemStack(new ItemStack(Items.POTIONITEM, 1), RETREAT_TYPE)));


Java:
public class RecipeRetreatPotion implements IBrewingRecipe {

    private final ItemStack inputStack, ingredientStack, outputStack;
 
    public RecipeRetreatPotion(ItemStack inputStack, ItemStack ingredientStack, ItemStack outputStack) {
     
        this.inputStack = inputStack;
        this.ingredientStack = ingredientStack;
        this.outputStack = outputStack;
    }
 
    @Override
    public boolean isInput(ItemStack inputStack) {

        boolean isValid = false;
     
        if (inputStack.getItem() == this.inputStack.getItem() && inputStack.hasTagCompound()) {
         
            isValid = PotionUtils.getEffectsFromStack(inputStack).equals(PotionUtils.getEffectsFromStack(this.inputStack));
        }
     
        return isValid;
    }

    @Override
    public boolean isIngredient(ItemStack ingredientStack) {
     
        boolean isValid = false;
     
        if (ingredientStack.getItem() == this.ingredientStack.getItem() && ingredientStack.hasTagCompound()) {
         
            isValid = PotionUtils.getEffectsFromStack(ingredientStack).equals(PotionUtils.getEffectsFromStack(this.ingredientStack));
        }
     
        return isValid;
    }

    @Override
    public ItemStack getOutput(ItemStack inputStack, ItemStack ingredientStack) {
     
        ItemStack output = ItemStack.EMPTY;
     
        if (this.isInput(inputStack) && this.isIngredient(ingredientStack)) {
         
            output = this.outputStack.copy();
        }
     
        return output;
    }
}

Для зелий других типов используйте Items#SPLASH_POTION и Items#LINGERING_POTION вместо Items#POTIONITEM.

Если хотите использовать зелье в крафтах на верстаке, то рецепт может выглядеть примерно так (рабочий в исходниках):
Java:
        GameRegistry.addShapedRecipe(new ResourceLocation("Utils"), new ResourceLocation("Recipes"), new ItemStack(Items.EXPERIENCE_BOTTLE), new Object[] {"   ", " S ", "   ", 'S', PotionUtils.addPotionToItemStack(new ItemStack(Items.POTIONITEM, 1), PotionsRegistry.EQUILIBRIUM_TYPE_STANDARD)});


Поместив простое зелье из статьи в центр сетки крафта вы получите склянку с опытом.

Всё, на этом туториал завершён. Оставляйте свои комментарии и советы в обсуждении. Спасибо за внимание.
Автор
AustereTony
Просмотры
1,988
Первый выпуск
Обновление
Оценка
5.00 звёзд 3 оценок

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

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

Разобраны все типы зелий. Очень полезно, спасибо за старание.
Материал подан разборчиво, всё структурировано, молодец!
Сверху