Guardian boss bars; Phase 7 approved
Adrian, 2026-11-03: guardians get a boss health bar like the Wither's, which
also leads players to them. Every player within 128 blocks of a living
guardian sees a red bar of their own (GuardianBars): name, zone, how many
blocks away, and HP. The End boss keeps the dragon's bar.
guardian_appears_and_returns checks the bar appears and goes once beaten.
Self-tests and the island check pass.
Phase 7 approved ("Then begin with the next phase"); README status updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
120c664cb0
commit
fe6b8dfe57
7 files changed
+72
-1
No files matched your search
@@ -21,6 +21,7 @@ import dev.cobblesurvival.selftest.SelfTest
|
||||
import dev.cobblesurvival.world.OnlyPokemon
|
||||
import dev.cobblesurvival.world.SharedLoot
|
||||
import dev.cobblesurvival.world.SpecialPokemon
|
||||
import dev.cobblesurvival.zone.GuardianBars
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import dev.cobblesurvival.zone.InZoneCondition
|
||||
import dev.cobblesurvival.zone.LevelPressure
|
||||
@@ -78,6 +79,7 @@ class CobbleSurvival {
|
||||
ZoneCondition.register()
|
||||
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
|
||||
Guardians.register()
|
||||
GuardianBars.register()
|
||||
BossFights.register()
|
||||
BossToughness.register()
|
||||
EndBoss.register()
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.cobblemon.mod.common.battles.BattleBuilder
|
||||
import com.cobblemon.mod.common.battles.MoveActionResponse
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.isInBattle
|
||||
import dev.cobblesurvival.zone.GuardianBars
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
@@ -49,6 +50,12 @@ object GuardianTests {
|
||||
s.check(pokemon.isUncatchable()) { "the guardian can be caught" }
|
||||
s.check(abs(pokemon.scaleModifier - definition.scale) < 0.01f) { "the guardian is size ${pokemon.scaleModifier}, not ${definition.scale}" }
|
||||
s.check(expectedLevel == null || pokemon.level == expectedLevel) { "the guardian is level ${pokemon.level}, not $expectedLevel" }
|
||||
// Adrian, 2026-11-03: a boss bar like the Wither's, with how far it is, leads the player to it.
|
||||
GuardianBars.update(level)
|
||||
val bar = GuardianBars.barFor(ZONE, player) ?: throw GameTestAssertException("no boss bar for the guardian 6 blocks away")
|
||||
s.check(player in bar.players && bar.progress == 1f && bar.name.string.contains(pokemon.species.name)) {
|
||||
"the guardian's bar is wrong: '${bar.name.string}' at ${bar.progress}"
|
||||
}
|
||||
s.moveTo(player, guardian.x + 3, guardian.y, guardian.z)
|
||||
}
|
||||
.thenWaitUntil { s.check(guardian.pokemon.entity === guardian) { "the guardian has not ticked yet" } }
|
||||
@@ -65,6 +72,7 @@ object GuardianTests {
|
||||
.thenIdle(100)
|
||||
.thenExecute {
|
||||
s.check(Guardians.guardianOf(level, ZONE) == null) { "the guardian came back right away" }
|
||||
s.check(GuardianBars.barFor(ZONE, player) == null) { "the beaten guardian's bar is still shown" }
|
||||
Guardians.expireDefeatForTest(level, ZONE)
|
||||
}
|
||||
.thenWaitUntil {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package dev.cobblesurvival.zone
|
||||
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.server.level.ServerBossEvent
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.BossEvent
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerEvent
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Every guardian shows a boss health bar, like the Wither's (Adrian, 2026-11-03), to each player within [RANGE]
|
||||
* blocks: its name, its zone and how far away it is, so the bar leads players to it, and its HP, which falls in
|
||||
* battle. Each player has a bar of their own, since the distance is theirs. The End boss has the dragon's bar
|
||||
* ([dev.cobblesurvival.boss.EndBoss]).
|
||||
*/
|
||||
object GuardianBars {
|
||||
const val RANGE = 128.0
|
||||
private const val EVERY_TICKS = 10
|
||||
private val bars = HashMap<Pair<String, UUID>, ServerBossEvent>()
|
||||
|
||||
fun register() {
|
||||
NeoForge.EVENT_BUS.addListener<ServerTickEvent.Post> { if (it.server.tickCount % EVERY_TICKS == 0) update(it.server.overworld()) }
|
||||
NeoForge.EVENT_BUS.addListener<PlayerEvent.PlayerLoggedOutEvent> { event ->
|
||||
bars.keys.filter { it.second == event.entity.uuid }.forEach { bars.remove(it)?.removeAllPlayers() }
|
||||
}
|
||||
}
|
||||
|
||||
/** The bar [player] sees for the guardian of [zone], if any. */
|
||||
fun barFor(zone: String, player: ServerPlayer): ServerBossEvent? = bars[zone to player.uuid]
|
||||
|
||||
fun update(level: ServerLevel) {
|
||||
val seen = HashSet<Pair<String, UUID>>()
|
||||
for (zone in Guardians.definitions.keys) {
|
||||
val guardian = Guardians.guardianOf(level, zone) ?: continue
|
||||
val pokemon = guardian.pokemon
|
||||
for (player in level.players()) {
|
||||
val distance = player.distanceTo(guardian)
|
||||
if (distance > RANGE || player.isSpectator) continue
|
||||
val key = zone to player.uuid
|
||||
seen += key
|
||||
val bar = bars.getOrPut(key) {
|
||||
ServerBossEvent(Component.empty(), BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.PROGRESS).also { it.addPlayer(player) }
|
||||
}
|
||||
bar.name = Component.translatableWithFallback("bossbar.cobblesurvival.guardian", "%s, guardian of %s (%s blocks away)",
|
||||
pokemon.getDisplayName(), Zones.byId[zone]?.name ?: zone, distance.toInt())
|
||||
bar.progress = (pokemon.currentHealth.toFloat() / pokemon.maxHealth.coerceAtLeast(1)).coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
bars.keys.filter { it !in seen }.forEach { bars.remove(it)?.removeAllPlayers() }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user