Move all logic regarding food / regeneration to MatteryFoodData

This commit is contained in:
DBotThePony 2025-03-14 10:37:54 +07:00
parent a34b485e68
commit c4d5ffefa5
Signed by: DBot
GPG Key ID: DCC23B5715498507
3 changed files with 129 additions and 112 deletions

View File

@ -5,15 +5,22 @@ import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.defineDecimal import ru.dbotthepony.mc.otm.core.math.defineDecimal
object PlayerConfig : AbstractConfig("player") { object PlayerConfig : AbstractConfig("player") {
init {
builder.push("Android")
}
val REGENERATE_ENERGY: Boolean by builder 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("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("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) .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 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("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("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.") .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") .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) .defineInRange("TIME_BETWEEN_NATURAL_REGENERATION", 120, 0, Int.MAX_VALUE)
init {
builder.pop()
}
object NanobotsRegeneration { object NanobotsRegeneration {
init { init {
builder.push("NanobotsRegeneration") builder.push("NanobotsRegeneration")

View File

@ -2,6 +2,9 @@ package ru.dbotthepony.mc.otm.player
import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.CompoundTag
import net.minecraft.world.Difficulty 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.entity.player.Player
import net.minecraft.world.food.FoodConstants import net.minecraft.world.food.FoodConstants
import net.minecraft.world.food.FoodData import net.minecraft.world.food.FoodData
@ -9,15 +12,24 @@ import net.minecraft.world.food.FoodProperties
import net.minecraft.world.level.GameRules import net.minecraft.world.level.GameRules
import ru.dbotthepony.mc.otm.config.IFoodRegenerationValues import ru.dbotthepony.mc.otm.config.IFoodRegenerationValues
import ru.dbotthepony.mc.otm.config.PlayerConfig 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.core.nbt.set
import ru.dbotthepony.mc.otm.registry.MDamageTypes
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MatteryFoodData(private var player: Player) : FoodData() { class MatteryFoodData(private var player: Player) : FoodData() {
private fun add(foodLevel: Int, saturation: Float) { private fun add(foodLevel: Int, saturation: Float) {
this.foodLevel = min(this.foodLevel + foodLevel, PlayerConfig.Food.HARD_FOOD_LIMIT) if (player.matteryPlayer.isAndroid && PlayerConfig.REGENERATE_ENERGY) {
this.saturationLevel = min(this.saturationLevel + saturation, this.foodLevel + PlayerConfig.Food.OVERSATURATION_LIMIT.toFloat()).coerceAtLeast(0f) 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) { override fun eat(foodLevelModifier: Int, saturationLevelModifier: Float) {
@ -29,11 +41,12 @@ class MatteryFoodData(private var player: Player) : FoodData() {
} }
override fun needsFood(): Boolean { 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) { 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) { override fun readAdditionalSaveData(compoundTag: CompoundTag) {
@ -41,25 +54,35 @@ class MatteryFoodData(private var player: Player) : FoodData() {
if ("lastFoodLevel" in compoundTag) if ("lastFoodLevel" in compoundTag)
lastFoodLevel = compoundTag.getInt("lastFoodLevel") lastFoodLevel = compoundTag.getInt("lastFoodLevel")
if ("foodAndroidEnergyToDrain" in compoundTag)
energyToDrain = compoundTag.getDecimal("foodAndroidEnergyToDrain")
} }
override fun addAdditionalSaveData(compoundTag: CompoundTag) { override fun addAdditionalSaveData(compoundTag: CompoundTag) {
super.addAdditionalSaveData(compoundTag) super.addAdditionalSaveData(compoundTag)
compoundTag["lastFoodLevel"] = lastFoodLevel compoundTag["lastFoodLevel"] = lastFoodLevel
compoundTag["foodAndroidEnergyToDrain"] = energyToDrain
} }
private var energyToDrain = Decimal.ZERO
private fun tickExhaustion() { private fun tickExhaustion() {
if (exhaustionLevel >= EXHAUSTION_PER_HUNGER_POINT) { if (exhaustionLevel >= EXHAUSTION_PER_HUNGER_POINT) {
var points = (exhaustionLevel / EXHAUSTION_PER_HUNGER_POINT).toInt() var points = (exhaustionLevel / EXHAUSTION_PER_HUNGER_POINT).toInt()
exhaustionLevel %= EXHAUSTION_PER_HUNGER_POINT exhaustionLevel %= EXHAUSTION_PER_HUNGER_POINT
if (saturationLevel > 0f) { if (player.matteryPlayer.isAndroid) {
val satisfied = min(saturationLevel.roundToInt(), points) energyToDrain += PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT * points
points -= satisfied } else {
saturationLevel -= satisfied 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) { override fun tick(player: Player) {
this.player = player this.player = player
if (player.matteryPlayer.isAndroid)
return
lastFoodLevel = foodLevel
tickExhaustion() tickExhaustion()
if ( if (player.matteryPlayer.isAndroid) {
player.level().gameRules.getBoolean(GameRules.RULE_NATURAL_REGENERATION) && if (energyToDrain > Decimal.ZERO)
(tickRegeneration(PlayerConfig.Food.FAST_REGEN) || tickRegeneration(PlayerConfig.Food.SLOW_REGEN)) energyToDrain -= player.matteryPlayer.androidEnergy.extractEnergy(energyToDrain, false)
) { else if (energyToDrain < Decimal.ZERO)
// do nothing energyToDrain += player.matteryPlayer.androidEnergy.receiveEnergy(-energyToDrain, false)
} else if (PlayerConfig.Food.ENABLE_STARVATION && foodLevel <= 0) {
if (++tickTimer >= PlayerConfig.Food.STARVATION_TICKS) {
tickTimer = 0
val threshold = when (player.level().difficulty) { if (player.level().difficulty == Difficulty.PEACEFUL && PlayerConfig.REGENERATE_ENERGY_IN_PEACEFUL)
Difficulty.PEACEFUL -> Float.POSITIVE_INFINITY player.matteryPlayer.androidEnergy.receiveEnergy(PlayerConfig.ANDROID_ENERGY_PER_HUNGER_POINT, false)
Difficulty.EASY -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_EASY.toFloat()
Difficulty.NORMAL -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_NORMAL.toFloat() if (!player.matteryPlayer.androidHasEnergy) {
Difficulty.HARD -> PlayerConfig.Food.STARVATION_HEALTH_LIMIT_HARD.toFloat() if (++tickTimer >= 20 && player.hurt(DamageSource(player.level().registryAccess().damageType(MDamageTypes.ANDROID_DISCHARGE)), 1f)) {
tickTimer = 0
} }
if (player.health > threshold) { val effect = player.activeEffectsMap[MobEffects.MOVEMENT_SLOWDOWN]
player.hurt(player.damageSources().starve(), 1.0f)
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 { } 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 { companion object {
const val EXHAUSTION_PER_HUNGER_POINT = 4f const val EXHAUSTION_PER_HUNGER_POINT = 4f
} }

View File

@ -436,8 +436,6 @@ class MatteryPlayer(val ply: Player) {
private var shouldPlaySound = false private var shouldPlaySound = false
private val research = IdentityHashMap<AndroidResearchType, AndroidResearch>() private val research = IdentityHashMap<AndroidResearchType, AndroidResearch>()
private var nextDischargeHurt = 20
private var nextHealTick = 0
/** /**
* This returns if player is an Android or will become one on death/sleep/etc * 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 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 * [IMatteryEnergyStorage] instance, representing Exopack battery charge
*/ */
@ -584,8 +585,6 @@ class MatteryPlayer(val ply: Player) {
savetables.bool(::isExopackCraftingUpgraded, "isExoSuitCraftingUpgraded") savetables.bool(::isExopackCraftingUpgraded, "isExoSuitCraftingUpgraded")
savetables.bool(::isExopackEnderAccessInstalled, "isExopackEnderAccessUpgraded") savetables.bool(::isExopackEnderAccessInstalled, "isExopackEnderAccessUpgraded")
savetables.bool(::acceptExopackChargeFromWirelessCharger) savetables.bool(::acceptExopackChargeFromWirelessCharger)
savetables.int(::nextDischargeHurt)
savetables.int(::nextHealTick)
savetables.vector(::lastLiquidPosition) savetables.vector(::lastLiquidPosition)
savetables.codec(::lastDimension, ResourceLocation.CODEC) savetables.codec(::lastDimension, ResourceLocation.CODEC)
@ -1213,81 +1212,6 @@ class MatteryPlayer(val ply: Player) {
lastLiquidPosition = ply.position 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) { for (feature in featureMap.values) {