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

0 件のコメント:

コメントを投稿