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
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")

View File

@ -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
}

View File

@ -436,8 +436,6 @@ class MatteryPlayer(val ply: Player) {
private var shouldPlaySound = false
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
@ -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) {