Skip to content

Example: Crop Calendar

Contributed by Mushymato, edited by focustense

  • Bindings
  • Events
  • Repeaters
  • Grid
  • Scrolling
  • Animation

Displays harvest timing and an animation of the growth phases for any given crop, selectable from a grid.

internal partial class CropsGridViewModel : INotifyPropertyChanged
{
    public IReadOnlyList<CropInfoboxModel> AllCrops { get; }

    [Notify]
    private CropInfoboxModel selectedCrop;

    public CropsGridViewModel()
    {
        AllCrops = Game1
            .objectData.Select(
                (kv) =>
                    Game1.cropData.TryGetValue(kv.Key, out CropData? crop)
                        ? new CropInfoboxModel(ItemRegistry.GetData(kv.Key), crop)
                        : null
            )
            .Where(cropInfo => cropInfo is not null)
            .ToList()!;
        selectedCrop = AllCrops[0];
    }

    public void SelectCrop(CropInfoboxModel cropInfo)
    {
        cropInfo.ResetPhase();
        SelectedCrop = cropInfo;
    }
}
internal partial record CropInfoboxModel(ParsedItemData Seed, CropData Crop)
    : INotifyPropertyChanged
{
    public string Name => Produce.DisplayName;
    public IReadOnlyList<bool> Harvest { get; } = GetHarvest(Crop).ToList();
    public ParsedItemData Produce { get; } =
        ItemRegistry.GetDataOrErrorItem(Crop.HarvestItemId);

    private readonly Texture2D cropTexture =
        Game1.content.Load<Texture2D>(Crop.Texture);

    private int phaseIndex = 0;

    [Notify]
    private Tuple<Texture2D, Rectangle> phase = null!;

    internal void NextPhase()
    {
        Phase = new Tuple<Texture2D, Rectangle>(
            cropTexture,
            new Rectangle(
                // odd number sprite index = right side
                Crop.SpriteIndex
                    % 2
                    * 128
                    + phaseIndex * 16,
                // zigzag
                Crop.SpriteIndex
                    / 2
                    * 32,
                16,
                32
            )
        );

        if (phaseIndex < 2)
            phaseIndex = 2;
        else if (phaseIndex == Crop.DaysInPhase.Count + 1)
            phaseIndex = Random.Shared.Next(0, 2);
        else
            phaseIndex++;
    }

    internal void ResetPhase()
    {
        phaseIndex = Random.Shared.Next(0, 2);
        NextPhase();
    }

    private static IEnumerable<bool> GetHarvest(CropData crop)
    {
        int growDays = crop.DaysInPhase.Sum();
        int regrowDays = crop.RegrowDays;
        if (regrowDays < 1)
        {
            yield return false;
            for (int day = 1; day < WorldDate.DaysPerMonth; day++)
                yield return day % growDays == 0;
        }
        else
        {
            yield return false;
            for (int day = 1; day < growDays; day++)
                yield return false;
            yield return true;
            for (int day = 1; day < WorldDate.DaysPerMonth - growDays; day++)
                yield return day % regrowDays == 0;
        }
    }
}
<lane layout="content 700px"
      orientation="horizontal"
      horizontal-content-alignment="start">
    <frame layout="400px stretch"
           background={@Mods/StardewUI/Sprites/ControlBorder}
           padding="32, 20"
           horizontal-content-alignment="middle">
        <lane *context={SelectedCrop}
              orientation="vertical"
              horizontal-content-alignment="middle">
            <banner text={Name} />
            <lane orientation="horizontal" >
                <image sprite={Seed} layout="64px 64px" margin="4"/>
                <image sprite={Produce} layout="64px 64px" margin="4"/>
            </lane>
            <grid layout="400px content"
                  item-layout="count: 7"
                  item-spacing="8,8"
                  horizontal-item-alignment="middle">
                <frame *repeat={Harvest}
                       layout="64px 64px"
                       background={@Mods/StardewUI/Sprites/ButtonLight}>
                    <image *if={this}
                           layout="48px 48px"
                           sprite={^Produce}
                           margin="8"/>
                </frame>
            </grid>
            <image sprite={Phase} layout="64px 128px" margin="0,16,0,0"/>
        </lane>
    </frame>
    <frame layout="600px stretch"
           background={@Mods/StardewUI/Sprites/ControlBorder}
           padding="20">
        <scrollable>
            <grid layout="content content"
                  item-layout="count: 6"
                  item-spacing="8, 8"
                  horizontal-item-alignment="middle">
                <image *repeat={AllCrops}
                       layout="64px 64px"
                       sprite={Seed}
                       tooltip={Name}
                       focusable="true"
                       click=|^SelectCrop(this)| />
            </grid>
        </scrollable>
    </frame>
</lane>