ShapedRecipe.java

package it.fulminazzo.yagl.items.recipes;

import it.fulminazzo.yagl.items.Item;
import it.fulminazzo.yagl.wrappers.Range;
import it.fulminazzo.yagl.wrappers.Wrapper;
import it.fulminazzo.fulmicollection.objects.FieldEquable;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.LinkedList;
import java.util.List;

/**
 * An implementation of {@link Recipe} to express the shaped recipes in Minecraft.
 * An example of shaped recipe is the TNT, which requires all the items in an exact position.
 */
public class ShapedRecipe extends RecipeImpl {
    private final @NotNull List<Item> ingredients;
    @Getter
    private final @NotNull Shape shape;

    private ShapedRecipe() {
        this("pending");
    }

    /**
     * Instantiates a new Shaped recipe.
     *
     * @param id the id
     */
    public ShapedRecipe(final @NotNull String id) {
        super(id);
        this.shape = new Shape();
        this.ingredients = new LinkedList<>();
    }

    @Override
    public @NotNull ShapedRecipe setOutput(final @NotNull Item output) {
        this.output = output.copy(Item.class);
        return this;
    }

    /**
     * Sets shape in the crafting table.
     * A 3x3 shape means all the crafting, a 1x1 means only the first slot.
     *
     * @param rows    the rows
     * @param columns the columns
     * @return this recipe
     */
    public @NotNull ShapedRecipe setShape(final int rows, final int columns) {
        this.shape.setRows(rows);
        this.shape.setColumns(columns);

        int size;
        while ((size = this.ingredients.size()) > rows * columns)
            this.ingredients.remove(size - 1);

        return this;
    }

    /**
     * Sets all the given ingredients using {@link #setIngredient(int, Item)},
     * where the first number is the index of the item.
     * If the array is too big, an {@link IllegalArgumentException} will be thrown
     * (but the ingredients will still be put in place).
     *
     * @param items the items
     * @return this recipe
     */
    public @NotNull ShapedRecipe setIngredients(final Item @NotNull ... items) {
        for (int i = 0; i < items.length; i++) setIngredient(i, items[i]);
        return this;
    }

    /**
     * Sets ingredient.
     *
     * @param position the position
     * @param item     the item
     * @return this recipe
     */
    public @NotNull ShapedRecipe setIngredient(final int position, final @Nullable Item item) {
        if (!this.shape.contains(position))
            throw new IllegalArgumentException(String.format("Shape %sx%s does not allow position %s",
                    this.shape.getRows(), this.shape.getColumns(), position));
        while (this.ingredients.size() - 1 < position) this.ingredients.add(null);
        this.ingredients.set(position, item == null ? null : item.copy(Item.class));
        return this;
    }

    @Override
    public @NotNull List<Item> getIngredients() {
        return new LinkedList<>(this.ingredients);
    }

    /**
     * The type Shape.
     */
    @Getter
    public static class Shape extends FieldEquable {
        static final int MIN_COLUMNS = 1;
        static final int MAX_COLUMNS = 3;
        static final int MIN_ROWS = 1;
        static final int MAX_ROWS = 3;
        @Range(min = MIN_ROWS, max = MAX_ROWS)
        private int rows;
        @Range(min = MIN_COLUMNS, max = MAX_COLUMNS)
        private int columns;

        /**
         * Instantiates a new Shape.
         */
        public Shape() {
            this(1, 1);
        }

        /**
         * Instantiates a new Shape.
         *
         * @param rows    the rows
         * @param columns the columns
         */
        public Shape(final int rows, final int columns) {
            setRows(rows);
            setColumns(columns);
        }

        /**
         * Sets the columns.
         * Should be between {@link #MIN_COLUMNS} and {@link #MAX_COLUMNS}.
         *
         * @param columns the columns
         */
        public void setColumns(final int columns) {
            this.columns = Wrapper.check(this, columns);
        }

        /**
         * Sets the rows.
         * Should be between {@link #MIN_ROWS} and {@link #MAX_ROWS}.
         *
         * @param rows the rows
         */
        public void setRows(final int rows) {
            this.rows = Wrapper.check(this, rows);
        }

        /**
         * Checks if the given number is in a valid range between 0 and {@link #rows} * {@link #columns}.
         *
         * @param num the num
         * @return true if contains
         */
        public boolean contains(int num) {
            return num >= 0 && num < this.rows * this.columns;
        }

        @Override
        public @NotNull String toString() {
            return String.format("Shape {rows: %s, columns: %s}", this.rows, this.columns);
        }
    }
}