2025年2月5日水曜日

forge Mod を最新版にマイグレーションする方法

forge Mod を最新版にマイグレーションする方法

概要

マインクラフトのバージョンを 1.20.6 から 1.21.4 に上げ forge Mod のバージョンを 50.1.0 から 54.0.18 にマイグレーションしてみたのでその方法を紹介します

環境

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

最新版の MDK のダウンロード

執筆時点での最新版は https://files.minecraftforge.net/net/minecraftforge/forge/index_1.21.4.html ここにあるのでダウンロードします
デフォルトだと安定版をダウンロードすることになっているので最新版をダウンロードしたい場合にはバージョンを指定しましょう

バックアップ

MDK の作業ディレクトリをバックアップしておきましょう
git なりで管理していればすぐに戻せるのでおすすめです
git など SCM で管理していない場合はディレクトリごとコピーしましょう

src ディレクトリの移動

基本的には自分で実装したファイルを最新の MDK に展開するだけです

  • cp -ipr src /tmp
  • cp README.txt /tmp

など自分で編集したファイルをどこかに移動しましょう
gradle.properties や build.gradle なども編集している場合は移動しておきましょう

一旦すべて空にする

MDK の作業ディレクトリを一旦すべて空にしましょう

  • rm -rf *

新しい MDK を展開する

ダウンロードした zip ファイルを展開します

  • mv ~/Downloads/forge-1.21.4-54.0.18-mdk.zip .

展開できたら移動していたファイルを戻します

  • cp -ipr /tmp/src .
  • cp /tmp/README.txt .

差分を確認する

git diff などで差分を確認します

  • 元のやつに戻したい場合は git checkout や手動で追記します
  • BOM 文字が入っているファイルがあるので削除します (vim で set nobomb)
  • 改行コードが CRLF になっているのを LF に修正します (nkf -Lu --overwrite build.gradle)

gradle.properties にはバージョン情報が記載されているので必ず差分がでます

ちなみに今回の 50.1.0 から 54.0.18 のマイグレーションでは「changelog.txt」と「gradle.properties」のみ差分が出たので最悪手動でも何とかなったかもしれません

  • git diff gradle.properties
diff --git a/gradle.properties b/gradle.properties
index 6c23fa8..810b037 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,13 +7,13 @@ org.gradle.daemon=false
 ## Environment Properties
 
 # The Minecraft version must agree with the Forge version to get a valid artifact
-minecraft_version=1.20.6
+minecraft_version=1.21.4
 # The Minecraft version range can use any release version of Minecraft as bounds.
 # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
 # as they do not follow standard versioning conventions.
-minecraft_version_range=[1.20.6,1.21)
+minecraft_version_range=[1.21.4,1.22)
 # The Forge version must agree with the Minecraft version to get a valid artifact
-forge_version=50.1.0
+forge_version=54.0.18
 # The Forge version range can use any version of Forge as bounds or match the loader version range
 forge_version_range=[0,)
 # The loader version range can only use the major version of Forge/FML as bounds
@@ -35,7 +35,7 @@ loader_version_range=[0,)
 mapping_channel=official
 # The mapping version to query from the mapping channel.
 # This must match the format required by the mapping channel.
-mapping_version=1.20.6
+mapping_version=1.21.4

動作確認

再度ビルドして問題なく動作すれば OK です

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

コード修正

非互換になったメソッドなどがある場合はコードも修正する必要があります
自分の場合は以下の修正が必要だったので紹介します
基本的には IDE や lsp-java を使っていればエディタ側で警告してくれるのでそれに沿って手動でコードを修正する感じです

src/main/java/com/example/examplemod/Config.java

これは自分は編集してなかったので最新版をそのまま持ってきました

src/main/java/com/example/examplemod/ExampleMod.java

ビルドのエラーと最新版のファイルと古いファイルを見比べつつエラーを解消させるしかありません (このあたりもっと簡単にできないだろうか)

  • public ExampleMod() が引数付きになっていたので修正 public ExampleMod(FMLJavaModLoadingContext context)
    • modEvnetBus の取得方法も変わっていたので修正 IEventBus modEventBus = context.getModEventBus();
    • ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC); -> context.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
  • ResourceLocation がプライベートメソッドになって使えなくなっていた
    • new ResourceLocation("examplemod", "laser_fire") -> ResourceLocation.fromNamespaceAndPath("examplemod", "laser_fire")
  • java.lang.NullPointerException: Block id not set
    • ブロックを定義する際に .setId(BLOCKS.key("example_block")) という感じでブロックIDを定義するようになっている
    • ブロックやアイテムを登録するときに設定するプロパティの挙動はカスタムクラス側ではなく Mod メイン側全部やるようにしたほうがいいかもしれない

src/main/java/com/example/examplemod/LaserGunItem.java

  • net.minecraft.world.InteractionResultHolder の廃止
    • use などのメソッドがだいぶ変わっている
    • InteractionResult.sidedSuccess などもなく InteractionResult.SUCCESS を直接 return するように修正
  • The method hurt(DamageSource, float) from the type Entity is deprecated
    • 以下のように修正
livingEntity.hurtServer(
        (ServerLevel) world,
        livingEntity.damageSources().playerAttack(player),
        amountOfdamage);

テクスチャが当たらない

models がうまく当たらなかったです
lang や sound、blockstates は問題なかったです

トラブルシューティング

今回は特にエラーなくマイグレーションできましたがエラーが起きるケースとして以下があるので注意しましょう

  • Java のバージョンのアップグレード
    • Java21 でもともと開発していたが MDK をアップグレードしたら Java23 が必要になった
    • その場合はビルド時にコケるので MDK のドキュメントと照らし合わせて Java もアップグレードする

キャッシュの削除

  • rm -rf ~/.gradle/caches && rm -rf build/*

最後に

かなり面倒な印象です
しかも forge Mod はかなりのペースで更新されているので更新されるたびにマイグレーションするのは大変かなと思います
メジャーバージョンが上がったりしたタイミングで上げるのがいいかなとは思います

もっと簡単にマイグレーションできる方法はないのでしょうか
もしくは NeoForge に移行するべきなのでしょうか

参考サイト

2025年2月4日火曜日

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 = 100.0F; // ダメージ量
    private String countKey = "laserGunKills"; // カウントのキー
    private int previousCount = 0;
    private boolean isLocalSent = false;

    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 {
            // サーバー側でダメージ処理
            fireLaser(world, player);
            // 討伐数の取得
            CompoundTag tag = player.getPersistentData(); // プレイヤーのデータを取得
            int currentCount = tag.getInt(countKey);
            // 前の値と比較して進んでいたら判定に進む、previousCountが0の場合は起動後まだ一度も倒していない状態なので判定しない
            if (previousCount != 0 && previousCount < currentCount) {
                // クライアント用の変数にも登録
                if (currentCount % 10 == 0) {
                    sendChatMessage(player, currentCount);
                    spawnReward(player, world);
                    // ローカルの通知も許可する、このロジックだと一発分遅れる
                    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 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);
    }

    // 報酬を降らせる処理
    private void spawnReward(Player player, Level level) {
        // プレイヤーの位置
        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);
        }
    }
}

ポイント

level.addFreshEntity で指定の座標にアイテムを発生させることができます

// 報酬を降らせる処理
private void spawnReward(Player player, Level level) {
    // プレイヤーの位置
    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);
    }
}

またアイテムの生成はサーバ側で行います

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

最後に

クライアント側とサーバ側で行う処理を混同させないようにしましょう
基本的にはクライアント側はそのユーザのみでサーバ側は全ユーザに対して行う処理を記載します

現状はカスタムアイテム側でダメージ判定や討伐数管理をしていますがワールド側のイベントハンドリングで敵が onLivingDeath などで敵が倒されたときの武器を判定して特定の武器であれば討伐数をカウントするなどの実装方法もあります
そのあたりの実装方法は経験に左右されるところなのでなかなか難しいかなと思います

参考サイト

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