Build: every tool pinned (JDK, packwiz, Gradle plugins, Python packages), exports normalised, tools/check_reproducible.py proves two clean builds are byte-identical. Server-side balance: a server can load config/cobblesurvival/datapack.zip in place of the jar's data, /reload applies it, tools/server_update.py packages it and refuses when a client change is needed. JEI drop pages are sent by the server. Code-held balance numbers moved to tuning/server.json. Performance: spark in the pack (server only), tools/perf_test.py with fake players whose chunks really are active, docs/performance.md. Adrian's feedback: default keys R, U, J, §; drop pages link to Cobblemon Info and show real chances; bosses only hurt in battles; battle XP halved; channels between the islands low level; temperature ×3 with insulation doubled; hazards start at once; no item dots on the map; a wild attack starts a battle; the starter only faints; quest book on join with a basics page and an HMs page; regional forms obtainable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.6 KiB
Kotlin
103 lines
4.6 KiB
Kotlin
package dev.cobblesurvival.zone
|
|
|
|
import com.google.gson.Gson
|
|
import com.google.gson.JsonElement
|
|
import dev.cobblesurvival.CobbleSurvival
|
|
import net.minecraft.ChatFormatting
|
|
import net.minecraft.core.Holder
|
|
import net.minecraft.core.registries.BuiltInRegistries
|
|
import net.minecraft.core.registries.Registries
|
|
import net.minecraft.network.chat.Component
|
|
import net.minecraft.resources.ResourceLocation
|
|
import net.minecraft.server.level.ServerPlayer
|
|
import net.minecraft.server.packs.resources.ResourceManager
|
|
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
|
|
import net.minecraft.tags.TagKey
|
|
import net.minecraft.util.profiling.ProfilerFiller
|
|
import net.minecraft.world.effect.MobEffect
|
|
import net.minecraft.world.effect.MobEffectInstance
|
|
import net.minecraft.world.entity.EquipmentSlot
|
|
import net.minecraft.world.item.Item
|
|
import net.neoforged.neoforge.common.NeoForge
|
|
import net.neoforged.neoforge.event.AddReloadListenerEvent
|
|
import net.neoforged.neoforge.event.tick.ServerTickEvent
|
|
import java.util.UUID
|
|
|
|
/**
|
|
* Zone hazards that Cold Sweat doesn't cover (roadmap Phase 3: "a poison swamp ... as area effects"): while a player
|
|
* is in the zone and wears nothing from the protection tag, they keep getting the effect. Potions, milk and golden
|
|
* apples still work against it. Definitions: `data/cobblesurvival/zone_hazards/<name>.json`.
|
|
*/
|
|
object ZoneHazards : SimpleJsonResourceReloadListener(Gson(), "zone_hazards") {
|
|
private const val CHECK_TICKS = 10
|
|
|
|
class Hazard(
|
|
val zone: String,
|
|
val effect: Holder<MobEffect>,
|
|
val amplifier: Int,
|
|
val durationTicks: Int,
|
|
val everyTicks: Int,
|
|
val protection: TagKey<Item>,
|
|
val warning: String?,
|
|
)
|
|
|
|
var hazards: List<Hazard> = emptyList()
|
|
private set
|
|
private val warned = HashMap<UUID, String>()
|
|
|
|
fun register() {
|
|
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
|
|
NeoForge.EVENT_BUS.addListener(::onServerTick)
|
|
}
|
|
|
|
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
|
|
hazards = files.mapNotNull { (id, element) ->
|
|
val json = element.asJsonObject
|
|
val effectId = ResourceLocation.parse(json.get("effect").asString)
|
|
val effect = BuiltInRegistries.MOB_EFFECT.getHolder(effectId).orElse(null)
|
|
if (effect == null) {
|
|
CobbleSurvival.LOGGER.error("Zone hazard {}: unknown effect {}", id, effectId)
|
|
return@mapNotNull null
|
|
}
|
|
Hazard(
|
|
zone = json.get("zone").asString,
|
|
effect = effect,
|
|
amplifier = json.get("amplifier")?.asInt ?: 0,
|
|
durationTicks = json.get("duration_ticks").asInt,
|
|
everyTicks = json.get("every_ticks").asInt,
|
|
protection = TagKey.create(Registries.ITEM, ResourceLocation.parse(json.get("protection").asString)),
|
|
warning = json.get("warning")?.asString,
|
|
)
|
|
}
|
|
CobbleSurvival.LOGGER.info("Loaded {} zone hazards", hazards.size)
|
|
}
|
|
|
|
private fun onServerTick(event: ServerTickEvent.Post) {
|
|
if (hazards.isEmpty()) return
|
|
if (event.server.tickCount % CHECK_TICKS != 0) return
|
|
for (player in event.server.playerList.players) {
|
|
if (player.isCreative || player.isSpectator) continue
|
|
val zone = Zones.at(player.serverLevel(), player.blockPosition())?.id
|
|
if (zone == null || hazards.none { it.zone == zone }) {
|
|
warned.remove(player.uuid)
|
|
continue
|
|
}
|
|
for (hazard in hazards) {
|
|
if (hazard.zone != zone || isProtected(player, hazard)) continue
|
|
// At once on entering the zone (Adrian, 2026-11-09), then renewed every every_ticks.
|
|
val current = player.getEffect(hazard.effect)
|
|
if (current != null && current.duration > hazard.durationTicks - hazard.everyTicks) continue
|
|
player.addEffect(MobEffectInstance(hazard.effect, hazard.durationTicks, hazard.amplifier))
|
|
if (hazard.warning != null && warned[player.uuid] != zone) {
|
|
warned[player.uuid] = zone
|
|
player.displayClientMessage(Component.translatable(hazard.warning).withStyle(ChatFormatting.DARK_GREEN), true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun isProtected(player: ServerPlayer, hazard: Hazard) =
|
|
listOf(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET)
|
|
.any { player.getItemBySlot(it).`is`(hazard.protection) }
|
|
}
|