Phase 6 done: see-through cores, faint on sacrifice and rebirth; Phase 7 design
Adrian (2026-11-02): the cores should be transparent so the Pokémon shows; the Pokémon's death sound when a sacrifice is made or a rebirth ends; then start the next phase (Phase 6 approved). - Cores drawn as a solid frame around tinted glass, rendered translucent, no occlusion (tools/machine_assets.py). - The shown Pokémon plays Cobblemon's faint animation and its cry pitched down (Cobblemon has no death sounds), vanishes after 2 s; a reborn Pokémon then shows in its new form. Tests check the new form. - Phase 7 decisions (every guardian gets area attacks, Groudon for the Basalt Furnace, Giratina replaces the Ender Dragon, dying or leaving the arena loses) and design in docs/design.md; README status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A free NeoForge 1.21.1 modpack on Cobblemon 1.8: one large island with soft-gated, Palworld/ARK-style progression, plus a companion mod for the mechanics no addon covers. The plan is in [docs/roadmap.md](docs/roadmap.md); decisions taken so far are in [docs/design.md](docs/design.md).
|
A free NeoForge 1.21.1 modpack on Cobblemon 1.8: one large island with soft-gated, Palworld/ARK-style progression, plus a companion mod for the mechanics no addon covers. The plan is in [docs/roadmap.md](docs/roadmap.md); decisions taken so far are in [docs/design.md](docs/design.md).
|
||||||
|
|
||||||
**Status:** Phases 0–5 approved (2026-10-31); Phase 6 (Pokémon machines) in progress. Checks per phase: `docs/checklists/`.
|
**Status:** Phases 0–6 approved (2026-11-02); Phase 7 (boss battles with area attacks) in progress. Checks per phase: `docs/checklists/`.
|
||||||
|
|
||||||
## What's in the pack
|
## What's in the pack
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ Elsewhere, creative mode gives the cores in the Functional Blocks tab; each has
|
|||||||
|
|
||||||
- [ ] Use a bare core (third row): ghost blocks show its shell for ten seconds, and chat lists what's missing. Place a new core anywhere: the same.
|
- [ ] Use a bare core (third row): ghost blocks show its shell for ten seconds, and chat lists what's missing. Place a new core anywhere: the same.
|
||||||
- [ ] Hover a core in the inventory: the tooltip says what it does and what its shell is made of.
|
- [ ] Hover a core in the inventory: the tooltip says what it does and what its shell is made of.
|
||||||
|
- [ ] Look at a working machine from the front: the core is see-through (frame and tinted glass), the Pokémon shows behind it.
|
||||||
|
- [ ] Sacrifice a Pokémon, or finish a rebirth: the Pokémon in the chamber faints with its cry pitched down, then disappears (a reborn one comes back in its new form).
|
||||||
|
|
||||||
- [ ] Build a Heat Core shell of bricks with glass in a side window, send out a Fire type and use the core: the Pokémon stands in the chamber, small enough to fit, and doesn't move. **Does it look right?** (the roadmap asks you to confirm this)
|
- [ ] Build a Heat Core shell of bricks with glass in a side window, send out a Fire type and use the core: the Pokémon stands in the chamber, small enough to fit, and doesn't move. **Does it look right?** (the roadmap asks you to confirm this)
|
||||||
- [ ] Put a furnace against the outside of that shell, a hopper with raw iron above it and a hopper with a chest below: the iron cooks without fuel, faster with a stronger Pokémon (speed doubled on 2026-11-02: 7.4× at Lv 50), and ends in the chest.
|
- [ ] Put a furnace against the outside of that shell, a hopper with raw iron above it and a hopper with a chest below: the iron cooks without fuel, faster with a stronger Pokémon (speed doubled on 2026-11-02: 7.4× at Lv 50), and ends in the chest.
|
||||||
@@ -41,5 +43,7 @@ Elsewhere, creative mode gives the cores in the Functional Blocks tab; each has
|
|||||||
|
|
||||||
## Ask Adrian
|
## Ask Adrian
|
||||||
|
|
||||||
|
**Phase 6 approved 2026-11-02** (after the see-through cores and the faint: "After that, you can begin with the next phase"). Numbers, casings and recipes stay placeholders.
|
||||||
|
|
||||||
- Does the Pokémon in the chamber look right (size, place, facing out through the core's side)?
|
- Does the Pokémon in the chamber look right (size, place, facing out through the core's side)?
|
||||||
- All numbers, casings and recipes are placeholders (`docs/balance.md`).
|
- All numbers, casings and recipes are placeholders (`docs/balance.md`).
|
||||||
@@ -245,7 +245,7 @@ Adrian's choices: the Large Furnace has a Fire Pokémon heat real furnaces, and
|
|||||||
|
|
||||||
**A Pokémon goes in** when its owner sends it out near the core and uses the core with an empty hand: the nearest of their sent-out Pokémon that fits walks in (it leaves the party and its data is kept in the core). Using the core again takes it out, back to the party or else the PC. Sneak-use shows the machine's state. Breaking the core or any part of the shell sends the Pokémon back to its owner, online or not. The core is the only place the Pokémon exists while inside, so nothing can be duplicated by breaking and inserting at once.
|
**A Pokémon goes in** when its owner sends it out near the core and uses the core with an empty hand: the nearest of their sent-out Pokémon that fits walks in (it leaves the party and its data is kept in the core). Using the core again takes it out, back to the party or else the PC. Sneak-use shows the machine's state. Breaking the core or any part of the shell sends the Pokémon back to its owner, online or not. The core is the only place the Pokémon exists while inside, so nothing can be duplicated by breaking and inserting at once.
|
||||||
|
|
||||||
**What's shown** is a real Cobblemon Pokémon entity, a copy, standing in the chamber: scaled to fit, no AI, invulnerable, silent, uncatchable, and it can't be battled or used. It's never kept in the world save: one loaded from disk is dropped, and the core puts a fresh one there.
|
**What's shown** is a real Cobblemon Pokémon entity, a copy, standing in the chamber: scaled to fit, no AI, invulnerable, silent, uncatchable, and it can't be battled or used. It's never kept in the world save: one loaded from disk is dropped, and the core puts a fresh one there. The cores are see-through (Adrian, 2026-11-02): a solid frame around tinted glass, rendered translucent, so the Pokémon shows through the core as well as through glass windows. When a sacrifice is made or a rebirth ends, the shown Pokémon plays Cobblemon's faint animation and its cry pitched down (Cobblemon has no separate death sounds; the games pitch the cry down when a Pokémon faints), and vanishes two seconds later; a reborn Pokémon then appears in its new form.
|
||||||
|
|
||||||
**Large Furnace.** Furnaces, blast furnaces and smokers (`#cobblesurvival:machine_furnaces`) standing against the outside of the shell burn without fuel while they have something to cook, and cook faster. They stay ordinary vanilla blocks, so their screens, recipes and hoppers work as always (input from above, output from below). How many are heated and how fast depend on the Pokémon's level and Special Attack (numbers in the data file and `docs/balance.md`).
|
**Large Furnace.** Furnaces, blast furnaces and smokers (`#cobblesurvival:machine_furnaces`) standing against the outside of the shell burn without fuel while they have something to cook, and cook faster. They stay ordinary vanilla blocks, so their screens, recipes and hoppers work as always (input from above, output from below). How many are heated and how fast depend on the Pokémon's level and Special Attack (numbers in the data file and `docs/balance.md`).
|
||||||
|
|
||||||
@@ -263,6 +263,18 @@ Self-tests (`MachineTests`): in and out, wrong Pokémon refused, broken shell an
|
|||||||
|
|
||||||
**Generator.** The Dynamo Core stores Forge Energy and pushes it to whatever touches the core and takes energy (cables, machines from other mods); its output per tick depends on the Pokémon's level and Special Attack, in line with Poke Power's 50 FE per level.
|
**Generator.** The Dynamo Core stores Forge Energy and pushes it to whatever touches the core and takes energy (cables, machines from other mods); its output per tick depends on the Pokémon's level and Special Attack, in line with Poke Power's 50 FE per level.
|
||||||
|
|
||||||
|
## Phase 7: boss battles with area attacks (Adrian, 2026-11-02)
|
||||||
|
|
||||||
|
Adrian's choices: every zone guardian gets area attacks (more and harder in harder zones); the Basalt Furnace's boss is **Groudon**; **Giratina** replaces the Ender Dragon; dying or leaving the arena loses the fight; area attacks hit only the player (the roadmap's default).
|
||||||
|
|
||||||
|
**Bosses are data**, `datapacks/core/data/cobblesurvival/bosses/<id>.json`, written by `tools/zone_bosses.py` from a tier table per zone band: which fight it is (a zone's guardian, or the End boss), the arena radius, and the attack patterns. A pattern has a shape (circle, ring, line, cone) and its size, where it's aimed (at the player, or around the boss), a telegraph time, damage, an optional effect (fire: set alight; ice: slowed and freezing) and a trigger (every so many seconds, every so many turns, or once when the boss's HP falls below a share).
|
||||||
|
|
||||||
|
**Runtime** (companion mod). A battle between players and a boss Pokémon becomes a boss fight. While it lasts, due patterns telegraph with particles on the ground where they'll land, then hit the battling players standing inside; everything stops when the battle ends. The arena is a circle around where the boss stood when the fight began: a player who leaves it, or dies, loses the fight (the battle ends, the boss stays, nothing drops). Cobblemon's own flee check only applies when a battle starts, which is why the arena is enforced here.
|
||||||
|
|
||||||
|
**Groudon** becomes the Basalt Furnace's guardian like the others (Lv 75, band top + 5), with the zone's placeholder drops.
|
||||||
|
|
||||||
|
**Giratina** replaces the Ender Dragon: the dragon no longer spawns; Giratina (uncatchable, Lv 80) appears on the main End island while a player is there, and beating it does what killing the dragon did (exit portal, gateways, the egg on the first win).
|
||||||
|
|
||||||
## Open decisions for Adrian (proceeding with the default, flagged)
|
## Open decisions for Adrian (proceeding with the default, flagged)
|
||||||
|
|
||||||
| Decision | Default used for now |
|
| Decision | Default used for now |
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
|||||||
internal var confirmBy: UUID? = null
|
internal var confirmBy: UUID? = null
|
||||||
internal var confirmUntil = 0L
|
internal var confirmUntil = 0L
|
||||||
private var display: PokemonEntity? = null
|
private var display: PokemonEntity? = null
|
||||||
|
/** No Pokémon shown before this game time (the old one is still fainting). */
|
||||||
|
private var showFrom = 0L
|
||||||
private var ticks = 0
|
private var ticks = 0
|
||||||
|
|
||||||
val def: MachineDefs.Def? get() = MachineDefs.byCore[BuiltInRegistries.BLOCK.getKey(blockState.block)]
|
val def: MachineDefs.Def? get() = MachineDefs.byCore[BuiltInRegistries.BLOCK.getKey(blockState.block)]
|
||||||
@@ -68,7 +70,7 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
|||||||
giveBack(level, Component.translatableWithFallback("message.cobblesurvival.machine.shell_broke", "The shell of the %s broke.", machineName))
|
giveBack(level, Component.translatableWithFallback("message.cobblesurvival.machine.shell_broke", "The shell of the %s broke.", machineName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (display?.isAlive != true) display = MachineDisplay.spawn(level, pokemon, worldPosition, shell!!.chamber, facing())
|
if (display?.isAlive != true && level.gameTime >= showFrom) display = MachineDisplay.spawn(level, pokemon, worldPosition, shell!!.chamber, facing())
|
||||||
}
|
}
|
||||||
MachineWork.tick(this, level, def)
|
MachineWork.tick(this, level, def)
|
||||||
receivedThisTick = 0
|
receivedThisTick = 0
|
||||||
@@ -184,8 +186,18 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Pokémon is gone for good (sacrificed). */
|
/** The Pokémon is gone for good (sacrificed): the one shown faints. */
|
||||||
internal fun consume() = clear()
|
internal fun consume(level: ServerLevel) {
|
||||||
|
faintDisplay(level)
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The shown Pokémon faints and goes; the next one shows once it's gone (a rebirth's new form). */
|
||||||
|
internal fun faintDisplay(level: ServerLevel) {
|
||||||
|
display?.takeIf { it.isAlive }?.let { MachineDisplay.faint(level, it) }
|
||||||
|
display = null
|
||||||
|
showFrom = level.gameTime + MachineDisplay.FAINT_TICKS
|
||||||
|
}
|
||||||
|
|
||||||
private fun clear() {
|
private fun clear() {
|
||||||
pokemon = null
|
pokemon = null
|
||||||
@@ -198,8 +210,6 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
|||||||
setChanged()
|
setChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun respawnDisplay() = removeDisplay()
|
|
||||||
|
|
||||||
private fun removeDisplay() {
|
private fun removeDisplay() {
|
||||||
display?.discard()
|
display?.discard()
|
||||||
display = null
|
display = null
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import com.cobblemon.mod.common.pokemon.Pokemon
|
|||||||
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
|
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
|
||||||
import net.minecraft.core.BlockPos
|
import net.minecraft.core.BlockPos
|
||||||
import net.minecraft.core.Direction
|
import net.minecraft.core.Direction
|
||||||
|
import net.minecraft.core.Holder
|
||||||
|
import net.minecraft.resources.ResourceLocation
|
||||||
|
import net.minecraft.sounds.SoundEvent
|
||||||
|
import net.minecraft.sounds.SoundSource
|
||||||
import net.minecraft.nbt.CompoundTag
|
import net.minecraft.nbt.CompoundTag
|
||||||
import net.minecraft.server.level.ServerLevel
|
import net.minecraft.server.level.ServerLevel
|
||||||
import net.minecraft.world.entity.Entity
|
import net.minecraft.world.entity.Entity
|
||||||
@@ -15,6 +19,7 @@ import net.neoforged.neoforge.common.NeoForge
|
|||||||
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent
|
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent
|
||||||
import net.neoforged.neoforge.event.entity.player.AttackEntityEvent
|
import net.neoforged.neoforge.event.entity.player.AttackEntityEvent
|
||||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent
|
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent
|
||||||
|
import net.neoforged.neoforge.event.tick.ServerTickEvent
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +33,12 @@ object MachineDisplay {
|
|||||||
const val TAG = "cobblesurvival_machine"
|
const val TAG = "cobblesurvival_machine"
|
||||||
/** The chamber is one block; leave a little room. */
|
/** The chamber is one block; leave a little room. */
|
||||||
private const val FIT = 0.9f
|
private const val FIT = 0.9f
|
||||||
|
/** A fainting Pokémon's cry is played lower, as in the games. */
|
||||||
|
private const val FAINT_PITCH = 0.6f
|
||||||
|
const val FAINT_TICKS = 40L
|
||||||
|
|
||||||
|
private class Fading(val entity: PokemonEntity, val until: Long)
|
||||||
|
private val fading = ArrayList<Fading>()
|
||||||
|
|
||||||
fun register() {
|
fun register() {
|
||||||
NeoForge.EVENT_BUS.addListener(::onJoin)
|
NeoForge.EVENT_BUS.addListener(::onJoin)
|
||||||
@@ -35,6 +46,23 @@ object MachineDisplay {
|
|||||||
NeoForge.EVENT_BUS.addListener<PlayerInteractEvent.EntityInteractSpecific> { if (isDisplay(it.target)) it.isCanceled = true }
|
NeoForge.EVENT_BUS.addListener<PlayerInteractEvent.EntityInteractSpecific> { if (isDisplay(it.target)) it.isCanceled = true }
|
||||||
NeoForge.EVENT_BUS.addListener<AttackEntityEvent> { if (isDisplay(it.target)) it.isCanceled = true }
|
NeoForge.EVENT_BUS.addListener<AttackEntityEvent> { if (isDisplay(it.target)) it.isCanceled = true }
|
||||||
CobblemonEvents.BATTLE_STARTED_PRE.subscribe(Priority.HIGHEST, ::onBattle)
|
CobblemonEvents.BATTLE_STARTED_PRE.subscribe(Priority.HIGHEST, ::onBattle)
|
||||||
|
NeoForge.EVENT_BUS.addListener<ServerTickEvent.Post> { event ->
|
||||||
|
if (fading.isEmpty()) return@addListener
|
||||||
|
val now = event.server.overworld().gameTime
|
||||||
|
fading.removeIf { f -> (f.until <= now || f.entity.isRemoved).also { if (it) f.entity.discard() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shown Pokémon dies (a sacrifice, or the old form at the end of a rebirth; Adrian, 2026-11-02): its cry,
|
||||||
|
* pitched down as for a fainting Pokémon, and Cobblemon's faint animation; then it's gone after [FAINT_TICKS].
|
||||||
|
*/
|
||||||
|
fun faint(level: ServerLevel, entity: PokemonEntity) {
|
||||||
|
val species = entity.pokemon.species.resourceIdentifier
|
||||||
|
val cry = SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(species.namespace, "pokemon.${species.path}.cry"))
|
||||||
|
level.playSeededSound(null, entity.x, entity.y, entity.z, Holder.direct(cry), SoundSource.NEUTRAL, 1f, FAINT_PITCH, level.random.nextLong())
|
||||||
|
entity.playAnimation("faint")
|
||||||
|
fading += Fading(entity, level.gameTime + FAINT_TICKS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDisplay(entity: Entity?): Boolean = entity is PokemonEntity && entity.pokemon.persistentData.contains(TAG)
|
fun isDisplay(entity: Entity?): Boolean = entity is PokemonEntity && entity.pokemon.persistentData.contains(TAG)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ object MachineWork {
|
|||||||
val rules = def.altar!!
|
val rules = def.altar!!
|
||||||
val rewards = altarRewards(rules, pokemon)
|
val rewards = altarRewards(rules, pokemon)
|
||||||
if (core.confirmBy == player.uuid && level.gameTime <= core.confirmUntil) {
|
if (core.confirmBy == player.uuid && level.gameTime <= core.confirmUntil) {
|
||||||
core.consume()
|
core.consume(level)
|
||||||
rewards.forEach { give(level, core, player, it.copy()) }
|
rewards.forEach { give(level, core, player, it.copy()) }
|
||||||
CobbleSurvival.LOGGER.info("{} sacrificed {} (Lv {}) at {}", player.gameProfile.name, pokemon.species.name, pokemon.level, core.blockPos)
|
CobbleSurvival.LOGGER.info("{} sacrificed {} (Lv {}) at {}", player.gameProfile.name, pokemon.species.name, pokemon.level, core.blockPos)
|
||||||
MachineBlockEntity.tell(player, Component.translatableWithFallback("message.cobblesurvival.machine.sacrificed",
|
MachineBlockEntity.tell(player, Component.translatableWithFallback("message.cobblesurvival.machine.sacrificed",
|
||||||
@@ -180,10 +180,10 @@ object MachineWork {
|
|||||||
// --- Rebirth Chamber -------------------------------------------------------------------------------------------
|
// --- Rebirth Chamber -------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
private fun rebirth(core: MachineBlockEntity, level: ServerLevel, rules: MachineDefs.RebirthRules, pokemon: Pokemon) {
|
private fun rebirth(core: MachineBlockEntity, level: ServerLevel, rules: MachineDefs.RebirthRules, pokemon: Pokemon) {
|
||||||
|
core.faintDisplay(level)
|
||||||
rebirth(pokemon, rules.perfectIvs)
|
rebirth(pokemon, rules.perfectIvs)
|
||||||
core.rebirthAt = 0
|
core.rebirthAt = 0
|
||||||
core.reborn = true
|
core.reborn = true
|
||||||
core.respawnDisplay()
|
|
||||||
core.setChanged()
|
core.setChanged()
|
||||||
core.owner?.let { level.server.playerList.getPlayer(it) }?.let {
|
core.owner?.let { level.server.playerList.getPlayer(it) }?.let {
|
||||||
MachineBlockEntity.tell(it, Component.translatableWithFallback("message.cobblesurvival.machine.reborn",
|
MachineBlockEntity.tell(it, Component.translatableWithFallback("message.cobblesurvival.machine.reborn",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ object Machines {
|
|||||||
})
|
})
|
||||||
|
|
||||||
private fun core(name: String, color: MapColor, sound: SoundType): DeferredBlock<MachineBlock> = BLOCKS.registerBlock(
|
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),
|
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 },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun register(bus: IEventBus) {
|
fun register(bus: IEventBus) {
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ object MachineTests {
|
|||||||
SelfTestCase("heat_core_cooks_with_hoppers", 800, ::heatCore),
|
SelfTestCase("heat_core_cooks_with_hoppers", 800, ::heatCore),
|
||||||
SelfTestCase("dynamo_core_powers_cables", 100, ::dynamo),
|
SelfTestCase("dynamo_core_powers_cables", 100, ::dynamo),
|
||||||
SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis),
|
SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis),
|
||||||
SelfTestCase("altar_core_sacrifice_needs_confirmation", 60, ::altar),
|
SelfTestCase("altar_core_sacrifice_needs_confirmation", 120, ::altar),
|
||||||
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 100, ::rebirth),
|
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 200, ::rebirth),
|
||||||
SelfTestCase("machine_preview_shows_the_shell", 300, ::preview),
|
SelfTestCase("machine_preview_shows_the_shell", 300, ::preview),
|
||||||
SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase),
|
SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase),
|
||||||
)
|
)
|
||||||
@@ -348,6 +348,13 @@ object MachineTests {
|
|||||||
s.helper.startSequence()
|
s.helper.startSequence()
|
||||||
.thenExecute { core.rebirthAt = level.gameTime }
|
.thenExecute { core.rebirthAt = level.gameTime }
|
||||||
.thenWaitUntil { s.check(core.reborn) { "the rebirth didn't finish" } }
|
.thenWaitUntil { s.check(core.reborn) { "the rebirth didn't finish" } }
|
||||||
|
.thenWaitUntil {
|
||||||
|
// The old form faints first; then the chamber shows the new one.
|
||||||
|
val shown = displays(level, core.blockPos)
|
||||||
|
s.check(shown.size == 1 && shown.single().pokemon.species.name == "Charmander") {
|
||||||
|
"after the rebirth the chamber shows ${shown.map { it.pokemon.species.name }}, not one Charmander"
|
||||||
|
}
|
||||||
|
}
|
||||||
.thenExecute {
|
.thenExecute {
|
||||||
core.use(player)
|
core.use(player)
|
||||||
val back = Cobblemon.storage.getParty(player).firstOrNull { it.uuid == pokemon.uuid }
|
val back = Cobblemon.storage.getParty(player).firstOrNull { it.uuid == pokemon.uuid }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {
|
"textures": {
|
||||||
"top": "cobblesurvival:block/altar_core_top",
|
"top": "cobblesurvival:block/altar_core_top",
|
||||||
"front": "cobblesurvival:block/altar_core_front",
|
"front": "cobblesurvival:block/altar_core_front",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {
|
"textures": {
|
||||||
"top": "cobblesurvival:block/dynamo_core_top",
|
"top": "cobblesurvival:block/dynamo_core_top",
|
||||||
"front": "cobblesurvival:block/dynamo_core_front",
|
"front": "cobblesurvival:block/dynamo_core_front",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {
|
"textures": {
|
||||||
"top": "cobblesurvival:block/heat_core_top",
|
"top": "cobblesurvival:block/heat_core_top",
|
||||||
"front": "cobblesurvival:block/heat_core_front",
|
"front": "cobblesurvival:block/heat_core_front",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {
|
"textures": {
|
||||||
"top": "cobblesurvival:block/rebirth_core_top",
|
"top": "cobblesurvival:block/rebirth_core_top",
|
||||||
"front": "cobblesurvival:block/rebirth_core_front",
|
"front": "cobblesurvival:block/rebirth_core_front",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {
|
"textures": {
|
||||||
"top": "cobblesurvival:block/synthesis_core_top",
|
"top": "cobblesurvival:block/synthesis_core_top",
|
||||||
"front": "cobblesurvival:block/synthesis_core_front",
|
"front": "cobblesurvival:block/synthesis_core_front",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 223 B |
|
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 249 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 242 B After Width: | Height: | Size: 258 B |
|
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 261 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 227 B |
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
Each core is an "orientable" block (front, side, top) facing the player who placed it. The 16x16 textures are drawn
|
Each core is an "orientable" block (front, side, top) facing the player who placed it. The 16x16 textures are drawn
|
||||||
here, from a palette and an emblem per machine, so they can be redrawn after a change. Our own art (pack license).
|
here, from a palette and an emblem per machine, so they can be redrawn after a change. Our own art (pack license).
|
||||||
|
The cores are see-through (Adrian, 2026-11-02: the Pokémon in the chamber behind the core must be visible): a solid
|
||||||
|
frame around tinted glass, the emblem faint on the front; the models render translucent.
|
||||||
|
|
||||||
Usage: tools/machine_assets.py
|
Usage: tools/machine_assets.py
|
||||||
"""
|
"""
|
||||||
@@ -83,51 +85,54 @@ EMBLEMS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def frame(image, dark, light):
|
GLASS_ALPHA = 70
|
||||||
|
EMBLEM_ALPHA = 150
|
||||||
|
|
||||||
|
|
||||||
|
def frame(image, dark, mid, light):
|
||||||
|
"""A two-pixel frame: light top-left edge, dark bottom-right edge, mid inner ring."""
|
||||||
for i in range(16):
|
for i in range(16):
|
||||||
image.putpixel((i, 0), light)
|
for a, b in ((i, 0), (0, i)):
|
||||||
image.putpixel((0, i), light)
|
image.putpixel((a, b), light + (255,))
|
||||||
image.putpixel((i, 15), dark)
|
for a, b in ((i, 15), (15, i)):
|
||||||
image.putpixel((15, i), dark)
|
image.putpixel((a, b), dark + (255,))
|
||||||
|
for i in range(1, 15):
|
||||||
|
for a, b in ((i, 1), (1, i), (i, 14), (14, i)):
|
||||||
|
image.putpixel((a, b), mid + (255,))
|
||||||
|
|
||||||
|
|
||||||
def side(dark, mid, light):
|
def glass(dark, mid, light, accent):
|
||||||
image = Image.new("RGBA", (16, 16), mid + (255,))
|
"""Tinted glass inside the frame, with a few lighter streaks so the face still reads as a surface."""
|
||||||
for y in range(16):
|
image = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
|
||||||
for x in range(16):
|
tint = tuple((m + a) // 2 for m, a in zip(mid, accent))
|
||||||
# Plates of 8x4 with a seam, offset every other row, like bricks or panels.
|
for y in range(2, 14):
|
||||||
if y % 4 == 0 or (x + (4 if (y // 4) % 2 else 0)) % 8 == 0:
|
for x in range(2, 14):
|
||||||
image.putpixel((x, y), dark + (255,))
|
streak = (x - y) in (3, 4) or (x - y) == -6
|
||||||
elif (x + y) % 7 == 0:
|
image.putpixel((x, y), (light if streak else tint) + (GLASS_ALPHA + (50 if streak else 0),))
|
||||||
image.putpixel((x, y), light + (255,))
|
frame(image, dark, mid, light)
|
||||||
frame(image, dark + (255,), light + (255,))
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def side(dark, mid, light, accent):
|
||||||
|
return glass(dark, mid, light, accent)
|
||||||
|
|
||||||
|
|
||||||
def top(dark, mid, light, accent):
|
def top(dark, mid, light, accent):
|
||||||
image = side(dark, mid, light)
|
image = glass(dark, mid, light, accent)
|
||||||
for y in range(5, 11):
|
for x, y in ((2, 2), (13, 2), (2, 13), (13, 13)):
|
||||||
for x in range(5, 11):
|
image.putpixel((x, y), accent + (255,))
|
||||||
edge = x in (5, 10) or y in (5, 10)
|
|
||||||
image.putpixel((x, y), (dark if edge else accent) + (255,))
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def front(dark, mid, light, accent, emblem):
|
def front(dark, mid, light, accent, emblem):
|
||||||
image = Image.new("RGBA", (16, 16), dark + (255,))
|
image = glass(dark, mid, light, accent)
|
||||||
for y in range(2, 14):
|
|
||||||
for x in range(2, 14):
|
|
||||||
image.putpixel((x, y), (mid if (x + y) % 2 else dark) + (255,))
|
|
||||||
frame(image, dark + (255,), light + (255,))
|
|
||||||
rows = EMBLEMS[emblem]
|
rows = EMBLEMS[emblem]
|
||||||
top_row = (16 - len(rows)) // 2
|
top_row = (16 - len(rows)) // 2
|
||||||
glow = tuple(min(255, c + 60) for c in accent)
|
glow = tuple(min(255, c + 60) for c in accent)
|
||||||
for y, row in enumerate(rows):
|
for y, row in enumerate(rows):
|
||||||
for x, c in enumerate(row):
|
for x, c in enumerate(row):
|
||||||
if c == "#":
|
if 2 <= x <= 13 and 2 <= top_row + y <= 13 and c in "#+":
|
||||||
image.putpixel((x, top_row + y), accent + (255,))
|
image.putpixel((x, top_row + y), (accent if c == "#" else glow) + (EMBLEM_ALPHA,))
|
||||||
elif c == "+":
|
|
||||||
image.putpixel((x, top_row + y), glow + (255,))
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
@@ -140,11 +145,12 @@ def main():
|
|||||||
textures = ASSETS / "textures/block"
|
textures = ASSETS / "textures/block"
|
||||||
textures.mkdir(parents=True, exist_ok=True)
|
textures.mkdir(parents=True, exist_ok=True)
|
||||||
for name, (dark, mid, light, accent, emblem) in CORES.items():
|
for name, (dark, mid, light, accent, emblem) in CORES.items():
|
||||||
side(dark, mid, light).save(textures / f"{name}_side.png")
|
side(dark, mid, light, accent).save(textures / f"{name}_side.png")
|
||||||
top(dark, mid, light, accent).save(textures / f"{name}_top.png")
|
top(dark, mid, light, accent).save(textures / f"{name}_top.png")
|
||||||
front(dark, mid, light, accent, emblem).save(textures / f"{name}_front.png")
|
front(dark, mid, light, accent, emblem).save(textures / f"{name}_front.png")
|
||||||
write_json(ASSETS / f"models/block/{name}.json", {
|
write_json(ASSETS / f"models/block/{name}.json", {
|
||||||
"parent": "minecraft:block/orientable",
|
"parent": "minecraft:block/orientable",
|
||||||
|
"render_type": "minecraft:translucent",
|
||||||
"textures": {"top": f"cobblesurvival:block/{name}_top", "front": f"cobblesurvival:block/{name}_front",
|
"textures": {"top": f"cobblesurvival:block/{name}_top", "front": f"cobblesurvival:block/{name}_front",
|
||||||
"side": f"cobblesurvival:block/{name}_side"},
|
"side": f"cobblesurvival:block/{name}_side"},
|
||||||
})
|
})
|
||||||
|
|||||||