diff --git a/PORT-CHANGELOG.md b/PORT-CHANGELOG.md index bca945d..3f5d22c 100644 --- a/PORT-CHANGELOG.md +++ b/PORT-CHANGELOG.md @@ -53,3 +53,12 @@ Dedicated-server runtime verification remains a later test step; this iteration - After a successful build, the script can commit and push the repository. - Git push retries automatically up to three times per round; after three failures the user can choose another three-attempt round or stop. - Added `.gitignore` for Gradle caches, build output, Loom runtime files, IDE metadata, and logs so Git integration does not stage generated files. + +## 0.9.0-alpha - Full-set armor effects + +- Attacker full set: `Better Armor` effect icon, +15 armor toughness, +0.25 knockback resistance. +- Scout full set: `Lightweight` effect icon, +0.025 movement speed. +- Defender full set: `Heavy Armor` effect icon, -0.02 movement speed, and final health damage reduced to 85%. +- Medical full set: `Self-Healing Device` ready effect; below 30% health triggers Regeneration IV for 5 seconds and a 30-second self-healing cooldown. +- Reused the original mod's mob-effect icons (`tough`, `heavy_armor`, `light_leg`, `rescue`, `rescue_cooldown`). +- Full-set logic runs server-side so it can later be tested on a dedicated Fabric server as well as integrated singleplayer. diff --git a/gradle.properties b/gradle.properties index 3e4c178..2d07254 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ loom_version=1.17-SNAPSHOT fabric_api_version=0.158.0+26.2 geckolib_version=5.5.4 -mod_version=0.8.0-alpha +mod_version=0.9.0-alpha maven_group=me.xjqsh archives_base_name=lrarmor diff --git a/src/main/java/me/xjqsh/lesraisinsarmor/LesRaisinsArmor.java b/src/main/java/me/xjqsh/lesraisinsarmor/LesRaisinsArmor.java index 0053bf8..a8caf1c 100644 --- a/src/main/java/me/xjqsh/lesraisinsarmor/LesRaisinsArmor.java +++ b/src/main/java/me/xjqsh/lesraisinsarmor/LesRaisinsArmor.java @@ -1,6 +1,8 @@ package me.xjqsh.lesraisinsarmor; import me.xjqsh.lesraisinsarmor.init.ModItems; +import me.xjqsh.lesraisinsarmor.effect.ModEffects; +import me.xjqsh.lesraisinsarmor.gameplay.ArmorSetEffects; import net.fabricmc.api.ModInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,7 +13,9 @@ public final class LesRaisinsArmor implements ModInitializer { @Override public void onInitialize() { + ModEffects.initialize(); ModItems.initialize(); + ArmorSetEffects.initialize(); LOGGER.info("LesRaisinsArmor Fabric 26.2 port initialized ({} armor pieces).", ModItems.ARMOR_ITEMS.size()); } } diff --git a/src/main/java/me/xjqsh/lesraisinsarmor/effect/ModEffects.java b/src/main/java/me/xjqsh/lesraisinsarmor/effect/ModEffects.java new file mode 100644 index 0000000..e59584a --- /dev/null +++ b/src/main/java/me/xjqsh/lesraisinsarmor/effect/ModEffects.java @@ -0,0 +1,53 @@ +package me.xjqsh.lesraisinsarmor.effect; + +import me.xjqsh.lesraisinsarmor.LesRaisinsArmor; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; + +public final class ModEffects { + private ModEffects() {} + + public static final Holder TOUGH = Registry.registerForHolder( + BuiltInRegistries.MOB_EFFECT, id("tough"), + new ArmorSetEffect(MobEffectCategory.BENEFICIAL, 0x59646f) + .addAttributeModifier(Attributes.ARMOR_TOUGHNESS, id("toughness"), 15.0, AttributeModifier.Operation.ADD_VALUE) + .addAttributeModifier(Attributes.KNOCKBACK_RESISTANCE, id("knockback_resistance"), 0.25, AttributeModifier.Operation.ADD_VALUE)); + + public static final Holder LIGHT_LEG = Registry.registerForHolder( + BuiltInRegistries.MOB_EFFECT, id("light_leg"), + new ArmorSetEffect(MobEffectCategory.BENEFICIAL, 0x76c7a2) + .addAttributeModifier(Attributes.MOVEMENT_SPEED, id("scout_speed"), 0.025, AttributeModifier.Operation.ADD_VALUE)); + + public static final Holder HEAVY_ARMOR = Registry.registerForHolder( + BuiltInRegistries.MOB_EFFECT, id("heavy_armor"), + new ArmorSetEffect(MobEffectCategory.BENEFICIAL, 0x55585e) + .addAttributeModifier(Attributes.MOVEMENT_SPEED, id("defender_speed"), -0.02, AttributeModifier.Operation.ADD_VALUE)); + + public static final Holder RESCUE = Registry.registerForHolder( + BuiltInRegistries.MOB_EFFECT, id("rescue"), + new ArmorSetEffect(MobEffectCategory.BENEFICIAL, 0xb84c4c)); + + public static final Holder RESCUE_COOLDOWN = Registry.registerForHolder( + BuiltInRegistries.MOB_EFFECT, id("rescue_cooldown"), + new ArmorSetEffect(MobEffectCategory.NEUTRAL, 0x6f6f6f)); + + private static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(LesRaisinsArmor.MOD_ID, path); + } + + public static void initialize() { + // Class loading performs registry registration. + } + + private static final class ArmorSetEffect extends MobEffect { + private ArmorSetEffect(MobEffectCategory category, int color) { + super(category, color); + } + } +} diff --git a/src/main/java/me/xjqsh/lesraisinsarmor/gameplay/ArmorSetEffects.java b/src/main/java/me/xjqsh/lesraisinsarmor/gameplay/ArmorSetEffects.java new file mode 100644 index 0000000..b6f8bc4 --- /dev/null +++ b/src/main/java/me/xjqsh/lesraisinsarmor/gameplay/ArmorSetEffects.java @@ -0,0 +1,81 @@ +package me.xjqsh.lesraisinsarmor.gameplay; + +import me.xjqsh.lesraisinsarmor.effect.ModEffects; +import me.xjqsh.lesraisinsarmor.item.LrArmorItem; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; + +public final class ArmorSetEffects { + private static final int SET_EFFECT_DURATION = 40; + private static final int REFRESH_THRESHOLD = 25; + private static final int RESCUE_COOLDOWN_TICKS = 30 * 20; + private static final int RESCUE_REGEN_TICKS = 5 * 20; + + private ArmorSetEffects() {} + + public static void initialize() { + ServerTickEvents.END_SERVER_TICK.register(server -> { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + tickPlayer(player); + } + }); + } + + private static void tickPlayer(ServerPlayer player) { + syncSetEffect(player, "attacker", ModEffects.TOUGH); + syncSetEffect(player, "scout", ModEffects.LIGHT_LEG); + syncSetEffect(player, "defender", ModEffects.HEAVY_ARMOR); + + if (hasFullSet(player, "medical")) { + tickMedical(player); + } else { + player.removeEffect(ModEffects.RESCUE); + } + } + + private static void tickMedical(ServerPlayer player) { + if (player.hasEffect(ModEffects.RESCUE_COOLDOWN)) { + player.removeEffect(ModEffects.RESCUE); + return; + } + + refreshEffect(player, ModEffects.RESCUE); + + if (player.getHealth() < player.getMaxHealth() * 0.30f) { + player.removeEffect(ModEffects.RESCUE); + player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, RESCUE_REGEN_TICKS, 3, false, true, true)); + player.addEffect(new MobEffectInstance(ModEffects.RESCUE_COOLDOWN, RESCUE_COOLDOWN_TICKS, 0, false, false, true)); + } + } + + private static void syncSetEffect(ServerPlayer player, String suit, net.minecraft.core.Holder effect) { + if (hasFullSet(player, suit)) { + refreshEffect(player, effect); + } else { + player.removeEffect(effect); + } + } + + private static void refreshEffect(ServerPlayer player, net.minecraft.core.Holder effect) { + MobEffectInstance current = player.getEffect(effect); + if (current == null || current.getDuration() < REFRESH_THRESHOLD) { + player.addEffect(new MobEffectInstance(effect, SET_EFFECT_DURATION, 0, false, false, true)); + } + } + + public static boolean hasFullSet(net.minecraft.world.entity.LivingEntity entity, String suit) { + return isSuitPiece(entity.getItemBySlot(EquipmentSlot.HEAD), suit) + && isSuitPiece(entity.getItemBySlot(EquipmentSlot.CHEST), suit) + && isSuitPiece(entity.getItemBySlot(EquipmentSlot.LEGS), suit) + && isSuitPiece(entity.getItemBySlot(EquipmentSlot.FEET), suit); + } + + private static boolean isSuitPiece(ItemStack stack, String suit) { + return stack.getItem() instanceof LrArmorItem armor && armor.suitId().equals(suit); + } +} diff --git a/src/main/java/me/xjqsh/lesraisinsarmor/mixin/LivingEntityDamageMixin.java b/src/main/java/me/xjqsh/lesraisinsarmor/mixin/LivingEntityDamageMixin.java new file mode 100644 index 0000000..6f5754a --- /dev/null +++ b/src/main/java/me/xjqsh/lesraisinsarmor/mixin/LivingEntityDamageMixin.java @@ -0,0 +1,26 @@ +package me.xjqsh.lesraisinsarmor.mixin; + +import me.xjqsh.lesraisinsarmor.gameplay.ArmorSetEffects; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityDamageMixin { + @ModifyArg( + method = "actuallyHurt", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;setHealth(F)V"), + index = 0 + ) + private float lrarmor$applyDefenderDamageReduction(float newHealth) { + LivingEntity self = (LivingEntity) (Object) this; + if (!ArmorSetEffects.hasFullSet(self, "defender")) { + return newHealth; + } + + float currentHealth = self.getHealth(); + float healthDamage = Math.max(0.0f, currentHealth - newHealth); + return currentHealth - healthDamage * 0.85f; + } +} diff --git a/src/main/resources/assets/lrarmor/lang/en_us.json b/src/main/resources/assets/lrarmor/lang/en_us.json index aa4cdba..0eac947 100644 --- a/src/main/resources/assets/lrarmor/lang/en_us.json +++ b/src/main/resources/assets/lrarmor/lang/en_us.json @@ -28,7 +28,6 @@ "item.lrarmor.medical_chestplate": "MEDICAL-CHEST", "item.lrarmor.medical_leggings": "MEDICAL-LEGS", "item.lrarmor.medical_boots": "MEDICAL-BOOTS", - "item.lrarmor.dea_armed_helmet": "Black Tactical Helmet", "item.lrarmor.dea_armed_chestplate": "DEA Chest", "item.lrarmor.dea_armed_leggings": "Denim Jeans", @@ -65,7 +64,6 @@ "item.lrarmor.joker_chestplate": "\"Joker\" Jacket", "item.lrarmor.joker_leggings": "\"Joker\" Pants", "item.lrarmor.joker_boots": "\"Joker\" Boots", - "suit.lrarmor.armored_chemical": "ARMORED CHEMICAL SUIT", "suit.lrarmor.attacker": "ATTACKER SUIT", "suit.lrarmor.chemical_protective": "CHEMICAL PROTECTIVE SUIT", @@ -82,10 +80,10 @@ "suit.lrarmor.fbi_armed": "FBI Special Suit", "suit.lrarmor.joker": "\"Joker\" Organization Suit", "suit.lrarmor.joker_armed": "\"Joker\" Organization Combat Suit", - "effect.lrarmor.tough": "Better Armor", "effect.lrarmor.heavy_armor": "Heavy Armor", "effect.lrarmor.light_leg": "Lightweight", "effect.lrarmor.rescue": "Self-Healing Device", - "tooltip.lramrmor.suit": "%s: %s" + "tooltip.lramrmor.suit": "%s: %s", + "effect.lrarmor.rescue_cooldown": "Self-Healing Cooldown" } diff --git a/src/main/resources/assets/lrarmor/lang/zh_cn.json b/src/main/resources/assets/lrarmor/lang/zh_cn.json index 42b9422..57d6fc3 100644 --- a/src/main/resources/assets/lrarmor/lang/zh_cn.json +++ b/src/main/resources/assets/lrarmor/lang/zh_cn.json @@ -1,4 +1,4 @@ - { +{ "itemGroup.tab.lrarmor": "绿葡萄护甲", "item.lrarmor.armored_chemical_helmet": "装甲防化服-头盔", "item.lrarmor.armored_chemical_chestplate": "装甲防化服-胸甲", @@ -28,7 +28,6 @@ "item.lrarmor.medical_chestplate": "创伤小组-胸甲", "item.lrarmor.medical_leggings": "创伤小组-护腿", "item.lrarmor.medical_boots": "创伤小组-靴子", - "item.lrarmor.dea_armed_helmet": "黑色战术头盔", "item.lrarmor.dea_armed_chestplate": "DEA 防弹衣", "item.lrarmor.dea_armed_leggings": "休闲牛仔裤", @@ -65,7 +64,6 @@ "item.lrarmor.joker_chestplate": "“愚者”礼服上衣", "item.lrarmor.joker_leggings": "“愚者”礼服长裤", "item.lrarmor.joker_boots": "“愚者”礼服靴子", - "suit.lrarmor.armored_chemical": "装甲防化服套装", "suit.lrarmor.attacker": "突击者套装", "suit.lrarmor.chemical_protective": "防化服套装", @@ -82,10 +80,10 @@ "suit.lrarmor.fbi_armed": "FBI特别组装甲套装", "suit.lrarmor.joker": "“愚者”组织套装", "suit.lrarmor.joker_armed": "“愚者”组织作战套装", - "effect.lrarmor.tough": "特制减震夹层", "effect.lrarmor.heavy_armor": "超重型全身装甲", "effect.lrarmor.light_leg": "轻量化腿部支撑", "effect.lrarmor.rescue": "便携自愈设备", - "tooltip.lramrmor.suit": "%s: %s" -} \ No newline at end of file + "tooltip.lramrmor.suit": "%s: %s", + "effect.lrarmor.rescue_cooldown": "自愈装置冷却" +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 0466b9a..56dc20d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "geckolib": ">=5.5.4" }, "mixins": [ + "lrarmor.mixins.json", { "config": "lrarmor.client.mixins.json", "environment": "client" diff --git a/src/main/resources/lrarmor.mixins.json b/src/main/resources/lrarmor.mixins.json new file mode 100644 index 0000000..7bd0907 --- /dev/null +++ b/src/main/resources/lrarmor.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "package": "me.xjqsh.lesraisinsarmor.mixin", + "mixins": [ + "LivingEntityDamageMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}