2025年2月3日月曜日

forge Mod で作成した武器のダメージを可変にする方法

forge Mod で作成した武器のダメージを可変にする方法

概要

これまでは固定でしたが可変にしてみました
例えば討伐した敵の数に応じてダメージ量が変わる方法を紹介します

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

サンプルコード

package com.example.examplemod;

import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

public class LaserGunItem extends Item {
    private float amountOfdamage = 1.0F; // 初期ダメージ量
    private float maxAmountOfdamage = 100.0F; // 最大ダメージ量
    private String countKey = "laserGunKills"; // 討伐数をCompoundTagで管理するためのカウントのキー
    private int previousCount = 0; // 前回の討伐数
    private boolean isLocalSent = false;
    private int levelUpCount = 10; // レベルアップする討伐数

    public LaserGunItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(
            Level world, Player player, InteractionHand hand) {
        // クライアント側のみでエフェクトを表示
        if (world.isClientSide()) {
            // ビームエフェクトを生成
            spawnLaserBeam(player);
            if (isLocalSent) {
                sendToastMessage(player, previousCount);
                isLocalSent = false;
            }
        } else {
            // 討伐数の取得
            int currentCount = getCurrentCount(player);
            // ダメージ量の設定
            setupUpWeaponDamage(currentCount);
            // サーバー側でダメージ処理
            fireLaser(world, player);
            // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
            if (previousCount != 0 && previousCount < currentCount) {
                // クライアント用の変数にも登録
                if (currentCount % levelUpCount == 0) {
                    sendChatMessage(player, currentCount);
                    spawnReward(world, player);
                    // ローカルの通知も許可する、このロジックだと一発分遅れる
                    isLocalSent = true;
                }
            }
            // 一つ前の値を保存
            previousCount = currentCount;
        }
        return InteractionResultHolder.sidedSuccess(
                player.getItemInHand(hand), world.isClientSide());
    }

    @Override
    public void appendHoverText(
            ItemStack stack,
            Item.TooltipContext context,
            List<Component> tooltip,
            TooltipFlag flag) {
        super.appendHoverText(stack, context, tooltip, flag);
        // ダメージ情報を追加
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.damage", amountOfdamage)
                        .withStyle(ChatFormatting.GREEN));

        // 他の情報も追加可能
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.description")
                        .withStyle(ChatFormatting.GRAY));
    }

    @Override
    public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
        // 特殊な処理が必要であればここで行う(今回は不要なのでデフォルト動作に任せます)
        // アイテムが q で捨てられたときにアイテムとして残すための設定
        return super.onEntityItemUpdate(stack, entity);
    }

    private int getCurrentCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey);
        return currentCount;
    }

    private void spawnLaserBeam(Player player) {
        // クライアント側エフェクト(例:パーティクル)
        player.level()
                .addParticle(
                        ParticleTypes.END_ROD,
                        player.getX(),
                        player.getEyeY(),
                        player.getZ(),
                        player.getLookAngle().x,
                        player.getLookAngle().y,
                        player.getLookAngle().z);
    }

    private void playWeaponSound(Level world, Player player) {
        if (world instanceof ServerLevel serverLevel) {
            serverLevel.playSound(
                    null, // プレイヤー(nullにすると全員が聞こえる)
                    player.blockPosition(), // 再生位置
                    ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
                    SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
                    1.0f, // 音量
                    1.0f // 音の高さ(ピッチ)
                    );
        }
    }

    private void fireLaser(Level world, Player player) {
        // 音の再生
        playWeaponSound(world, player);
        // 発射用の座標と距離の設定
        Vec3 start = player.getEyePosition(1.0F);
        Vec3 direction = player.getLookAngle();
        double range = 50.0D; // 50ブロック先をターゲット
        Vec3 end = start.add(direction.scale(range));

        // ブロックとのヒット判定
        HitResult blockHit =
                world.clip(
                        new ClipContext(
                                start,
                                end,
                                ClipContext.Block.OUTLINE,
                                ClipContext.Fluid.NONE,
                                player));
        // エンティティとのヒット判定
        EntityHitResult entityHit =
                ProjectileUtil.getEntityHitResult(
                        world,
                        player,
                        start,
                        end,
                        player.getBoundingBox().expandTowards(direction.scale(range)).inflate(1.0D),
                        entity -> entity instanceof LivingEntity && entity != player);
        // 優先的にエンティティを処理
        if (entityHit != null) {
            System.out.println("Hit entity!");
            Entity hitEntity = entityHit.getEntity();
            if (hitEntity instanceof LivingEntity livingEntity) {
                // 敵にダメージを与える
                livingEntity.hurt(
                        livingEntity.damageSources().playerAttack(player), amountOfdamage);
                // 倒したか確認
                if (livingEntity.isDeadOrDying()) {
                    // カウントを増やす
                    incrementKillCount(player);
                }
            }
        } else if (blockHit != null && blockHit.getType() == HitResult.Type.BLOCK) {
            // ブロックにヒットした場合
            System.out.println("Hit block at: " + ((BlockHitResult) blockHit).getBlockPos());
            BlockHitResult blockResult = (BlockHitResult) blockHit;
            BlockPos hitPos = blockResult.getBlockPos();
            // ヒット位置に火をつける例
            world.setBlockAndUpdate(hitPos, Blocks.FIRE.defaultBlockState());
        } else {
            // 何にも当たらなかった場合
            System.out.println("Missed!");
        }
    }

    private void incrementKillCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey); // 現在のカウント
        tag.putInt(countKey, currentCount + 1); // カウントを増加
    }

    private void sendChatMessage(Player player, int currentCount) {
        player.sendSystemMessage(
                Component.translatable(
                                "The player %s killed %d mobs with laser gun."
                                        .formatted(player.getName().getString(), currentCount))
                        .withStyle(ChatFormatting.GREEN));
    }

    private void sendToastMessage(Player player, int currentCount) {
        String title = "Good job!";
        String description = "Killed %d mobs with laser gun.".formatted(currentCount);
        Minecraft minecraft = Minecraft.getInstance();
        ToastComponent toastComponent = minecraft.getToasts();
        // トーストを作成
        SystemToast toast =
                new SystemToast(
                        SystemToast.SystemToastId.PERIODIC_NOTIFICATION, // トーストのタイプ(任意の値でOK)
                        Component.literal(title), // タイトル
                        Component.literal(description) // 説明
                        );
        // トーストを表示
        toastComponent.addToast(toast);
    }

    // 報酬を降らせる処理
    private void spawnReward(Level level, Player player) {
        // プレイヤーの位置
        double x = player.getX();
        double y = player.getY() + 20; // プレイヤーの上空20ブロック
        double z = player.getZ();
        for (int i = 0; i < 10; ++i) {
            // 報酬アイテム(例: ダイヤモンド)
            ItemEntity reward = new ItemEntity(level, x, y, z, Items.DIAMOND.getDefaultInstance());
            level.addFreshEntity(reward);
        }
    }

    // 武器の強さを更新する
    private void setupUpWeaponDamage(int currentCount) {
        // 討伐数が2以下の場合は初期のダメージ量にする
        if (currentCount <= 2) {
            return;
        }
        // ダメージ量は討伐数/2とする
        float newAmountOfdamage = currentCount / 2;
        // 最大を超えているかチェック
        if (newAmountOfdamage >= maxAmountOfdamage) {
            amountOfdamage = maxAmountOfdamage;
        } else {
            // ダメージ量の更新
            amountOfdamage = newAmountOfdamage;
        }
    }
}

ポイント

まず武器の強さを設定するメソッドを準備します
今回は「総討伐数」を元にダメージ量を計算するので必要な引数は討伐数のみになります
討伐数 (currentCount) は CompoundTag で管理されている値なのでサーバ側の処理で取り出す必要があります

// 武器の強さを更新する
private void setupUpWeaponDamage(int currentCount) {
    // ダメージ量は討伐数/2とする
    float newAmountOfdamage = currentCount / 2;
    // 最大を超えているかチェック
    if (newAmountOfdamage >= maxAmountOfdamage) {
        amountOfdamage = maxAmountOfdamage;
    } else {
        // ダメージ量の更新
        amountOfdamage = newAmountOfdamage;
    }
}

CompoundTag を扱うので setupWeaponDamage は InteractionResultHolder のサーバ側の処理で行う必要があります
もしかすると tick などの常に呼ばれるハンドラを使えばそこでサーバ側の値を取得できるかもしれませんが今回は既存の処理をそのまま使っています
本当はコンストラクタでやりたいのですがコンストラクタでは Level を扱えません
なので現状の処理だと起動した直後はダメージ量が 0 になっており一度武器でアクションをしないとダメージ量が初期化されないので注意してください (このあたりは改良の余地あり、Mod のメイン側で計算してもいいのかも)

@Override
public InteractionResultHolder<ItemStack> use(
        Level world, Player player, InteractionHand hand) {
    // クライアント側のみでエフェクトを表示
    if (world.isClientSide()) {
        // ビームエフェクトを生成
        spawnLaserBeam(player);
        if (isLocalSent) {
            sendToastMessage(player, previousCount);
            isLocalSent = false;
        }
    } else {
        // 討伐数の取得
        int currentCount = getCurrentCount(player);
        // ダメージ量の設定
        setupUpWeaponDamage(currentCount);
        // サーバー側でダメージ処理
        fireLaser(world, player);
        // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
        if (previousCount != 0 && previousCount < currentCount) {
            // クライアント用の変数にも登録
            if (currentCount % levelUpCount == 0) {
                sendChatMessage(player, currentCount);
                spawnReward(world, player);
                // ローカルの通知も許可する、このロジックだと一発分遅れる
                isLocalSent = true;
            }
        }
        // 一つ前の値を保存
        previousCount = currentCount;
    }
    return InteractionResultHolder.sidedSuccess(
            player.getItemInHand(hand), world.isClientSide());
}

最後に

forge Mod で作成した武器のダメージを可変にしてみました
基本的には変数にするだけですがゲームを再起動したあともダメージ量を引き継ぎたい場合はデータを永続化するような仕組みが必要になります

2025年2月2日日曜日

forge Mod でチャット欄を使ったカスタムコマンドを実装する方法

forge Mod でチャット欄を使ったカスタムコマンドを実装する方法

概要

forge Mod でカスタムコマンドを作成する方法を紹介します

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

コマンドを管理するクラス

特に forge 側の機能を使わずマイクラですでに用意されているクラスを使います
コマンドを登録するためのメソッドと実際にコマンドが行う処理を実装します
今回はサーバ側に保存された CompoundTag の情報をクリアする処理にしています

  • vim src/main/java/com/example/examplemod/ResetKillCommand.java
package com.example.examplemod;

import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;

public class ResetKillCommand {
    private static String countKey = "laserGunKills"; // 討伐数をCompoundTagで管理するためのカウントのキー

    // コマンドを登録するメソッド
    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
        dispatcher.register(
                Commands.literal("resetkills") // コマンド名
                        .requires(source -> source.hasPermission(1)) // 実行に必要な権限レベル (1 = プレイヤー権限)
                        .executes(ResetKillCommand::resetKills) // 実行する処理
                );
    }

    // 実行処理
    private static int resetKills(CommandContext<CommandSourceStack> context) {
        CommandSourceStack source = context.getSource();
        // 実行者がプレイヤーかどうか確認
        if (source.getEntity() instanceof Player player) {
            // プレイヤーのCompoundTagから討伐数をリセット
            player.getPersistentData().remove(countKey);
            // プレイヤーにメッセージを送信
            player.sendSystemMessage(Component.literal("Kill count has been reset."));
        } else {
            // コンソール実行時
            source.sendFailure(Component.literal("This command can only be executed by a player."));
        }
        return 1; // コマンドが成功した場合
    }
}

権限について

hasPermission で指定可能な権限は以下の通りです

説明
0 全プレイヤー: 権限に関係なく誰でも実行可能。
1 一般プレイヤー: コマンドブロックなどの基本操作が可能。
2 オペレーター (OP): サーバー管理者向けの基本コマンド権限。
3 高度なオペレーター: プレイヤー管理、サーバー設定変更など。
4 完全な管理者: サーバーの完全制御権限を持つ。

コマンドの登録

Mod のメイン側の onRegisterCommands イベントハンドラに今回追加したコマンドを登録するだけです

  • src/main/java/com/example/examplemod/ExampleMod.java
@SubscribeEvent
public void onRegisterCommands(RegisterCommandsEvent event) {
    // コマンドを登録
    ResetKillCommand.register(event.getDispatcher());
}

動作確認

  • ./gradlew clean && ./gradlew build && ./gradlew runData && ./gradlew runClient

実際にコマンドが使えることが確認できれば OK です

最後に

forge Mod でカスタムコマンドを作成してみました
サーバ側の裏側のデータやチートコマンドなどを実装するときに使います
ブロックやアイテム、ゲームイベントにするまでもないような処理をカスタムコマンドにしてしまうのもいいかなと思います

参考サイト

2025年2月1日土曜日

forge Mod で右上に出るトースト通知を実装する方法

forge Mod で右上に出るトースト通知を実装する方法

概要

前回はワールド全体に表示することができるチャットメッセージの送信方法を紹介しました
今回はクライアントごとに表示することができるトーストメッセージの送信方法を紹介します
ポイントはクライアントサイドでないと表示できない点です

環境

  • macOS 15.2
  • Java 21.0.5
  • forrge MDK 1.20.6-50.1.32
  • minecraft 1.20.6

サンプルコード

package com.example.examplemod;

import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

public class LaserGunItem extends Item {
    private float amountOfdamage = 100.0F; // ダメージ量
    private String countKey = "laserGunKills"; // カウントのキー
    private int localCurrentCount = 0;

    public LaserGunItem(Properties properties) {
        super(properties);
    }

    @Override
    public InteractionResultHolder<ItemStack> use(
            Level world, Player player, InteractionHand hand) {
        // クライアント側のみでエフェクトを表示
        if (world.isClientSide()) {
            // ビームエフェクトを生成
            spawnLaserBeam(player);
            if (localCurrentCount != 0 && localCurrentCount % 10 == 0) {
                sendToastMessage(player, localCurrentCount);
            }
        } else {
            // サーバー側でダメージ処理
            fireLaser(world, player);
            // 討伐数の取得
            CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
            int currentCount = tag.getInt(countKey);
            // クライアント用の変数にも登録
            localCurrentCount = currentCount;
            if (currentCount % 10 == 0) {
                sendChatMessage(player, currentCount);
            }
        }
        return InteractionResultHolder.sidedSuccess(
                player.getItemInHand(hand), world.isClientSide());
    }

    @Override
    public void appendHoverText(
            ItemStack stack,
            Item.TooltipContext context,
            List<Component> tooltip,
            TooltipFlag flag) {
        super.appendHoverText(stack, context, tooltip, flag);
        // ダメージ情報を追加
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.damage", amountOfdamage)
                        .withStyle(ChatFormatting.GREEN));

        // 他の情報も追加可能
        tooltip.add(
                Component.translatable("item.examplemod.laser_gun.description")
                        .withStyle(ChatFormatting.GRAY));
    }

    @Override
    public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
        // 特殊な処理が必要であればここで行う(今回は不要なのでデフォルト動作に任せます)
        // アイテムが q で捨てられたときにアイテムとして残すための設定
        return super.onEntityItemUpdate(stack, entity);
    }

    private void spawnLaserBeam(Player player) {
        // クライアント側エフェクト(例:パーティクル)
        player.level()
                .addParticle(
                        ParticleTypes.END_ROD,
                        player.getX(),
                        player.getEyeY(),
                        player.getZ(),
                        player.getLookAngle().x,
                        player.getLookAngle().y,
                        player.getLookAngle().z);
    }

    private void fireLaser(Level world, Player player) {
        // 音の再生
        if (world instanceof ServerLevel serverLevel) {
            serverLevel.playSound(
                    null, // プレイヤー(nullにすると全員が聞こえる)
                    player.blockPosition(), // 再生位置
                    ExampleMod.LASER_FIRE_SOUND.get(), // サウンドイベント
                    SoundSource.PLAYERS, // サウンドの種類(PLAYERS, BLOCKS, AMBIENT など)
                    1.0f, // 音量
                    1.0f // 音の高さ(ピッチ)
                    );
        }
        // 発射用の座標と距離の設定
        Vec3 start = player.getEyePosition(1.0F);
        Vec3 direction = player.getLookAngle();
        double range = 50.0D; // 50ブロック先をターゲット
        Vec3 end = start.add(direction.scale(range));

        // ブロックとのヒット判定
        HitResult blockHit =
                world.clip(
                        new ClipContext(
                                start,
                                end,
                                ClipContext.Block.OUTLINE,
                                ClipContext.Fluid.NONE,
                                player));
        // エンティティとのヒット判定
        EntityHitResult entityHit =
                ProjectileUtil.getEntityHitResult(
                        world,
                        player,
                        start,
                        end,
                        player.getBoundingBox().expandTowards(direction.scale(range)).inflate(1.0D),
                        entity -> entity instanceof LivingEntity && entity != player);
        // 優先的にエンティティを処理
        if (entityHit != null) {
            System.out.println("Hit entity!");
            Entity hitEntity = entityHit.getEntity();
            if (hitEntity instanceof LivingEntity livingEntity) {
                // 敵にダメージを与える
                livingEntity.hurt(
                        livingEntity.damageSources().playerAttack(player), amountOfdamage);
                // 倒したか確認
                if (livingEntity.isDeadOrDying()) {
                    // カウントを増やす
                    incrementKillCount(player);
                }
            }
        } else if (blockHit != null && blockHit.getType() == HitResult.Type.BLOCK) {
            // ブロックにヒットした場合
            System.out.println("Hit block at: " + ((BlockHitResult) blockHit).getBlockPos());
            BlockHitResult blockResult = (BlockHitResult) blockHit;
            BlockPos hitPos = blockResult.getBlockPos();
            // ヒット位置に火をつける例
            world.setBlockAndUpdate(hitPos, Blocks.FIRE.defaultBlockState());
        } else {
            // 何にも当たらなかった場合
            System.out.println("Missed!");
        }
    }

    private void incrementKillCount(Player player) {
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey); // 現在のカウント
        tag.putInt(countKey, currentCount + 1); // カウントを増加
    }

    private void sendChatMessage(Player player, int currentCount) {
        player.sendSystemMessage(
                Component.translatable(
                                "The player %s killed %d mobs with laser gun."
                                        .formatted(player.getName().getString(), currentCount))
                        .withStyle(ChatFormatting.GREEN));
    }

    private void sendToastMessage(Player player, int currentCount) {
        String title = "Good job!";
        String description = "Killed %d mobs with laser gun.".formatted(currentCount);
        Minecraft minecraft = Minecraft.getInstance();
        ToastComponent toastComponent = minecraft.getToasts();
        // トーストを作成
        SystemToast toast =
                new SystemToast(
                        SystemToast.SystemToastId.PERIODIC_NOTIFICATION, // トーストのタイプ(任意の値でOK)
                        Component.literal(title), // タイトル
                        Component.literal(description) // 説明
                        );
        // トーストを表示
        toastComponent.addToast(toast);
    }
}

ポイント

まずトースト通知を送信するメソッドを実装します
ToastComponent を取得し SystemToast でメッセージ情報を作成し通知します
通知用のタイトルや概要文は文字列で指定すれば OK です

そして最大のポイントはトースト通知はクライアント側でしか使えない点です
サーバ側の処理中にこのメソッドを呼び出しても通知されないので注意しましょう

private void sendToastMessage(Player player, int currentCount) {
    String title = "Good job!";
    String description = "Killed %d mobs with laser gun.".formatted(currentCount);
    Minecraft minecraft = Minecraft.getInstance();
    ToastComponent toastComponent = minecraft.getToasts();
    // トーストを作成
    SystemToast toast =
            new SystemToast(
                    SystemToast.SystemToastId.PERIODIC_NOTIFICATION, // トーストのタイプ(任意の値でOK)
                    Component.literal(title), // タイトル
                    Component.literal(description) // 説明
                    );
    // トーストを表示
    toastComponent.addToast(toast);
}

呼び出す際はクライアント側で呼び出す必要があります
今回はサーバ側で保持している討伐数に応じてトースト通知をするので isClientSide() でサーバ側かクライアント側かの処理を分岐している部分があるのでそこで sendToastMessage をクライアント側で呼び出しています

またこれもポイントですが討伐数を管理している CompoundTag はサーバ側の処理中でなければ取得できません
なのでサーバ側の処理で取得した値をローカルで管理している変数に代入しその値でトースト通知をするかどうかの判断をしています

InteractionResultHolder は基本的にサーバ側 -> クライアント側の順番で一度のアクションで二度呼ばれます
しかもサーバ側の処理が先なので以下の実装だと 10 体目の撃破時に先にチャット通知が行われ 11 体目を倒すタイミングでトースト通知が行われる点に注意してください

@Override
public InteractionResultHolder<ItemStack> use(
        Level world, Player player, InteractionHand hand) {
    // クライアント側のみでエフェクトを表示
    if (world.isClientSide()) {
        // ビームエフェクトを生成
        spawnLaserBeam(player);
        if (localCurrentCount != 0 && localCurrentCount % 10 == 0) {
            sendToastMessage(player, localCurrentCount);
        }
    } else {
        // サーバー側でダメージ処理
        fireLaser(world, player);
        // 討伐数の取得
        CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
        int currentCount = tag.getInt(countKey);
        // クライアント用の変数にも登録
        localCurrentCount = currentCount;
        if (currentCount % 10 == 0) {
            sendChatMessage(player, currentCount);
        }
    }
    return InteractionResultHolder.sidedSuccess(
            player.getItemInHand(hand), world.isClientSide());
}

最後に

forge Mod でトースト通知を実装してみました
通知自体は非常に簡単にできますがトースト通知が使える条件があるので注意しましょう
また過度な通知はゲームの負荷になるのでやめましょう

チャットでの通知はゲームをプレイしているプレイヤー全員に通知したい場合に使いトースト通知はユーザ個別に通知したい場合に使う感じかなと思います

参考サイト