diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/config/PlayerConfig.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/config/PlayerConfig.kt index fb5108a30..7bad9ab1c 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/config/PlayerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/config/PlayerConfig.kt @@ -5,15 +5,22 @@ import ru.dbotthepony.mc.otm.core.math.Decimal import ru.dbotthepony.mc.otm.core.math.defineDecimal object PlayerConfig : AbstractConfig("player") { + init { + builder.push("Android") + } + val REGENERATE_ENERGY: Boolean by builder - .comment("If (technically) hunger is above threshold, it turns into energy") .comment("This setting controls whenever to regenerate small amount of energy while eating as Android") .comment("And also whenever to regenerate energy while in peaceful") - .comment("If this is disabled, any (technically) excess hunger will be nullified, unless playing on peaceful difficulty.") .define("REGENERATE_ENERGY", true) + val REGENERATE_ENERGY_IN_PEACEFUL: Boolean by builder + .comment("Regenerate energy while in peaceful") + .comment("This is disabled by default because this is easily exploitable") + .define("REGENERATE_ENERGY_IN_PEACEFUL", false) + val TIME_BETWEEN_NATURAL_REGENERATION: Int by builder - .comment("Time in ticks between natural health regeneration ticks") + .comment("Time in ticks between natural health regeneration ticks for Android") .comment("Default value is meant to be one of downsides of being an android,") .comment("so please, don't blindly buff it, players have ability to research into Nanobots Regeneration,") .comment("which provide superior regeneration on average than human players.") @@ -24,6 +31,10 @@ object PlayerConfig : AbstractConfig("player") { .comment("for android players, since 'hunger' (for compatibility) is managed by mod in such case") .defineInRange("TIME_BETWEEN_NATURAL_REGENERATION", 120, 0, Int.MAX_VALUE) + init { + builder.pop() + } + object NanobotsRegeneration { init { builder.push("NanobotsRegeneration") diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryFoodData.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryFoodData.kt index 1fc92dfa0..279438107 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryFoodData.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryFoodData.kt @@ -2,6 +2,9 @@ package ru.dbotthepony.mc.otm.player import net.minecraft.nbt.CompoundTag import net.minecraft.world.Difficulty +import net.minecraft.world.damagesource.DamageSource +import net.minecraft.world.effect.MobEffectInstance +import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.player.Player import net.minecraft.world.food.FoodConstants import net.minecraft.world.food.FoodData @@ -9,15 +12,24 @@ import net.minecraft.world.food.FoodProperties import net.minecraft.world.level.GameRules import ru.dbotthepony.mc.otm.config.IFoodRegenerationValues import ru.dbotthepony.mc.otm.config.PlayerConfig +import ru.dbotthepony.mc.otm.core.damageType +import ru.dbotthepony.mc.otm.core.math.Decimal +import ru.dbotthepony.mc.otm.core.math.getDecimal +import ru.dbotthepony.mc.otm.core.math.set import ru.dbotthepony.mc.otm.core.nbt.set +import ru.dbotthepony.mc.otm.registry.MDamageTypes import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt class MatteryFoodData(private var player: Player) : FoodData() { private fun add(foodLevel: Int, saturation: Float) { - this.foodLevel = min(this.foodLevel + foodLevel, PlayerConfig.Food.HARD_FOOD_LIMIT) - this.saturationLevel = min(this.saturationLevel + saturation, this.foodLevel + PlayerConfig.Food.OVERSATURATION_LIMIT.toFloat()).coerceAtLeast(0f) + if (player.matteryPlayer.isAndroid && PlayerConfig.REGENERATE_ENERGY) { + energyToDrain -= PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * foodLevel + PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * saturation + } else if (!player.matteryPlayer.isAndroid) { + this.foodLevel = min(this.foodLevel + foodLevel, PlayerConfig.Food.HARD_FOOD_LIMIT) + this.saturationLevel = min(this.saturationLevel + saturation, this.foodLevel + PlayerConfig.Food.OVERSATURATION_LIMIT.toFloat()).coerceAtLeast(0f) + } } override fun eat(foodLevelModifier: Int, saturationLevelModifier: Float) { @@ -29,11 +41,12 @@ class MatteryFoodData(private var player: Player) : FoodData() { } override fun needsFood(): Boolean { - return foodLevel < PlayerConfig.Food.SOFT_FOOD_LIMIT + return player.matteryPlayer.isAndroid || foodLevel < PlayerConfig.Food.SOFT_FOOD_LIMIT } override fun addExhaustion(exhaustion: Float) { - this.exhaustionLevel = min(this.exhaustionLevel + exhaustion, PlayerConfig.Food.EXHAUSTION_LIMIT.toFloat()) + // store exhaustion as usual, and handle its reduction in tick for both humans and androids + this.exhaustionLevel = min(this.exhaustionLevel + exhaustion, PlayerConfig.Food.EXHAUSTION_LIMIT.toFloat()).coerceAtLeast(0f) } override fun readAdditionalSaveData(compoundTag: CompoundTag) { @@ -41,25 +54,35 @@ class MatteryFoodData(private var player: Player) : FoodData() { if ("lastFoodLevel" in compoundTag) lastFoodLevel = compoundTag.getInt("lastFoodLevel") + + if ("foodAndroidEnergyToDrain" in compoundTag) + energyToDrain = compoundTag.getDecimal("foodAndroidEnergyToDrain") } override fun addAdditionalSaveData(compoundTag: CompoundTag) { super.addAdditionalSaveData(compoundTag) compoundTag["lastFoodLevel"] = lastFoodLevel + compoundTag["foodAndroidEnergyToDrain"] = energyToDrain } + private var energyToDrain = Decimal.ZERO + private fun tickExhaustion() { if (exhaustionLevel >= EXHAUSTION_PER_HUNGER_POINT) { var points = (exhaustionLevel / EXHAUSTION_PER_HUNGER_POINT).toInt() exhaustionLevel %= EXHAUSTION_PER_HUNGER_POINT - if (saturationLevel > 0f) { - val satisfied = min(saturationLevel.roundToInt(), points) - points -= satisfied - saturationLevel -= satisfied - } + if (player.matteryPlayer.isAndroid) { + energyToDrain += PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * points + } else { + if (saturationLevel > 0f) { + val satisfied = min(saturationLevel.roundToInt(), points) + points -= satisfied + saturationLevel -= satisfied + } - foodLevel = max(0, foodLevel - points) + foodLevel = max(0, foodLevel - points) + } } } @@ -82,37 +105,96 @@ class MatteryFoodData(private var player: Player) : FoodData() { override fun tick(player: Player) { this.player = player - if (player.matteryPlayer.isAndroid) - return - - lastFoodLevel = foodLevel tickExhaustion() - if ( - player.level().gameRules.getBoolean(GameRules.RULE_NATURAL_REGENERATION) && - (tickRegeneration(PlayerConfig.Food.FAST_REGEN) || tickRegeneration(PlayerConfig.Food.SLOW_REGEN)) - ) { - // do nothing - } else if (PlayerConfig.Food.ENABLE_STARVATION && foodLevel <= 0) { - if (++tickTimer >= PlayerConfig.Food.STARVATION_TICKS) { - tickTimer = 0 + if (player.matteryPlayer.isAndroid) { + if (energyToDrain > Decimal.ZERO) + energyToDrain -= player.matteryPlayer.androidEnergy.extractEnergy(energyToDrain, false) + else if (energyToDrain < Decimal.ZERO) + energyToDrain += player.matteryPlayer.androidEnergy.receiveEnergy(-energyToDrain, false) - val threshold = when (player.level().difficulty) { - Difficulty.PEACEFUL -> Float.POSITIVE_INFINITY - Difficulty.EASY -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_EASY.toFloat() - Difficulty.NORMAL -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_NORMAL.toFloat() - Difficulty.HARD -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_HARD.toFloat() + if (player.level().difficulty == Difficulty.PEACEFUL && PlayerConfig.REGENERATE_ENERGY_IN_PEACEFUL) + player.matteryPlayer.androidEnergy.receiveEnergy(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT, false) + + if (!player.matteryPlayer.androidHasEnergy) { + if (++tickTimer >= 20 && player.hurt(DamageSource(player.level().registryAccess().damageType(MDamageTypes.ANDROID_DISCHARGE)), 1f)) { + tickTimer = 0 } - if (player.health > threshold) { - player.hurt(player.damageSources().starve(), 1.0f) + val effect = player.activeEffectsMap[MobEffects.MOVEMENT_SLOWDOWN] + + if (effect == null || effect.duration < 40 || effect.amplifier < 2) { + player.addEffect(MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, effect?.duration?.coerceAtLeast(60) ?: 60, effect?.amplifier?.coerceAtLeast(2) ?: 2, false, false)) + } + } else { + if (player.isHurt && player.level().gameRules.getBoolean(GameRules.RULE_NATURAL_REGENERATION)) { + if (++tickTimer >= PlayerConfig.TIME_BETWEEN_NATURAL_REGENERATION) { + player.heal(1f) + addExhaustion(6f) + tickTimer = 0 + } + } else { + tickTimer = 0 } } } else { - tickTimer = 0 + lastFoodLevel = foodLevel + energyToDrain = Decimal.ZERO + + if ( + player.level().gameRules.getBoolean(GameRules.RULE_NATURAL_REGENERATION) && + (tickRegeneration(PlayerConfig.Food.FAST_REGEN) || tickRegeneration(PlayerConfig.Food.SLOW_REGEN)) + ) { + // do nothing + } else if (PlayerConfig.Food.ENABLE_STARVATION && foodLevel <= 0) { + if (++tickTimer >= PlayerConfig.Food.STARVATION_TICKS) { + tickTimer = 0 + + val threshold = when (player.level().difficulty) { + Difficulty.PEACEFUL -> Float.POSITIVE_INFINITY + Difficulty.EASY -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_EASY.toFloat() + Difficulty.NORMAL -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_NORMAL.toFloat() + Difficulty.HARD -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_HARD.toFloat() + } + + if (player.health > threshold) { + player.hurt(player.damageSources().starve(), 1.0f) + } + } + } else { + tickTimer = 0 + } } } + override fun getFoodLevel(): Int { + if (player.matteryPlayer.isAndroid) + return if (player.matteryPlayer.androidHasEnergy) PlayerConfig.Food.SOFT_FOOD_LIMIT else 0 + + return super.getFoodLevel() + } + + override fun getLastFoodLevel(): Int { + if (player.matteryPlayer.isAndroid) + return getFoodLevel() + + return super.getLastFoodLevel() + } + + override fun getExhaustionLevel(): Float { + if (player.matteryPlayer.isAndroid && player.matteryPlayer.androidHasEnergy) + return 0f + + return super.getExhaustionLevel() + } + + override fun getSaturationLevel(): Float { + if (player.matteryPlayer.isAndroid) + return if (player.matteryPlayer.androidHasEnergy) player.matteryPlayer.androidEnergy.batteryLevel.percentage(player.matteryPlayer.androidEnergy.maxBatteryLevel) * PlayerConfig.Food.SOFT_FOOD_LIMIT else 0f + + return super.getSaturationLevel() + } + companion object { const val EXHAUSTION_PER_HUNGER_POINT = 4f } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryPlayer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryPlayer.kt index bde1b88f8..2553a6656 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryPlayer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/player/MatteryPlayer.kt @@ -436,8 +436,6 @@ class MatteryPlayer(val ply: Player) { private var shouldPlaySound = false private val research = IdentityHashMap() - private var nextDischargeHurt = 20 - private var nextHealTick = 0 /** * This returns if player is an Android or will become one on death/sleep/etc @@ -563,6 +561,9 @@ class MatteryPlayer(val ply: Player) { */ val androidEnergy = BatteryBackedEnergyStorage(ply, syncher, PlayerConfig.ANDROID_MAX_ENERGY, PlayerConfig.ANDROID_MAX_ENERGY, true) + val androidHasEnergy: Boolean + get() = androidEnergy.batteryLevel > Decimal.TEN + /** * [IMatteryEnergyStorage] instance, representing Exopack battery charge */ @@ -584,8 +585,6 @@ class MatteryPlayer(val ply: Player) { savetables.bool(::isExopackCraftingUpgraded, "isExoSuitCraftingUpgraded") savetables.bool(::isExopackEnderAccessInstalled, "isExopackEnderAccessUpgraded") savetables.bool(::acceptExopackChargeFromWirelessCharger) - savetables.int(::nextDischargeHurt) - savetables.int(::nextHealTick) savetables.vector(::lastLiquidPosition) savetables.codec(::lastDimension, ResourceLocation.CODEC) @@ -1213,81 +1212,6 @@ class MatteryPlayer(val ply: Player) { lastLiquidPosition = ply.position } - - val stats = ply.foodData - val fourTimesTheHunger = PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * 4 - - // истощение - if (stats.exhaustionLevel > 0f) { - val extracted = androidEnergy.extractEnergy(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * (stats.exhaustionLevel / 4f), false) - stats.setExhaustion(stats.exhaustionLevel - (extracted / PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT).toFloat() * 4f) - } - - // Обычный голод - while ( - stats.foodLevel < 18 && - androidEnergy.batteryLevel >= fourTimesTheHunger && - androidEnergy.extractEnergyExact(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT, false) - ) { - stats.foodLevel++ - } - - // "поглощение" излишек голода, как при мирном режиме, так и при поедании обычной еды - if (PlayerConfig.REGENERATE_ENERGY) { - while (stats.foodLevel > 18 && androidEnergy.receiveEnergyExact(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT / 2, false)) { - stats.foodLevel-- - } - } else if (ply.level().difficulty != Difficulty.PEACEFUL) { - stats.foodLevel = stats.foodLevel.coerceAtMost(18) - } - - val foodLevel = stats.foodLevel.toFloat() - - // насыщение - if (stats.saturationLevel < foodLevel && androidEnergy.batteryLevel >= fourTimesTheHunger) { - val extracted = androidEnergy.extractEnergy(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * (foodLevel - stats.saturationLevel), false) - stats.setSaturation(stats.saturationLevel + (extracted / PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT).toFloat()) - } - - if (androidEnergy.batteryLevel <= Decimal.TEN && !ply.isCreative && ply.level().difficulty != Difficulty.PEACEFUL) { - if (stats.saturationLevel > 1f) { - if (androidEnergy.receiveEnergyExact(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT, false)) { - stats.setSaturation(stats.saturationLevel - 1f) - } - } else if (stats.saturationLevel > 0f) { - val received = androidEnergy.receiveEnergy(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * stats.saturationLevel, false) - stats.setSaturation(stats.saturationLevel - (received / PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT).toFloat()) - } else if (stats.foodLevel > 0) { - // так как голод не тикает для андроидов, "умереть с голоду" мы не можем - // но со стороны будет выглядеть как будто мы умираем с голода - if (androidEnergy.receiveEnergyExact(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT, false)) { - stats.foodLevel-- - } - } - - if (androidEnergy.batteryLevel <= Decimal.TEN) { - if (--nextDischargeHurt <= 0 && ply.hurt(DamageSource(ply.level().registryAccess().damageType(MDamageTypes.ANDROID_DISCHARGE)), 1f)) { - nextDischargeHurt = 20 - } - - val effect = ply.activeEffectsMap[MobEffects.MOVEMENT_SLOWDOWN] - - if (effect == null || effect.duration < 40 || effect.amplifier < 2) { - ply.addEffect(MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, effect?.duration?.coerceAtLeast(60) ?: 60, effect?.amplifier?.coerceAtLeast(2) ?: 2, false, false)) - } - } - } else { - nextDischargeHurt = 20 - - if (ply.isHurt && ply.level().gameRules.getBoolean(GameRules.RULE_NATURAL_REGENERATION)) { - if (--nextHealTick <= 0) { - nextHealTick = if (ply.level().difficulty == Difficulty.PEACEFUL) 10 else PlayerConfig.TIME_BETWEEN_NATURAL_REGENERATION - ply.heal(1f) - } - } else { - nextHealTick = if (ply.level().difficulty == Difficulty.PEACEFUL) 10 else PlayerConfig.TIME_BETWEEN_NATURAL_REGENERATION - } - } } for (feature in featureMap.values) {