Big, tough bosses; working machines show it; JEI drop pages on the roadmap
Bosses (Adrian, 2026-11-02: Eternatus too small and too easy; guardians way bigger and tougher; only defensive stats up; no attack takes more than 25 %): - Guardians scale to about 8 blocks tall (1.5x-12x), Eternatus to 15 blocks (2.5x, was 0.2x). Cave guardians look for a spot with room above them. Bosses already in a world grow on the next check outside battle. - Perfect IVs and 170 EVs each in HP, Defense and Sp. Def, nothing offensive. - The Guardian's Ward, a Showdown-only held item (held_items/guardiansward.js) handed to every boss by a HeldItemProvider: Defense and Sp. Def x2 while defending, and one attack (all hits together) takes at most 25 % of max HP. - Area attacks that start at the boss reach half its width further. Machines (Adrian, 2026-11-02): a working core is active: it glows, its front animates (flickering fire on the furnace core, pulsing emblems elsewhere), and particles show in the chamber and at the core (client side). The Pokemon inside flinches about every 15 s (red flash, Cobblemon's hit sound, no damage). Roadmap Phase 8: every item that can drop gets a JEI page naming its sources. Self-tests: practice_bosses_spawn checks size and training, boss_takes_a_quarter_per_attack runs a real battle (each Lv 100 Bullet Seed takes exactly a quarter), heat_core_cooks_with_hoppers checks the active state and the flinch. Self-tests and the island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f0bc03919
commit
120c664cb0
58 files changed
+681
-86
No files matched your search
@@ -11,6 +11,7 @@ import dev.cobblesurvival.ore.OreNodes
|
||||
import dev.cobblesurvival.permadeath.Permadeath
|
||||
import dev.cobblesurvival.boss.BossCommands
|
||||
import dev.cobblesurvival.boss.BossFights
|
||||
import dev.cobblesurvival.boss.BossToughness
|
||||
import dev.cobblesurvival.boss.EndBoss
|
||||
import dev.cobblesurvival.machine.MachineShowcase
|
||||
import dev.cobblesurvival.machine.Machines
|
||||
@@ -78,6 +79,7 @@ class CobbleSurvival {
|
||||
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
|
||||
Guardians.register()
|
||||
BossFights.register()
|
||||
BossToughness.register()
|
||||
EndBoss.register()
|
||||
Gates.register()
|
||||
ShoulderPassives.register()
|
||||
|
||||
@@ -53,8 +53,9 @@ object BossCommands {
|
||||
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())
|
||||
BossToughness.toughen(pokemon, scale)
|
||||
pokemon.heal()
|
||||
val entity = PokemonEntity(level, pokemon)
|
||||
entity.moveTo(pos.x, pos.y, pos.z, 0f, 0f)
|
||||
entity.setPersistenceRequired()
|
||||
|
||||
@@ -36,14 +36,18 @@ object BossDefs : SimpleJsonResourceReloadListener(Gson(), "bosses") {
|
||||
val effect: Effect,
|
||||
val trigger: Trigger,
|
||||
) {
|
||||
/** Where this pattern lands, aimed from [boss] at [target]. */
|
||||
fun area(boss: Vec3, target: Vec3): Area {
|
||||
/**
|
||||
* Where this pattern lands, aimed from [boss] at [target]. Shapes that start at the boss reach [body] further
|
||||
* (the boss's half width): a big boss's ring starts at its edge, not inside it.
|
||||
*/
|
||||
fun area(boss: Vec3, target: Vec3, body: Double = 0.0): Area {
|
||||
val origin = if (aim == Aim.BOSS) boss else target
|
||||
val grow = if (aim == Aim.BOSS) body else 0.0
|
||||
return when (shape) {
|
||||
Shape.CIRCLE -> Area.Circle(origin, radius)
|
||||
Shape.RING -> Area.Ring(origin, inner, outer)
|
||||
Shape.LINE -> Area.Line(boss, heading(boss, target), length, width / 2)
|
||||
Shape.CONE -> Area.Cone(boss, heading(boss, target), length, Math.toRadians(angle / 2))
|
||||
Shape.CIRCLE -> Area.Circle(origin, radius + grow)
|
||||
Shape.RING -> Area.Ring(origin, inner + grow, outer + grow)
|
||||
Shape.LINE -> Area.Line(boss, heading(boss, target), length + body, width / 2)
|
||||
Shape.CONE -> Area.Cone(boss, heading(boss, target), length + body, Math.toRadians(angle / 2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ object BossFights {
|
||||
fights += fight
|
||||
battle.players.forEach {
|
||||
it.sendSystemMessage(Component.translatableWithFallback("message.cobblesurvival.boss.start",
|
||||
"Boss fight: watch the ground and dodge its attacks. Leaving the arena (%s blocks) loses the fight.",
|
||||
"Boss fight: watch the ground and dodge its attacks. Leaving the arena (%s blocks) loses the fight. No single attack takes more than a quarter of its HP.",
|
||||
def.arena.toInt()).withStyle(ChatFormatting.GOLD))
|
||||
}
|
||||
CobbleSurvival.LOGGER.info("Boss fight {} begins against {}", def.id, battle.players.joinToString { it.gameProfile.name })
|
||||
@@ -135,7 +135,7 @@ object BossFights {
|
||||
/** Starts [pattern], aimed at [target]: it telegraphs, then lands. */
|
||||
fun cast(fight: Fight, pattern: BossDefs.Pattern, target: Vec3): Telegraph {
|
||||
val level = fight.boss.level() as ServerLevel
|
||||
val telegraph = Telegraph(pattern, pattern.area(fight.boss.position(), target), level.gameTime + pattern.telegraphTicks)
|
||||
val telegraph = Telegraph(pattern, pattern.area(fight.boss.position(), target, fight.boss.bbWidth / 2.0), level.gameTime + pattern.telegraphTicks)
|
||||
fight.telegraphs += telegraph
|
||||
val origin = telegraph.area.origin
|
||||
level.playSound(null, origin.x, origin.y, origin.z, SoundEvents.NOTE_BLOCK_BELL.value(), SoundSource.HOSTILE, 1.2f, 0.6f)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package dev.cobblesurvival.boss
|
||||
|
||||
import com.cobblemon.mod.common.api.Priority
|
||||
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemManager
|
||||
import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemProvider
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.pokemon.IVs
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import net.minecraft.network.chat.Component
|
||||
|
||||
/**
|
||||
* Bosses are big and tough, not strong (Adrian, 2026-11-02: Eternatus was too small and too easy; guardians should be
|
||||
* way bigger and tougher; only their defensive stats go up, never Attack or Speed; no single attack may take more
|
||||
* than a quarter of their HP). Every boss (zone guardian, End boss, practice copy) gets:
|
||||
* - its size ([toughen]'s scale, from tools/zone_spawns.py and tools/zone_bosses.py);
|
||||
* - perfect IVs and [EVS] EVs in HP, Defense and Sp. Def, nothing else (510 in all, the games' limit);
|
||||
* - in battle the Guardian's Ward, a Showdown held item no Minecraft item stands for
|
||||
* (`data/cobblesurvival/held_items/guardiansward.js`): Defense and Sp. Def multiplied, and at most a quarter of
|
||||
* its max HP lost to one attack. This provider hands it out ahead of whatever the boss really holds.
|
||||
*/
|
||||
object BossToughness : HeldItemManager {
|
||||
const val WARD = "guardiansward"
|
||||
const val EVS = 170
|
||||
private val DEFENSIVE = listOf(Stats.HP, Stats.DEFENCE, Stats.SPECIAL_DEFENCE)
|
||||
|
||||
fun register() = HeldItemProvider.register(this, Priority.HIGHEST)
|
||||
|
||||
fun isBoss(pokemon: Pokemon): Boolean {
|
||||
val data = pokemon.persistentData
|
||||
return data.contains(Guardians.TAG) || data.contains(EndBoss.TAG) || data.contains(BossFights.TAG)
|
||||
}
|
||||
|
||||
/** Gives [pokemon] its boss [scale] and training; true if anything changed (a boss from before these rules). */
|
||||
fun toughen(pokemon: Pokemon, scale: Float): Boolean {
|
||||
var changed = false
|
||||
if (pokemon.scaleModifier != scale) {
|
||||
pokemon.scaleModifier = scale
|
||||
changed = true
|
||||
}
|
||||
for (stat in DEFENSIVE) {
|
||||
if (pokemon.ivs[stat] != IVs.MAX_VALUE) {
|
||||
pokemon.setIV(stat, IVs.MAX_VALUE)
|
||||
changed = true
|
||||
}
|
||||
if (pokemon.evs[stat] != EVS) {
|
||||
pokemon.setEV(stat, EVS)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/** [toughen]s a boss already in the world, outside battle: at full health and its new size. */
|
||||
fun refresh(entity: PokemonEntity, scale: Float): Boolean {
|
||||
if (entity.isBattling || !toughen(entity.pokemon, scale)) return false
|
||||
entity.pokemon.heal()
|
||||
entity.refreshDimensions()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun showdownId(pokemon: BattlePokemon): String? = WARD.takeIf { isBoss(pokemon.effectedPokemon) }
|
||||
|
||||
override fun nameOf(showdownId: String): Component =
|
||||
Component.translatableWithFallback("item.cobblesurvival.guardian_ward", "Guardian's Ward")
|
||||
|
||||
override fun handleStartInstruction(pokemon: BattlePokemon, battle: PokemonBattle, message: BattleMessage) {}
|
||||
|
||||
override fun handleEndInstruction(pokemon: BattlePokemon, battle: PokemonBattle, message: BattleMessage) {}
|
||||
|
||||
override fun give(pokemon: BattlePokemon, itemID: String) {}
|
||||
|
||||
override fun take(pokemon: BattlePokemon, itemID: String) {}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ object EndBoss {
|
||||
.filter { it !== (boss ?: it) }
|
||||
.forEach { it.discard() }
|
||||
if (boss != null) {
|
||||
BossToughness.refresh(boss, def.scale)
|
||||
if (boss.distanceToSqr(spot.x + 0.5, spot.y.toDouble(), spot.z + 0.5) > LEASH * LEASH) boss.teleportTo(spot.x + 0.5, spot.y.toDouble(), spot.z + 0.5)
|
||||
level.dragonFight?.let { fight ->
|
||||
(bossBar.get(fight) as ServerBossEvent).apply {
|
||||
@@ -111,8 +112,9 @@ object EndBoss {
|
||||
fun spawn(level: ServerLevel): PokemonEntity? {
|
||||
val def = BossDefs.end?.end ?: return null
|
||||
val pokemon = PokemonProperties.parse("${def.species} level=${def.level} uncatchable=yes").create()
|
||||
pokemon.scaleModifier = def.scale
|
||||
pokemon.persistentData.putBoolean(TAG, true)
|
||||
BossToughness.toughen(pokemon, def.scale)
|
||||
pokemon.heal()
|
||||
val entity = PokemonEntity(level, pokemon)
|
||||
val spot = spot(level)
|
||||
entity.moveTo(spot.x + 0.5, spot.y.toDouble(), spot.z + 0.5, 180f, 0f)
|
||||
|
||||
@@ -22,18 +22,22 @@ import net.minecraft.world.level.block.entity.BlockEntityType
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
|
||||
/** A machine's core block; which machine it is comes from its id ([MachineDefs]). It faces the player who placed it. */
|
||||
/**
|
||||
* A machine's core block; which machine it is comes from its id ([MachineDefs]). It faces the player who placed it.
|
||||
* While its machine works it is [ACTIVE]: it glows, its face animates and particles show ([MachineEffects]).
|
||||
*/
|
||||
class MachineBlock(properties: Properties) : BaseEntityBlock(properties) {
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH))
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(ACTIVE, false))
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<out BaseEntityBlock> = CODEC
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING)
|
||||
builder.add(FACING, ACTIVE)
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState =
|
||||
@@ -55,7 +59,7 @@ class MachineBlock(properties: Properties) : BaseEntityBlock(properties) {
|
||||
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity = MachineBlockEntity(pos, state)
|
||||
|
||||
override fun <T : BlockEntity> getTicker(level: Level, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? =
|
||||
if (level.isClientSide) null
|
||||
if (level.isClientSide) createTickerHelper(type, Machines.BLOCK_ENTITY.get(), BlockEntityTicker { tickLevel, pos, tickState, _ -> MachineEffects.clientTick(tickLevel, pos, tickState) })
|
||||
else createTickerHelper(type, Machines.BLOCK_ENTITY.get(), BlockEntityTicker { tickLevel, _, _, core -> core.serverTick(tickLevel as ServerLevel) })
|
||||
|
||||
override fun useWithoutItem(state: BlockState, level: Level, pos: BlockPos, player: Player, hitResult: BlockHitResult): InteractionResult {
|
||||
@@ -71,6 +75,9 @@ class MachineBlock(properties: Properties) : BaseEntityBlock(properties) {
|
||||
|
||||
companion object {
|
||||
val FACING = BlockStateProperties.HORIZONTAL_FACING
|
||||
val ACTIVE: BooleanProperty = BooleanProperty.create("active")
|
||||
/** Light a working core gives off. */
|
||||
const val ACTIVE_LIGHT = 10
|
||||
val CODEC: MapCodec<MachineBlock> = simpleCodec(::MachineBlock)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.Containers
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.entity.BlockEntity
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.phys.AABB
|
||||
@@ -57,6 +58,8 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
/** No Pokémon shown before this game time (the old one is still fainting). */
|
||||
private var showFrom = 0L
|
||||
private var ticks = 0
|
||||
/** The core shows as working ([MachineBlock.ACTIVE]) until this game time; work keeps pushing it on. */
|
||||
private var activeUntil = 0L
|
||||
|
||||
val def: MachineDefs.Def? get() = MachineDefs.byCore[BuiltInRegistries.BLOCK.getKey(blockState.block)]
|
||||
val machineName: Component get() = blockState.block.name
|
||||
@@ -73,9 +76,22 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
if (display?.isAlive != true && level.gameTime >= showFrom) display = MachineDisplay.spawn(level, pokemon, worldPosition, shell!!.chamber, facing())
|
||||
}
|
||||
MachineWork.tick(this, level, def)
|
||||
showWork(level, def)
|
||||
receivedThisTick = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows that the machine works (Adrian, 2026-11-02): the core turns [MachineBlock.ACTIVE] (it glows, its face
|
||||
* animates, particles show) and stays so [ACTIVE_LINGER] ticks after the work stops, so it doesn't flicker; now and
|
||||
* then the Pokémon inside flinches as if hit, without taking damage ([MachineDisplay.flinch]).
|
||||
*/
|
||||
private fun showWork(level: ServerLevel, def: MachineDefs.Def) {
|
||||
if (MachineWork.working(this, level, def)) activeUntil = level.gameTime + ACTIVE_LINGER
|
||||
val active = level.gameTime < activeUntil
|
||||
if (blockState.getValue(MachineBlock.ACTIVE) != active) level.setBlock(worldPosition, blockState.setValue(MachineBlock.ACTIVE, active), Block.UPDATE_CLIENTS)
|
||||
if (active && level.random.nextInt(FLINCH_ONE_IN) == 0) display?.takeIf { it.isAlive }?.let { MachineDisplay.flinch(level, it) }
|
||||
}
|
||||
|
||||
private fun refreshShell(level: ServerLevel, def: MachineDefs.Def) {
|
||||
val min = worldPosition.offset(-SHELL_REACH, -SHELL_REACH, -SHELL_REACH)
|
||||
val max = worldPosition.offset(SHELL_REACH, SHELL_REACH, SHELL_REACH)
|
||||
@@ -268,6 +284,10 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
private const val SHELL_REACH = 2
|
||||
/** How near the core a sent-out Pokémon must be to walk in. */
|
||||
private const val REACH = 16.0
|
||||
/** Ticks a core keeps showing as working after its work stops. */
|
||||
private const val ACTIVE_LINGER = 40
|
||||
/** While working, the Pokémon inside flinches on average once in this many ticks (15 s). */
|
||||
private const val FLINCH_ONE_IN = 300
|
||||
|
||||
fun tell(player: ServerPlayer, message: Component) = player.sendSystemMessage(message)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,18 @@ object MachineDisplay {
|
||||
fading += Fading(entity, level.gameTime + FAINT_TICKS)
|
||||
}
|
||||
|
||||
/**
|
||||
* The shown Pokémon flinches as if hit (Adrian, 2026-11-02): it flashes red as a hurt entity does and a hit sounds,
|
||||
* but it takes no damage (the display copy can't, and the Pokémon in the core is untouched).
|
||||
*/
|
||||
fun flinch(level: ServerLevel, entity: PokemonEntity) {
|
||||
level.broadcastDamageEvent(entity, level.damageSources().generic())
|
||||
level.playSound(null, entity.x, entity.y, entity.z, HIT, SoundSource.NEUTRAL, 0.7f, 0.9f + level.random.nextFloat() * 0.2f)
|
||||
}
|
||||
|
||||
/** Cobblemon's own sound for a Pokémon taking a hit in battle. */
|
||||
private val HIT = SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath("cobblemon", "impact.normal"))
|
||||
|
||||
fun isDisplay(entity: Entity?): Boolean = entity is PokemonEntity && entity.pokemon.persistentData.contains(TAG)
|
||||
|
||||
fun spawn(level: ServerLevel, pokemon: Pokemon, core: BlockPos, chamber: BlockPos, facing: Direction): PokemonEntity {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package dev.cobblesurvival.machine
|
||||
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.particles.ParticleOptions
|
||||
import net.minecraft.core.particles.ParticleTypes
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import net.minecraft.sounds.SoundEvents
|
||||
import net.minecraft.sounds.SoundSource
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* What a working machine looks like (Adrian, 2026-11-02: particles around the Pokémon and an animated core show that
|
||||
* something is happening, like the fire in a furnace). Client side, from the core's [MachineBlock.ACTIVE] state:
|
||||
* particles rise around the Pokémon in the chamber and flicker at the core's face, now and then with a sound. The core's
|
||||
* animated face and its glow come with the active state itself (models from tools/machine_assets.py, light in [Machines]).
|
||||
*/
|
||||
object MachineEffects {
|
||||
/** [chamber] around the Pokémon, [face] at the core's front (with [smoke] beside it), [sound] now and then. */
|
||||
private class Look(val chamber: ParticleOptions, val face: ParticleOptions, val smoke: ParticleOptions?, val sound: SoundEvent?, val volume: Float = 0.6f)
|
||||
|
||||
private val LOOKS = mapOf(
|
||||
"heat_core" to Look(ParticleTypes.FLAME, ParticleTypes.FLAME, ParticleTypes.SMOKE, SoundEvents.FURNACE_FIRE_CRACKLE, 1f),
|
||||
"dynamo_core" to Look(ParticleTypes.ELECTRIC_SPARK, ParticleTypes.ELECTRIC_SPARK, null, SoundEvents.COPPER_BULB_TURN_ON, 0.3f),
|
||||
"synthesis_core" to Look(ParticleTypes.ENCHANT, ParticleTypes.HAPPY_VILLAGER, null, SoundEvents.BEACON_AMBIENT, 0.3f),
|
||||
"altar_core" to Look(ParticleTypes.SOUL, ParticleTypes.SOUL_FIRE_FLAME, ParticleTypes.SMOKE, SoundEvents.SCULK_CATALYST_BLOOM, 0.5f),
|
||||
"rebirth_core" to Look(ParticleTypes.END_ROD, ParticleTypes.ENCHANT, null, SoundEvents.AMETHYST_BLOCK_CHIME),
|
||||
)
|
||||
|
||||
/** Per tick: chance of a particle around the Pokémon, at the face, and of a sound. */
|
||||
private const val CHAMBER_CHANCE = 0.35
|
||||
private const val FACE_CHANCE = 0.2
|
||||
private const val SOUND_CHANCE = 0.02
|
||||
|
||||
fun clientTick(level: Level, pos: BlockPos, state: BlockState) {
|
||||
if (!state.getValue(MachineBlock.ACTIVE)) return
|
||||
val look = LOOKS[BuiltInRegistries.BLOCK.getKey(state.block).path] ?: return
|
||||
val random = level.random
|
||||
val facing = state.getValue(MachineBlock.FACING)
|
||||
val chamber = pos.relative(facing.opposite)
|
||||
if (random.nextDouble() < CHAMBER_CHANCE) {
|
||||
val angle = random.nextDouble() * 2 * PI
|
||||
val radius = 0.35 + random.nextDouble() * 0.15
|
||||
val x = chamber.x + 0.5 + cos(angle) * radius
|
||||
val y = chamber.y + 0.1 + random.nextDouble() * 0.8
|
||||
val z = chamber.z + 0.5 + sin(angle) * radius
|
||||
// Enchanting glyphs fly from where they start to the given offset's origin: here, into the Pokémon.
|
||||
if (look.chamber == ParticleTypes.ENCHANT) level.addParticle(look.chamber, chamber.x + 0.5, chamber.y + 0.5, chamber.z + 0.5, x - chamber.x - 0.5, y - chamber.y - 0.5, z - chamber.z - 0.5)
|
||||
else level.addParticle(look.chamber, x, y, z, 0.0, 0.02, 0.0)
|
||||
}
|
||||
if (random.nextDouble() < FACE_CHANCE) {
|
||||
// Like a lit furnace: low on the core's front, a little in front of it.
|
||||
val across = random.nextDouble() * 0.6 - 0.3
|
||||
val x = pos.x + 0.5 + facing.stepX * 0.52 + facing.stepZ * across
|
||||
val y = pos.y + 0.1 + random.nextDouble() * 0.4
|
||||
val z = pos.z + 0.5 + facing.stepZ * 0.52 + facing.stepX * across
|
||||
level.addParticle(look.face, x, y, z, 0.0, 0.0, 0.0)
|
||||
look.smoke?.let { level.addParticle(it, x, y + 0.2, z, 0.0, 0.0, 0.0) }
|
||||
}
|
||||
if (look.sound != null && random.nextDouble() < SOUND_CHANCE) {
|
||||
level.playLocalSound(pos, look.sound, SoundSource.BLOCKS, look.volume, 0.9f + random.nextFloat() * 0.2f, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,23 @@ object MachineWork {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the machine is doing something this tick, for how it looks ([MachineBlockEntity.showWork]): a furnace it
|
||||
* heats has something to cook; the generator isn't full; the synthesizer gets energy; a rebirth runs; the altar
|
||||
* holds a Pokémon.
|
||||
*/
|
||||
fun working(core: MachineBlockEntity, level: ServerLevel, def: MachineDefs.Def): Boolean {
|
||||
val pokemon = core.pokemon ?: return false
|
||||
return when (def.kind) {
|
||||
MachineDefs.Kind.FURNACE -> core.furnaces.take(def.furnaces!!.count(pokemon.level))
|
||||
.any { (level.getBlockEntity(it) as? AbstractFurnaceBlockEntity)?.getItem(0)?.isEmpty == false }
|
||||
MachineDefs.Kind.GENERATOR -> core.energy < def.energy!!.capacity
|
||||
MachineDefs.Kind.SYNTHESIS -> core.receivedThisTick > 0
|
||||
MachineDefs.Kind.REBIRTH -> core.rebirthAt > 0
|
||||
MachineDefs.Kind.ALTAR -> true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Large Furnace -------------------------------------------------------------------------------------------
|
||||
|
||||
private fun heat(core: MachineBlockEntity, level: ServerLevel, rules: MachineDefs.FurnaceRules, pokemon: Pokemon) {
|
||||
|
||||
@@ -41,7 +41,8 @@ object Machines {
|
||||
private fun core(name: String, color: MapColor, sound: SoundType): DeferredBlock<MachineBlock> = BLOCKS.registerBlock(
|
||||
name, ::MachineBlock, BlockBehaviour.Properties.of().mapColor(color).strength(3.5f).requiresCorrectToolForDrops().sound(sound)
|
||||
// See-through, so the Pokémon in the chamber behind the core shows (Adrian, 2026-11-02).
|
||||
.noOcclusion().isViewBlocking { _, _, _ -> false }.isSuffocating { _, _, _ -> false },
|
||||
.noOcclusion().isViewBlocking { _, _, _ -> false }.isSuffocating { _, _, _ -> false }
|
||||
.lightLevel { if (it.getValue(MachineBlock.ACTIVE)) MachineBlock.ACTIVE_LIGHT else 0 },
|
||||
)
|
||||
|
||||
fun register(bus: IEventBus) {
|
||||
|
||||
@@ -2,10 +2,14 @@ package dev.cobblesurvival.selftest
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.MoveActionResponse
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.util.getBattleState
|
||||
import dev.cobblesurvival.boss.Area
|
||||
import dev.cobblesurvival.boss.BossDefs
|
||||
import dev.cobblesurvival.boss.BossFights
|
||||
import dev.cobblesurvival.boss.BossToughness
|
||||
import dev.cobblesurvival.boss.EndBoss
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import net.minecraft.core.BlockPos
|
||||
@@ -33,8 +37,13 @@ object BossTests {
|
||||
SelfTestCase("boss_arena_leaving_loses", 400, ::arena),
|
||||
SelfTestCase("end_boss_replaces_the_dragon", 200, ::endBoss),
|
||||
SelfTestCase("practice_bosses_spawn", 20, ::practice),
|
||||
SelfTestCase("boss_takes_a_quarter_per_attack", 1200, ::damageCap),
|
||||
)
|
||||
|
||||
/** Guardians stand at least this tall (Morpeko, the smallest, reaches about 5 blocks), the End boss [END_HEIGHT]. */
|
||||
private const val GUARDIAN_HEIGHT = 4.5f
|
||||
private const val END_HEIGHT = 12f
|
||||
|
||||
/** `/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") {
|
||||
@@ -42,11 +51,51 @@ object BossTests {
|
||||
?: 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" }
|
||||
// Adrian, 2026-11-02: way bigger, tougher but not stronger.
|
||||
val tall = if (which == "end") END_HEIGHT else GUARDIAN_HEIGHT
|
||||
s.check(entity.bbHeight >= tall) { "the $which boss is only %.1f blocks tall".format(entity.bbHeight) }
|
||||
val p = entity.pokemon
|
||||
val defensive = listOf(Stats.HP, Stats.DEFENCE, Stats.SPECIAL_DEFENCE)
|
||||
s.check(defensive.all { p.ivs[it] == 31 && p.evs[it] == BossToughness.EVS }) { "the $which boss lacks its HP/Defense/Sp. Def training" }
|
||||
s.check(listOf(Stats.ATTACK, Stats.SPECIAL_ATTACK, Stats.SPEED).all { p.evs[it] == 0 }) { "the $which boss has offensive EVs" }
|
||||
s.check(p.currentHealth == p.maxHealth && BossToughness.isBoss(p)) { "the $which boss isn't at full health or isn't a boss" }
|
||||
entity.discard()
|
||||
}
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/**
|
||||
* The Guardian's Ward (Adrian, 2026-11-02: no single attack takes more than a quarter of a boss's HP): a Lv 100
|
||||
* Garchomp's Bullet Seed, every hit enough to knock out a Lv 15 boss, takes a quarter at most per use.
|
||||
*/
|
||||
private fun damageCap(s: Scenario) {
|
||||
val player = s.player()
|
||||
s.give(player, s.pokemon("garchomp", 100, "bulletseed"))
|
||||
val bossPokemon = s.pokemon("morpeko", 15, "splash")
|
||||
bossPokemon.persistentData.putString(BossFights.TAG, TEST_BOSS)
|
||||
val boss = s.wild(bossPokemon, s.at(7.5, 1.0, 3.5))
|
||||
val battle = { BattleRegistry.getBattleByParticipatingPlayer(player) }
|
||||
s.afterTest { battle()?.end() }
|
||||
// Two attacks, then the battle waits for a third choice that never comes.
|
||||
var attacks = 0
|
||||
s.answerBattles(player) { actor, _ ->
|
||||
if (attacks == 2) null else MoveActionResponse("bulletseed", actor.getSide().getOppositeSide().activePokemon.first().getPNX()).also { attacks++ }
|
||||
}
|
||||
s.startWildBattle(player, boss)
|
||||
.thenWaitUntil {
|
||||
val b = battle()
|
||||
val waiting = player.getBattleState()?.second?.let { it.mustChoose && it.request != null } == true
|
||||
s.check(b == null || (attacks == 2 && b.turn >= 3 && waiting)) { "two attacks aren't done yet: ${s.battleState(player)}" }
|
||||
}
|
||||
.thenExecute {
|
||||
val max = boss.pokemon.maxHealth
|
||||
val hp = boss.pokemon.currentHealth
|
||||
s.check(hp in (max - 2 * (max / 4))..<max) { "after two attacks that each could knock it out, the boss has $hp of $max HP (a quarter at most per attack)" }
|
||||
battle()?.end()
|
||||
}
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
private fun implemented(species: String) = PokemonSpecies.getByName(species)?.implemented == true
|
||||
|
||||
private fun coverage(s: Scenario) {
|
||||
|
||||
@@ -247,7 +247,17 @@ object MachineTests {
|
||||
level.setBlockAndUpdate(furnace.below(), Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.DOWN))
|
||||
(level.getBlockEntity(furnace.above()) as HopperBlockEntity).setItem(0, ItemStack(Items.RAW_IRON, 4))
|
||||
val output = level.getBlockEntity(furnace.below()) as HopperBlockEntity
|
||||
val active = { core.blockState.getValue(MachineBlock.ACTIVE) }
|
||||
s.helper.startSequence()
|
||||
// Adrian, 2026-11-02: a working machine shows it (the core glows and animates, particles show).
|
||||
.thenWaitUntil { s.check(active()) { "the Large Furnace's core doesn't show that it works" } }
|
||||
.thenExecute {
|
||||
// The Pokémon inside flinches as if hit, but takes no damage.
|
||||
val shown = displays(level, core.blockPos).firstOrNull() ?: throw GameTestAssertException("no Pokémon shown in the chamber")
|
||||
MachineDisplay.flinch(level, shown)
|
||||
val inside = core.pokemon!!
|
||||
s.check(shown.isAlive && inside.currentHealth == inside.maxHealth) { "a flinch hurt the Pokémon (${inside.currentHealth}/${inside.maxHealth})" }
|
||||
}
|
||||
.thenWaitUntil {
|
||||
val ingots = (0 until output.containerSize).sumOf { output.getItem(it).takeIf { s -> s.`is`(Items.IRON_INGOT) }?.count ?: 0 }
|
||||
s.check(ingots >= 4) { "the hopper under the furnace has $ingots iron ingots, not 4" }
|
||||
@@ -256,6 +266,7 @@ object MachineTests {
|
||||
val fuel = (level.getBlockEntity(furnace) as AbstractFurnaceBlockEntity).getItem(1)
|
||||
s.check(fuel.isEmpty) { "the furnace got fuel: $fuel" }
|
||||
}
|
||||
.thenWaitUntil { s.check(!active()) { "the Large Furnace's core still shows work with nothing left to cook" } }
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonElement
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.boss.BossToughness
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.HolderLookup
|
||||
@@ -31,6 +32,7 @@ import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent
|
||||
import java.util.UUID
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* One Alpha guardian per zone (roadmap Phase 3): a big, uncatchable Pokémon standing at the zone's sample point.
|
||||
@@ -48,6 +50,10 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
private const val NEAR = 96.0
|
||||
private const val LEASH = 24.0
|
||||
private const val TICKS_PER_HOUR = 72_000L
|
||||
/** How far (blocks, each way) a cave guardian's spot may lie from the zone's sample point. */
|
||||
private const val CAVE_SEARCH = 24
|
||||
/** A cave spot needs at least this much air above it, even when no spot fits the whole guardian. */
|
||||
private const val MIN_ROOM = 3
|
||||
|
||||
class Definition(
|
||||
val zone: String,
|
||||
@@ -94,6 +100,11 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
val entry = state.entry(zone)
|
||||
val guardian = entry.entity?.let { level.getEntity(it) as? PokemonEntity }?.takeIf { it.isAlive }
|
||||
if (guardian != null) {
|
||||
// A guardian from before it grew (Adrian, 2026-11-02): grown, it may need a roomier cave spot.
|
||||
if (BossToughness.refresh(guardian, definition.scale) && sample.y != null) {
|
||||
spotFor(level, zone, sample, guardian.bbHeight)?.let { entry.spot = it }
|
||||
entry.spot?.let { guardian.teleportTo(it.x + 0.5, it.y.toDouble(), it.z + 0.5) }
|
||||
}
|
||||
entry.spot?.let { leash(guardian, it) }
|
||||
continue
|
||||
}
|
||||
@@ -103,21 +114,29 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
// The old guardian may still be loading from disk with its chunk; wait for that.
|
||||
val chunk = ChunkPos(SectionPos.blockToSectionCoord(sample.x), SectionPos.blockToSectionCoord(sample.z))
|
||||
if (!level.areEntitiesLoaded(chunk.toLong())) continue
|
||||
val spot = entry.spot ?: spotFor(level, zone, sample)?.also { entry.spot = it } ?: continue
|
||||
val spawned = spawn(level, definition, spot) ?: continue
|
||||
val entity = create(level, definition)
|
||||
// A spot picked for a smaller guardian is kept only while this one still fits there.
|
||||
val spot = entry.spot?.takeIf { sample.y == null || room(level, it, entity.bbHeight) >= ceil(entity.bbHeight) }
|
||||
?: spotFor(level, zone, sample, entity.bbHeight)?.also { entry.spot = it } ?: continue
|
||||
val spawned = spawn(level, entity, spot) ?: continue
|
||||
entry.entity = spawned.uuid
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawn(level: ServerLevel, definition: Definition, spot: BlockPos): PokemonEntity? {
|
||||
private fun create(level: ServerLevel, definition: Definition): PokemonEntity {
|
||||
val pokemon = PokemonProperties.parse(definition.pokemon).create()
|
||||
pokemon.scaleModifier = definition.scale
|
||||
pokemon.persistentData.putString(TAG, definition.zone)
|
||||
val entity = PokemonEntity(level, pokemon)
|
||||
BossToughness.toughen(pokemon, definition.scale)
|
||||
pokemon.heal()
|
||||
return PokemonEntity(level, pokemon)
|
||||
}
|
||||
|
||||
private fun spawn(level: ServerLevel, entity: PokemonEntity, spot: BlockPos): PokemonEntity? {
|
||||
val zone = entity.pokemon.persistentData.getString(TAG)
|
||||
entity.moveTo(spot.x + 0.5, spot.y.toDouble(), spot.z + 0.5, level.random.nextFloat() * 360f, 0f)
|
||||
entity.setPersistenceRequired()
|
||||
if (!level.addFreshEntity(entity)) return null
|
||||
CobbleSurvival.LOGGER.info("Guardian of {} appeared at {}", definition.zone, spot.toShortString())
|
||||
CobbleSurvival.LOGGER.info("Guardian of {} appeared at {}, {} blocks tall", zone, spot.toShortString(), "%.1f".format(entity.bbHeight))
|
||||
return entity
|
||||
}
|
||||
|
||||
@@ -128,18 +147,33 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
}
|
||||
}
|
||||
|
||||
/** Where the guardian stands: the surface at the sample point, or for a cave zone open floor near it. */
|
||||
private fun spotFor(level: ServerLevel, zone: String, sample: ZoneSample): BlockPos? {
|
||||
/** Air above [pos], counted up to the [height] a guardian needs. */
|
||||
private fun room(level: ServerLevel, pos: BlockPos, height: Float): Int {
|
||||
val need = ceil(height).toInt()
|
||||
return (0 until need).firstOrNull { !level.getBlockState(pos.above(it)).isAir } ?: need
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the guardian stands: the surface at the sample point, or for a cave zone open floor near it with room for
|
||||
* its [height]; when no floor has that much, the roomiest one found (at least [MIN_ROOM]).
|
||||
*/
|
||||
private fun spotFor(level: ServerLevel, zone: String, sample: ZoneSample, height: Float): BlockPos? {
|
||||
val caveY = sample.y ?: return BlockPos(sample.x, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z), sample.z)
|
||||
for (radius in 0..16 step 2) for (dx in -radius..radius) for (dz in -radius..radius) {
|
||||
var best: BlockPos? = null
|
||||
var bestRoom = MIN_ROOM - 1
|
||||
for (radius in 0..CAVE_SEARCH step 2) for (dx in -radius..radius) for (dz in -radius..radius) {
|
||||
if (maxOf(abs(dx), abs(dz)) != radius) continue
|
||||
for (dy in 0..24) for (sign in intArrayOf(1, -1)) {
|
||||
val pos = BlockPos(sample.x + dx, caveY + sign * dy, sample.z + dz)
|
||||
val open = (0 until 3).all { level.getBlockState(pos.above(it)).isAir }
|
||||
if (open && level.getBlockState(pos.below()).isSolid && Zones.at(level, pos)?.id == zone) return pos
|
||||
if (!level.getBlockState(pos).isAir || !level.getBlockState(pos.below()).isSolid) continue
|
||||
val room = room(level, pos, height)
|
||||
if (room <= bestRoom || Zones.at(level, pos)?.id != zone) continue
|
||||
best = pos
|
||||
bestRoom = room
|
||||
if (room >= ceil(height)) return pos
|
||||
}
|
||||
}
|
||||
return null
|
||||
return best
|
||||
}
|
||||
|
||||
/** A second copy of a zone's guardian (e.g. one that wandered into a chunk loading late) never joins. */
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": {
|
||||
"active=false,facing=north": {
|
||||
"model": "cobblesurvival:block/altar_core"
|
||||
},
|
||||
"facing=east": {
|
||||
"active=false,facing=east": {
|
||||
"model": "cobblesurvival:block/altar_core",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south": {
|
||||
"active=false,facing=south": {
|
||||
"model": "cobblesurvival:block/altar_core",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"active=false,facing=west": {
|
||||
"model": "cobblesurvival:block/altar_core",
|
||||
"y": 270
|
||||
},
|
||||
"active=true,facing=north": {
|
||||
"model": "cobblesurvival:block/altar_core_on"
|
||||
},
|
||||
"active=true,facing=east": {
|
||||
"model": "cobblesurvival:block/altar_core_on",
|
||||
"y": 90
|
||||
},
|
||||
"active=true,facing=south": {
|
||||
"model": "cobblesurvival:block/altar_core_on",
|
||||
"y": 180
|
||||
},
|
||||
"active=true,facing=west": {
|
||||
"model": "cobblesurvival:block/altar_core_on",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": {
|
||||
"active=false,facing=north": {
|
||||
"model": "cobblesurvival:block/dynamo_core"
|
||||
},
|
||||
"facing=east": {
|
||||
"active=false,facing=east": {
|
||||
"model": "cobblesurvival:block/dynamo_core",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south": {
|
||||
"active=false,facing=south": {
|
||||
"model": "cobblesurvival:block/dynamo_core",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"active=false,facing=west": {
|
||||
"model": "cobblesurvival:block/dynamo_core",
|
||||
"y": 270
|
||||
},
|
||||
"active=true,facing=north": {
|
||||
"model": "cobblesurvival:block/dynamo_core_on"
|
||||
},
|
||||
"active=true,facing=east": {
|
||||
"model": "cobblesurvival:block/dynamo_core_on",
|
||||
"y": 90
|
||||
},
|
||||
"active=true,facing=south": {
|
||||
"model": "cobblesurvival:block/dynamo_core_on",
|
||||
"y": 180
|
||||
},
|
||||
"active=true,facing=west": {
|
||||
"model": "cobblesurvival:block/dynamo_core_on",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": {
|
||||
"active=false,facing=north": {
|
||||
"model": "cobblesurvival:block/heat_core"
|
||||
},
|
||||
"facing=east": {
|
||||
"active=false,facing=east": {
|
||||
"model": "cobblesurvival:block/heat_core",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south": {
|
||||
"active=false,facing=south": {
|
||||
"model": "cobblesurvival:block/heat_core",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"active=false,facing=west": {
|
||||
"model": "cobblesurvival:block/heat_core",
|
||||
"y": 270
|
||||
},
|
||||
"active=true,facing=north": {
|
||||
"model": "cobblesurvival:block/heat_core_on"
|
||||
},
|
||||
"active=true,facing=east": {
|
||||
"model": "cobblesurvival:block/heat_core_on",
|
||||
"y": 90
|
||||
},
|
||||
"active=true,facing=south": {
|
||||
"model": "cobblesurvival:block/heat_core_on",
|
||||
"y": 180
|
||||
},
|
||||
"active=true,facing=west": {
|
||||
"model": "cobblesurvival:block/heat_core_on",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": {
|
||||
"active=false,facing=north": {
|
||||
"model": "cobblesurvival:block/rebirth_core"
|
||||
},
|
||||
"facing=east": {
|
||||
"active=false,facing=east": {
|
||||
"model": "cobblesurvival:block/rebirth_core",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south": {
|
||||
"active=false,facing=south": {
|
||||
"model": "cobblesurvival:block/rebirth_core",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"active=false,facing=west": {
|
||||
"model": "cobblesurvival:block/rebirth_core",
|
||||
"y": 270
|
||||
},
|
||||
"active=true,facing=north": {
|
||||
"model": "cobblesurvival:block/rebirth_core_on"
|
||||
},
|
||||
"active=true,facing=east": {
|
||||
"model": "cobblesurvival:block/rebirth_core_on",
|
||||
"y": 90
|
||||
},
|
||||
"active=true,facing=south": {
|
||||
"model": "cobblesurvival:block/rebirth_core_on",
|
||||
"y": 180
|
||||
},
|
||||
"active=true,facing=west": {
|
||||
"model": "cobblesurvival:block/rebirth_core_on",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": {
|
||||
"active=false,facing=north": {
|
||||
"model": "cobblesurvival:block/synthesis_core"
|
||||
},
|
||||
"facing=east": {
|
||||
"active=false,facing=east": {
|
||||
"model": "cobblesurvival:block/synthesis_core",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south": {
|
||||
"active=false,facing=south": {
|
||||
"model": "cobblesurvival:block/synthesis_core",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"active=false,facing=west": {
|
||||
"model": "cobblesurvival:block/synthesis_core",
|
||||
"y": 270
|
||||
},
|
||||
"active=true,facing=north": {
|
||||
"model": "cobblesurvival:block/synthesis_core_on"
|
||||
},
|
||||
"active=true,facing=east": {
|
||||
"model": "cobblesurvival:block/synthesis_core_on",
|
||||
"y": 90
|
||||
},
|
||||
"active=true,facing=south": {
|
||||
"model": "cobblesurvival:block/synthesis_core_on",
|
||||
"y": 180
|
||||
},
|
||||
"active=true,facing=west": {
|
||||
"model": "cobblesurvival:block/synthesis_core_on",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/altar_core_top",
|
||||
"front": "cobblesurvival:block/altar_core_front_on",
|
||||
"side": "cobblesurvival:block/altar_core_side"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/dynamo_core_top",
|
||||
"front": "cobblesurvival:block/dynamo_core_front_on",
|
||||
"side": "cobblesurvival:block/dynamo_core_side"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/heat_core_top",
|
||||
"front": "cobblesurvival:block/heat_core_front_on",
|
||||
"side": "cobblesurvival:block/heat_core_side"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/rebirth_core_top",
|
||||
"front": "cobblesurvival:block/rebirth_core_front_on",
|
||||
"side": "cobblesurvival:block/rebirth_core_side"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/synthesis_core_top",
|
||||
"front": "cobblesurvival:block/synthesis_core_front_on",
|
||||
"side": "cobblesurvival:block/synthesis_core_side"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"animation": {
|
||||
"frametime": 3,
|
||||
"interpolate": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1010 B |
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"animation": {
|
||||
"frametime": 3,
|
||||
"interpolate": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"animation": {
|
||||
"frametime": 3,
|
||||
"interpolate": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 996 B |
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"animation": {
|
||||
"frametime": 3,
|
||||
"interpolate": true
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 928 B |
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"animation": {
|
||||
"frametime": 3,
|
||||
"interpolate": true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user