diff --git a/README.md b/README.md index c0ceee8..88be512 100644 --- a/README.md +++ b/README.md @@ -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). -**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 diff --git a/docs/checklists/phase-6.md b/docs/checklists/phase-6.md index 89e72be..0a2e165 100644 --- a/docs/checklists/phase-6.md +++ b/docs/checklists/phase-6.md @@ -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. - [ ] 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) - [ ] 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 +**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)? - All numbers, casings and recipes are placeholders (`docs/balance.md`). diff --git a/docs/design.md b/docs/design.md index 08dce70..f9fd921 100644 --- a/docs/design.md +++ b/docs/design.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. -**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`). @@ -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. +## 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/.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) | Decision | Default used for now | diff --git a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineBlockEntity.kt b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineBlockEntity.kt index 219af69..5e507e1 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineBlockEntity.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineBlockEntity.kt @@ -54,6 +54,8 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine internal var confirmBy: UUID? = null internal var confirmUntil = 0L 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 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)) 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) receivedThisTick = 0 @@ -184,8 +186,18 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine return true } - /** The Pokémon is gone for good (sacrificed). */ - internal fun consume() = clear() + /** The Pokémon is gone for good (sacrificed): the one shown faints. */ + 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() { pokemon = null @@ -198,8 +210,6 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine setChanged() } - internal fun respawnDisplay() = removeDisplay() - private fun removeDisplay() { display?.discard() display = null diff --git a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineDisplay.kt b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineDisplay.kt index 40b14a3..49a6894 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineDisplay.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineDisplay.kt @@ -8,6 +8,10 @@ import com.cobblemon.mod.common.pokemon.Pokemon import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty import net.minecraft.core.BlockPos 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.server.level.ServerLevel 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.player.AttackEntityEvent import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent +import net.neoforged.neoforge.event.tick.ServerTickEvent import java.util.UUID /** @@ -28,6 +33,12 @@ object MachineDisplay { const val TAG = "cobblesurvival_machine" /** The chamber is one block; leave a little room. */ 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() fun register() { NeoForge.EVENT_BUS.addListener(::onJoin) @@ -35,6 +46,23 @@ object MachineDisplay { NeoForge.EVENT_BUS.addListener { if (isDisplay(it.target)) it.isCanceled = true } NeoForge.EVENT_BUS.addListener { if (isDisplay(it.target)) it.isCanceled = true } CobblemonEvents.BATTLE_STARTED_PRE.subscribe(Priority.HIGHEST, ::onBattle) + NeoForge.EVENT_BUS.addListener { 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) diff --git a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineWork.kt b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineWork.kt index 1ab6685..c958bf2 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineWork.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/machine/MachineWork.kt @@ -163,7 +163,7 @@ object MachineWork { val rules = def.altar!! val rewards = altarRewards(rules, pokemon) if (core.confirmBy == player.uuid && level.gameTime <= core.confirmUntil) { - core.consume() + core.consume(level) rewards.forEach { give(level, core, player, it.copy()) } 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", @@ -180,10 +180,10 @@ object MachineWork { // --- Rebirth Chamber ------------------------------------------------------------------------------------------- private fun rebirth(core: MachineBlockEntity, level: ServerLevel, rules: MachineDefs.RebirthRules, pokemon: Pokemon) { + core.faintDisplay(level) rebirth(pokemon, rules.perfectIvs) core.rebirthAt = 0 core.reborn = true - core.respawnDisplay() core.setChanged() core.owner?.let { level.server.playerList.getPlayer(it) }?.let { MachineBlockEntity.tell(it, Component.translatableWithFallback("message.cobblesurvival.machine.reborn", diff --git a/mod/src/main/kotlin/dev/cobblesurvival/machine/Machines.kt b/mod/src/main/kotlin/dev/cobblesurvival/machine/Machines.kt index 8cea5f0..f29c500 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/machine/Machines.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/machine/Machines.kt @@ -39,7 +39,9 @@ object Machines { }) private fun core(name: String, color: MapColor, sound: SoundType): DeferredBlock = 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) { diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/MachineTests.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/MachineTests.kt index c82fcbf..df388d0 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/selftest/MachineTests.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/MachineTests.kt @@ -58,8 +58,8 @@ object MachineTests { SelfTestCase("heat_core_cooks_with_hoppers", 800, ::heatCore), SelfTestCase("dynamo_core_powers_cables", 100, ::dynamo), SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis), - SelfTestCase("altar_core_sacrifice_needs_confirmation", 60, ::altar), - SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 100, ::rebirth), + SelfTestCase("altar_core_sacrifice_needs_confirmation", 120, ::altar), + SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 200, ::rebirth), SelfTestCase("machine_preview_shows_the_shell", 300, ::preview), SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase), ) @@ -348,6 +348,13 @@ object MachineTests { s.helper.startSequence() .thenExecute { core.rebirthAt = level.gameTime } .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 { core.use(player) val back = Cobblemon.storage.getParty(player).firstOrNull { it.uuid == pokemon.uuid } diff --git a/mod/src/main/resources/assets/cobblesurvival/models/block/altar_core.json b/mod/src/main/resources/assets/cobblesurvival/models/block/altar_core.json index 4477e67..4b96625 100644 --- a/mod/src/main/resources/assets/cobblesurvival/models/block/altar_core.json +++ b/mod/src/main/resources/assets/cobblesurvival/models/block/altar_core.json @@ -1,5 +1,6 @@ { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": { "top": "cobblesurvival:block/altar_core_top", "front": "cobblesurvival:block/altar_core_front", diff --git a/mod/src/main/resources/assets/cobblesurvival/models/block/dynamo_core.json b/mod/src/main/resources/assets/cobblesurvival/models/block/dynamo_core.json index b11dfe5..1e3a558 100644 --- a/mod/src/main/resources/assets/cobblesurvival/models/block/dynamo_core.json +++ b/mod/src/main/resources/assets/cobblesurvival/models/block/dynamo_core.json @@ -1,5 +1,6 @@ { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": { "top": "cobblesurvival:block/dynamo_core_top", "front": "cobblesurvival:block/dynamo_core_front", diff --git a/mod/src/main/resources/assets/cobblesurvival/models/block/heat_core.json b/mod/src/main/resources/assets/cobblesurvival/models/block/heat_core.json index bc2c408..964793e 100644 --- a/mod/src/main/resources/assets/cobblesurvival/models/block/heat_core.json +++ b/mod/src/main/resources/assets/cobblesurvival/models/block/heat_core.json @@ -1,5 +1,6 @@ { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": { "top": "cobblesurvival:block/heat_core_top", "front": "cobblesurvival:block/heat_core_front", diff --git a/mod/src/main/resources/assets/cobblesurvival/models/block/rebirth_core.json b/mod/src/main/resources/assets/cobblesurvival/models/block/rebirth_core.json index 7f8a329..3ba5d62 100644 --- a/mod/src/main/resources/assets/cobblesurvival/models/block/rebirth_core.json +++ b/mod/src/main/resources/assets/cobblesurvival/models/block/rebirth_core.json @@ -1,5 +1,6 @@ { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": { "top": "cobblesurvival:block/rebirth_core_top", "front": "cobblesurvival:block/rebirth_core_front", diff --git a/mod/src/main/resources/assets/cobblesurvival/models/block/synthesis_core.json b/mod/src/main/resources/assets/cobblesurvival/models/block/synthesis_core.json index 2619560..4f4baaf 100644 --- a/mod/src/main/resources/assets/cobblesurvival/models/block/synthesis_core.json +++ b/mod/src/main/resources/assets/cobblesurvival/models/block/synthesis_core.json @@ -1,5 +1,6 @@ { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": { "top": "cobblesurvival:block/synthesis_core_top", "front": "cobblesurvival:block/synthesis_core_front", diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_front.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_front.png index 39b1efe..18bfb74 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_front.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_front.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_side.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_side.png index e9c08ee..971faa4 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_side.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_side.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_top.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_top.png index dd0d529..42bfca9 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_top.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/altar_core_top.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_front.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_front.png index 6d1e92c..9ff2804 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_front.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_front.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_side.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_side.png index 5d132fc..bbd757c 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_side.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_side.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_top.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_top.png index a17a139..39c43e1 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_top.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/dynamo_core_top.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_front.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_front.png index 085a183..c538bb2 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_front.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_front.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_side.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_side.png index e2da16f..29c05c9 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_side.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_side.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_top.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_top.png index e55222f..6a13c5c 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_top.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/heat_core_top.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_front.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_front.png index 17a4668..28e74b1 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_front.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_front.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_side.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_side.png index ea2c469..43359e2 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_side.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_side.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_top.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_top.png index d8e6415..71611d3 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_top.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/rebirth_core_top.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_front.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_front.png index 1dd7334..62b2a30 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_front.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_front.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_side.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_side.png index 524e9fa..2fd2b2b 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_side.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_side.png differ diff --git a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_top.png b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_top.png index 2724a90..2a29a08 100644 Binary files a/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_top.png and b/mod/src/main/resources/assets/cobblesurvival/textures/block/synthesis_core_top.png differ diff --git a/tools/machine_assets.py b/tools/machine_assets.py index f7c9e64..fabd104 100644 --- a/tools/machine_assets.py +++ b/tools/machine_assets.py @@ -3,6 +3,8 @@ 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). +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 """ @@ -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): - image.putpixel((i, 0), light) - image.putpixel((0, i), light) - image.putpixel((i, 15), dark) - image.putpixel((15, i), dark) + for a, b in ((i, 0), (0, i)): + image.putpixel((a, b), light + (255,)) + for a, b in ((i, 15), (15, i)): + 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): - image = Image.new("RGBA", (16, 16), mid + (255,)) - for y in range(16): - for x in range(16): - # Plates of 8x4 with a seam, offset every other row, like bricks or panels. - if y % 4 == 0 or (x + (4 if (y // 4) % 2 else 0)) % 8 == 0: - image.putpixel((x, y), dark + (255,)) - elif (x + y) % 7 == 0: - image.putpixel((x, y), light + (255,)) - frame(image, dark + (255,), light + (255,)) +def glass(dark, mid, light, accent): + """Tinted glass inside the frame, with a few lighter streaks so the face still reads as a surface.""" + image = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) + tint = tuple((m + a) // 2 for m, a in zip(mid, accent)) + for y in range(2, 14): + for x in range(2, 14): + streak = (x - y) in (3, 4) or (x - y) == -6 + image.putpixel((x, y), (light if streak else tint) + (GLASS_ALPHA + (50 if streak else 0),)) + frame(image, dark, mid, light) return image +def side(dark, mid, light, accent): + return glass(dark, mid, light, accent) + + def top(dark, mid, light, accent): - image = side(dark, mid, light) - for y in range(5, 11): - for x in range(5, 11): - edge = x in (5, 10) or y in (5, 10) - image.putpixel((x, y), (dark if edge else accent) + (255,)) + image = glass(dark, mid, light, accent) + for x, y in ((2, 2), (13, 2), (2, 13), (13, 13)): + image.putpixel((x, y), accent + (255,)) return image def front(dark, mid, light, accent, emblem): - image = Image.new("RGBA", (16, 16), dark + (255,)) - 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,)) + image = glass(dark, mid, light, accent) rows = EMBLEMS[emblem] top_row = (16 - len(rows)) // 2 glow = tuple(min(255, c + 60) for c in accent) for y, row in enumerate(rows): for x, c in enumerate(row): - if c == "#": - image.putpixel((x, top_row + y), accent + (255,)) - elif c == "+": - image.putpixel((x, top_row + y), glow + (255,)) + if 2 <= x <= 13 and 2 <= top_row + y <= 13 and c in "#+": + image.putpixel((x, top_row + y), (accent if c == "#" else glow) + (EMBLEM_ALPHA,)) return image @@ -140,11 +145,12 @@ def main(): textures = ASSETS / "textures/block" textures.mkdir(parents=True, exist_ok=True) 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") front(dark, mid, light, accent, emblem).save(textures / f"{name}_front.png") write_json(ASSETS / f"models/block/{name}.json", { "parent": "minecraft:block/orientable", + "render_type": "minecraft:translucent", "textures": {"top": f"cobblesurvival:block/{name}_top", "front": f"cobblesurvival:block/{name}_front", "side": f"cobblesurvival:block/{name}_side"}, })