Phase 7: practice-boss and guardian-reset commands; steadier spawn survey
Adrian (2026-11-02): a way to force-spawn the bosses. - /cobblesurvival boss <zone|end>: a practice copy next to you (same species, level, size, attacks; no drops, not the guardian's defeat). - /cobblesurvival guardians reset <zone>: despawns the zone's guardian and clears its defeat, so it appears again when someone comes near (Guardians.reset, formerly resetForTest). - Self-test practice_bosses_spawn. - spawn_survey stands on the ground below trees: on a mangrove's roots it found no natural ground and nothing could spawn at the Mire sample. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd232bdf6a
commit
c451c58483
6 files changed
+116
-6
No files matched your search
@@ -9,6 +9,7 @@ import dev.cobblesurvival.dev.OreVeins
|
|||||||
import dev.cobblesurvival.ore.BaseLoot
|
import dev.cobblesurvival.ore.BaseLoot
|
||||||
import dev.cobblesurvival.ore.OreNodes
|
import dev.cobblesurvival.ore.OreNodes
|
||||||
import dev.cobblesurvival.permadeath.Permadeath
|
import dev.cobblesurvival.permadeath.Permadeath
|
||||||
|
import dev.cobblesurvival.boss.BossCommands
|
||||||
import dev.cobblesurvival.boss.BossFights
|
import dev.cobblesurvival.boss.BossFights
|
||||||
import dev.cobblesurvival.boss.EndBoss
|
import dev.cobblesurvival.boss.EndBoss
|
||||||
import dev.cobblesurvival.machine.MachineShowcase
|
import dev.cobblesurvival.machine.MachineShowcase
|
||||||
@@ -132,7 +133,8 @@ class CobbleSurvival {
|
|||||||
val lines = Guardians.status(context.source.server.overworld())
|
val lines = Guardians.status(context.source.server.overworld())
|
||||||
context.source.sendSuccess({ Component.literal(lines.joinToString("\n").ifEmpty { "no guardians loaded" }) }, false)
|
context.source.sendSuccess({ Component.literal(lines.joinToString("\n").ifEmpty { "no guardians loaded" }) }, false)
|
||||||
1
|
1
|
||||||
})
|
}.then(BossCommands.guardianReset()))
|
||||||
|
.then(BossCommands.practice())
|
||||||
if (SelfTest.enabled) root.then(SelfTest.command())
|
if (SelfTest.enabled) root.then(SelfTest.command())
|
||||||
event.dispatcher.register(root)
|
event.dispatcher.register(root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package dev.cobblesurvival.boss
|
||||||
|
|
||||||
|
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||||
|
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||||
|
import net.minecraft.commands.CommandSourceStack
|
||||||
|
import net.minecraft.commands.Commands
|
||||||
|
import net.minecraft.commands.SharedSuggestionProvider
|
||||||
|
import net.minecraft.network.chat.Component
|
||||||
|
import net.minecraft.server.level.ServerLevel
|
||||||
|
import net.minecraft.world.phys.Vec3
|
||||||
|
import dev.cobblesurvival.zone.Guardians
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator commands to try the bosses (Adrian, 2026-11-02: a way to force-spawn them):
|
||||||
|
* - `/cobblesurvival boss <zone|end>`: a practice copy next to you, anywhere. Same species, level, size and attacks
|
||||||
|
* as the real boss, uncatchable; beating it gives nothing and doesn't count as the guardian's defeat.
|
||||||
|
* - `/cobblesurvival guardians reset <zone>`: the zone's real guardian forgets its last defeat (the one standing is
|
||||||
|
* despawned) and appears again when a player comes near its spot (`/cobblesurvival guardians` says where).
|
||||||
|
*/
|
||||||
|
object BossCommands {
|
||||||
|
private const val END = "end"
|
||||||
|
private const val DISTANCE = 6.0
|
||||||
|
|
||||||
|
private fun choices() = Guardians.definitions.keys.sorted() + END
|
||||||
|
|
||||||
|
fun practice(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal("boss").then(
|
||||||
|
Commands.argument("boss", StringArgumentType.word())
|
||||||
|
.suggests { _, builder -> SharedSuggestionProvider.suggest(choices(), builder) }
|
||||||
|
.executes { context ->
|
||||||
|
val source = context.source
|
||||||
|
val which = StringArgumentType.getString(context, "boss")
|
||||||
|
val facing = source.entity?.lookAngle?.multiply(1.0, 0.0, 1.0)?.normalize() ?: Vec3(0.0, 0.0, 1.0)
|
||||||
|
val entity = spawnPractice(source.level, source.position.add(facing.scale(DISTANCE)), which)
|
||||||
|
if (entity == null) {
|
||||||
|
source.sendFailure(Component.literal("No boss \"$which\"; try one of: ${choices().joinToString()}"))
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
source.sendSuccess({ Component.literal("A practice ${entity.pokemon.species.name} (Lv ${entity.pokemon.level}) appeared: no drops, it doesn't count.") }, true)
|
||||||
|
1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/** A practice copy of the boss [which] (a zone or "end") at [pos]; null if there is no such boss. */
|
||||||
|
fun spawnPractice(level: ServerLevel, pos: Vec3, which: String): PokemonEntity? {
|
||||||
|
val (properties, scale, def) = if (which == END) {
|
||||||
|
val end = BossDefs.end?.end ?: return null
|
||||||
|
Triple("${end.species} level=${end.level}", end.scale, BossDefs.end ?: return null)
|
||||||
|
} else {
|
||||||
|
val guardian = Guardians.definitions[which] ?: return null
|
||||||
|
Triple(guardian.pokemon, guardian.scale, BossDefs.byGuardian[which] ?: return null)
|
||||||
|
}
|
||||||
|
val pokemon = PokemonProperties.parse("$properties uncatchable=yes").create()
|
||||||
|
pokemon.scaleModifier = scale
|
||||||
|
pokemon.persistentData.putString(BossFights.TAG, def.id.toString())
|
||||||
|
val entity = PokemonEntity(level, pokemon)
|
||||||
|
entity.moveTo(pos.x, pos.y, pos.z, 0f, 0f)
|
||||||
|
entity.setPersistenceRequired()
|
||||||
|
return entity.takeIf { level.addFreshEntity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun guardianReset(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal("reset").then(
|
||||||
|
Commands.argument("zone", StringArgumentType.word())
|
||||||
|
.suggests { _, builder -> SharedSuggestionProvider.suggest(Guardians.definitions.keys.sorted(), builder) }
|
||||||
|
.executes { context ->
|
||||||
|
val zone = StringArgumentType.getString(context, "zone")
|
||||||
|
if (zone !in Guardians.definitions) {
|
||||||
|
context.source.sendFailure(Component.literal("No guardian for \"$zone\""))
|
||||||
|
return@executes 0
|
||||||
|
}
|
||||||
|
Guardians.reset(context.source.server.overworld(), zone)
|
||||||
|
context.source.sendSuccess({ Component.literal("The guardian of $zone appears again when a player comes near its spot.") }, true)
|
||||||
|
1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -32,8 +32,21 @@ object BossTests {
|
|||||||
SelfTestCase("boss_attacks_land_and_stop_with_the_battle", 400, ::landAndStop),
|
SelfTestCase("boss_attacks_land_and_stop_with_the_battle", 400, ::landAndStop),
|
||||||
SelfTestCase("boss_arena_leaving_loses", 400, ::arena),
|
SelfTestCase("boss_arena_leaving_loses", 400, ::arena),
|
||||||
SelfTestCase("end_boss_replaces_the_dragon", 200, ::endBoss),
|
SelfTestCase("end_boss_replaces_the_dragon", 200, ::endBoss),
|
||||||
|
SelfTestCase("practice_bosses_spawn", 20, ::practice),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** `/cobblesurvival boss <zone|end>`: a copy that fights like the real boss, for every guardian and the End. */
|
||||||
|
private fun practice(s: Scenario) {
|
||||||
|
for (which in Guardians.definitions.keys + "end") {
|
||||||
|
val entity = dev.cobblesurvival.boss.BossCommands.spawnPractice(s.level, s.at(7.5, 1.0, 7.5), which)
|
||||||
|
?: throw GameTestAssertException("no practice boss for $which")
|
||||||
|
s.check(BossFights.defOf(entity) != null && entity.pokemon.isUncatchable()) { "the practice $which is no uncatchable boss" }
|
||||||
|
s.check(entity.pokemon.persistentData.getString(Guardians.TAG).isEmpty()) { "the practice $which counts as the real guardian" }
|
||||||
|
entity.discard()
|
||||||
|
}
|
||||||
|
s.helper.succeed()
|
||||||
|
}
|
||||||
|
|
||||||
private fun implemented(species: String) = PokemonSpecies.getByName(species)?.implemented == true
|
private fun implemented(species: String) = PokemonSpecies.getByName(species)?.implemented == true
|
||||||
|
|
||||||
private fun coverage(s: Scenario) {
|
private fun coverage(s: Scenario) {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ object GuardianTests {
|
|||||||
val definition = Guardians.definitions[ZONE] ?: throw GameTestAssertException("no guardian defined for $ZONE")
|
val definition = Guardians.definitions[ZONE] ?: throw GameTestAssertException("no guardian defined for $ZONE")
|
||||||
val sample = Zones.samples[ZONE] ?: throw GameTestAssertException("no sample point for $ZONE")
|
val sample = Zones.samples[ZONE] ?: throw GameTestAssertException("no sample point for $ZONE")
|
||||||
val level = s.level
|
val level = s.level
|
||||||
Guardians.resetForTest(level, ZONE)
|
Guardians.reset(level, ZONE)
|
||||||
s.afterTest { Guardians.resetForTest(level, ZONE) }
|
s.afterTest { Guardians.reset(level, ZONE) }
|
||||||
|
|
||||||
val player = s.player()
|
val player = s.player()
|
||||||
s.give(player, s.pokemon("mewtwo", 100, "earthquake")) // a spread move needs no target
|
s.give(player, s.pokemon("mewtwo", 100, "earthquake")) // a spread move needs no target
|
||||||
|
|||||||
@@ -108,13 +108,29 @@ object ZoneTests {
|
|||||||
}.thenSucceed()
|
}.thenSucceed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ground at [x] [z], below any tree: standing on a mangrove's roots or logs, the survey once found no natural
|
||||||
|
* ground around it, so nothing could spawn (2026-11-02).
|
||||||
|
*/
|
||||||
|
private fun ground(level: net.minecraft.server.level.ServerLevel, x: Int, z: Int): BlockPos {
|
||||||
|
var y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z)
|
||||||
|
while (y > level.minBuildHeight) {
|
||||||
|
val below = level.getBlockState(BlockPos(x, y - 1, z))
|
||||||
|
val tree = below.`is`(net.minecraft.tags.BlockTags.LOGS) || below.`is`(net.minecraft.tags.BlockTags.LEAVES) ||
|
||||||
|
below.`is`(net.minecraft.world.level.block.Blocks.MANGROVE_ROOTS)
|
||||||
|
if (below.blocksMotion() && !tree) break
|
||||||
|
y--
|
||||||
|
}
|
||||||
|
return BlockPos(x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
/** What can spawn around the sample right now, and how many spawnable positions Cobblemon found there. */
|
/** What can spawn around the sample right now, and how many spawnable positions Cobblemon found there. */
|
||||||
private fun survey(s: Scenario, player: ServerPlayer, zoneId: String, sample: ZoneSample): Pair<Set<SpawnDetail>, Int> {
|
private fun survey(s: Scenario, player: ServerPlayer, zoneId: String, sample: ZoneSample): Pair<Set<SpawnDetail>, Int> {
|
||||||
val level = s.level
|
val level = s.level
|
||||||
// Generate the chunks around the sample, then find where to stand.
|
// Generate the chunks around the sample, then find where to stand.
|
||||||
for (dx in -2..2) for (dz in -2..2) level.getChunk((sample.x shr 4) + dx, (sample.z shr 4) + dz)
|
for (dx in -2..2) for (dz in -2..2) level.getChunk((sample.x shr 4) + dx, (sample.z shr 4) + dz)
|
||||||
val stand = if (sample.y == null) {
|
val stand = if (sample.y == null) {
|
||||||
BlockPos(sample.x, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z), sample.z)
|
ground(level, sample.x, sample.z)
|
||||||
} else {
|
} else {
|
||||||
caveFloor(s, zoneId, sample, sample.y)
|
caveFloor(s, zoneId, sample, sample.y)
|
||||||
?: throw GameTestAssertException("no open $zoneId cave near ${sample.x} ${sample.y} ${sample.z}")
|
?: throw GameTestAssertException("no open $zoneId cave near ${sample.x} ${sample.y} ${sample.z}")
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** For the self-tests: forget the zone's guardian (despawning it) or pretend it was beaten long enough ago. */
|
/** Forget the zone's guardian (despawning it) and its last defeat: it appears again when a player comes near.
|
||||||
fun resetForTest(level: ServerLevel, zone: String) {
|
* For the self-tests and `/cobblesurvival guardians reset <zone>`. */
|
||||||
|
fun reset(level: ServerLevel, zone: String) {
|
||||||
val entry = GuardianState.of(level).entry(zone)
|
val entry = GuardianState.of(level).entry(zone)
|
||||||
entry.entity?.let { level.getEntity(it) }?.discard()
|
entry.entity?.let { level.getEntity(it) }?.discard()
|
||||||
entry.entity = null
|
entry.entity = null
|
||||||
|
|||||||
Reference in new issue
Block a user