From 29f78fa72179722ee4e731144fa59a40d3de7a04 Mon Sep 17 00:00:00 2001 From: The_miro Date: Fri, 14 Aug 2026 12:02:19 +0200 Subject: [PATCH] Sand phasing, fire on the strike, faster swimming, and the arcana core Deathworm chitin moves through sand the way the worm does. Collision is asked of the block, with the entity only along for the ride in the collision context, so this is the one thing an attribute and an event cannot carry between them and the mod's first mixin - two lines into BlockStateBase, with SandPhasing.java owning the decision. Which blocks count is the cmmodular:sand_like tag: the common c: sand, gravel and concrete powder tags, each optional, plus the vanilla loose blocks by name. Sand you stand inside cannot suffocate you. It does not drop you through the desert. A block wholly below the feet still holds you up, so sand stays something to walk on and a dune becomes something to walk into - step in and you keep your level, jump and the sand under you turns solid and puts you back on top. Without that the first sand block you stood on would swallow you down to the sandstone and leave you there. Dragon bone burns what it hits, as Ice and Fire's own flamed dragonbone sword does. That is a fake Fire Aspect: MIAPI's immolate is bonus damage against something already alight, and its own wiki marks it unimplemented. Ice and Fire ships one dragon bone for all three kinds of dragon, so it cannot be the fire dragon's alone. Sea serpent scales swim: +0.10 swim speed per module in each worn slot, which lands a full suit near the mark Armory's scuba set sets. And the arcana core, the one item here that is not a material or a module. Four arcane essence, four cinder essence, a nether star in the middle, and out comes a gem that fits any socket and ends Iron's Spells' limits - a mana pool nothing can empty, cooldowns and cast times at zero, ten times the spell power. The item is always registered because an item cannot be conditional; its material and recipe live in the Iron's Spells pack, so without that mod it is nothing. Its texture is the heart of the sea's shape in purple, drawn here. Attributes that a worn or held piece grants were going missing on the way through a portal - the modifiers are transient, the attribute map is the only place they live, and nothing re-applies them until the piece is taken off and put back on, which is why cinder essence stopped being fire immunity exactly where it was wanted. EquipmentAttributes.java rebuilds the map from the equipment after a dimension change or a respawn, and reads this mod's own attributes off the equipment as a fallback. Co-Authored-By: Claude Opus 5 --- README.md | 83 ++++++++++++- gradle.properties | 2 +- .../eu/abdelbaki/cmmodular/ArcanaCore.java | 43 +++++++ .../eu/abdelbaki/cmmodular/CMModular.java | 3 + .../cmmodular/EquipmentAttributes.java | 98 ++++++++++++++++ .../eu/abdelbaki/cmmodular/FireImmunity.java | 4 +- .../eu/abdelbaki/cmmodular/SandPhasing.java | 110 ++++++++++++++++++ .../eu/abdelbaki/cmmodular/VacuumSeal.java | 4 +- .../abdelbaki/cmmodular/WaterBreathing.java | 4 +- .../cmmodular/mixin/BlockStateBaseMixin.java | 40 +++++++ .../resources/META-INF/neoforge.mods.toml | 5 + .../assets/cmmodular/lang/en_us.json | 3 + .../cmmodular/models/item/arcana_core.json | 6 + .../cmmodular/textures/items/arcana_core.png | Bin 0 -> 283 bytes src/main/resources/cmmodular.mixins.json | 12 ++ .../data/cmmodular/tags/block/sand_like.json | 14 +++ .../miapi/materials/bone/dragonbone.json | 5 + .../materials/scute/deathworm_chitin_red.json | 10 +- .../scute/deathworm_chitin_white.json | 10 +- .../scute/deathworm_chitin_yellow.json | 10 +- .../scute/sea_serpent_scales_blue.json | 28 ++++- .../scute/sea_serpent_scales_bronze.json | 28 ++++- .../scute/sea_serpent_scales_deepblue.json | 28 ++++- .../scute/sea_serpent_scales_green.json | 28 ++++- .../scute/sea_serpent_scales_purple.json | 28 ++++- .../scute/sea_serpent_scales_red.json | 28 ++++- .../scute/sea_serpent_scales_teal.json | 28 ++++- .../miapi/materials/crystal/arcana_core.json | 88 ++++++++++++++ .../data/cmmodular/recipe/arcana_core.json | 18 +++ tools/generate_materials.py | 32 +++-- tools/materials.py | 74 +++++++++++- 31 files changed, 839 insertions(+), 35 deletions(-) create mode 100644 src/main/java/eu/abdelbaki/cmmodular/ArcanaCore.java create mode 100644 src/main/java/eu/abdelbaki/cmmodular/EquipmentAttributes.java create mode 100644 src/main/java/eu/abdelbaki/cmmodular/SandPhasing.java create mode 100644 src/main/java/eu/abdelbaki/cmmodular/mixin/BlockStateBaseMixin.java create mode 100644 src/main/resources/assets/cmmodular/models/item/arcana_core.json create mode 100644 src/main/resources/assets/cmmodular/textures/items/arcana_core.png create mode 100644 src/main/resources/cmmodular.mixins.json create mode 100644 src/main/resources/data/cmmodular/tags/block/sand_like.json create mode 100644 src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/crystal/arcana_core.json create mode 100644 src/main/resources/packs/irons_spellbooks/data/cmmodular/recipe/arcana_core.json diff --git a/README.md b/README.md index b7232db..1f07314 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Create/Mekanism Modular A [Truly Modular](https://modrinth.com/mod/modular-item-api) addon for Minecraft -1.21.1 / NeoForge. It adds 127 materials drawn from 22 mods - Create, Mekanism, Ice +1.21.1 / NeoForge. It adds 128 materials drawn from 22 mods - Create, Mekanism, Ice and Fire, Immersive Engineering, Ender IO, Applied Energistics, Occultism, Ars Nouveau, Iron's Spells, Pastel, Crystal Chronicles, NauTec, Superb Warfare, Industrial Foregoing, RFTools, Create Deco, Steampunk Dimension and the Create @@ -11,6 +11,8 @@ used to build modular tools and armour. On top of the materials it adds a few modules of its own: a khopesh blade for Arsenal, and a sealed helmet and life support pack for Armory that between them let you breathe in the vacuum Create: Cosmonautics puts above the atmosphere. +It adds one item as well, the arcana core, which is an endgame gem that ends +every limit Iron's Spells puts on a caster. Every source mod is optional. Install any subset - the materials for mods you do not have are never registered. @@ -20,7 +22,7 @@ not have are never registered. Almost all of this mod is data: MIAPI material JSON under `src/main/resources/packs//data/cmmodular/miapi/materials/`. -The small amount of Java exists for two reasons. +The small amount of Java exists for the handful of things data cannot do. **Conditional loading.** MIAPI reads materials with its own loader, which does not honour `neoforge:conditions` or anything else that could switch a file off, @@ -62,6 +64,37 @@ watches for it. Neither side knows about the other. answers the same `LivingBreatheEvent`, but only when the bearer's eyes are in water, so a fang is a fang rather than half a spacesuit. +**Moving through sand.** Deathworm chitin does what the worm does, and this is +the one thing an attribute and an event cannot carry on their own: whether a +block collides is asked of the block, with the entity along for the ride in the +collision context and nothing in between. So there is one mixin, +`BlockStateBaseMixin`, and it is two lines - `SandPhasing.java` owns the +decision. A block in `cmmodular:sand_like` returns no collision to a bearer of +`cmmodular:sand_phasing`, and sand you are standing inside of cannot suffocate +you. + +It does not drop you through the desert. A block wholly below the feet still +holds the bearer up, so sand stays something to walk on and a dune becomes +something to walk into: step in and you keep your level, jump and the sand under +you turns solid and puts you back on top. Without that rule the first sand block +you stood on would swallow you down to the sandstone and leave you there, which +is a trap rather than an ability. + +**Attributes that go missing.** Modifiers granted by worn and held items are +transient - vanilla reads them when equipment changes and the entity's attribute +map is the only place they live. Go through a portal and they can be gone, with +no equipment change to follow, so nothing puts them back until the piece is +taken off and put on again; cinder essence's fire immunity is the one that gets +noticed, because the Nether is exactly where it was wanted. +`EquipmentAttributes.java` rebuilds the map from the equipment after a dimension +change or a respawn, which is what re-equipping would have done and which fixes +every attribute rather than only this mod's, and reads this mod's own attributes +back off the equipment as well, so the answer is the same whether or not the map +ever lost them. + +**One item.** The arcana core, which is the only thing here that is not a +material or a module - see below. + ## Modules Three of the packs hold modules rather than materials. They are hand-written @@ -105,6 +138,30 @@ drawn in, rather than wearing the plain piece's art. All of the art here is placeholder-grade and meant to be repainted - see Textures below. +## The arcana core + +The one item this mod adds, and the only late-game thing in it that is not +simply a good material. Four arcane essence at the corners, four cinder essence +on the edges, a nether star in the middle - all of which are already endgame - +and what comes out is a gem that fits any socket and ends Iron's Spells' +limits: a mana pool of a million refilled a hundred times over, cooldowns and +cast times at zero, and ten times the spell power. It is deliberately absurd, +which is the point of the recipe. + +Cooldown and cast time are read by Iron's Spells as a factor on a base of 1 and +spent as `time * (2 - value)`, so a `*` modifier of 1 puts the value at 2 and +the time at nothing; spell power is the same reading, where `1 + 9` is ten +times. There is no "infinite mana" to ask for, so a pool nothing can empty and a +regeneration that refills it instantly is what that means here. + +The item is always registered, because an item cannot be added conditionally the +way a datapack can. Nothing that makes it worth having is: the material and the +recipe both live in the Iron's Spells pack, so without that mod the core cannot +be crafted, is not a material, and grants nothing. + +Its texture is the heart of the sea's shape in purple, drawn here, and +placeholder-grade like the rest of the art. + ## Stats Tinkers' Construct 3 is the reference, re-scaled onto MIAPI's vanilla-derived @@ -191,8 +248,10 @@ textures. The generated JSON is committed, so a plain build does not need this. python3 tools/generate_materials.py --jars ~/path/to/mod/jars ``` -Point it at the pack's `mods` folder. Every item id in `materials.py` is checked -against those jars and the run fails on a typo, rather than shipping a material +Point it at the pack's `mods` folder. This mod's own namespace is served out of +`src/main/resources/assets/cmmodular/` rather than a jar, so the arcana core is +checked and sampled like everything else. Every item id in `materials.py` is +checked against those jars and the run fails on a typo, rather than shipping a material that silently refuses to load. A mod whose jar is not there is skipped instead, with a line saying so, and its @@ -217,6 +276,22 @@ folders, so the hand-written packs survive it. emissive, so it still reads as fire in game. - Dragon scales are one material per colour (12 dragon, 7 sea serpent, 3 death worm chitin). Colours within an element share stats and differ only in palette. +- Sea serpent scales add `miapi:generic.swim_speed` per worn slot, 0.10 a + module. It stacks per module like everything else here, so a full suit of + them lands near +1 - about what Armory's own scuba set is worth, which is the + company a sea serpent belongs in. +- Deathworm chitin's sand phasing is gated on a tag, `cmmodular:sand_like`, so + a pack can widen it: the common `#c:sands`, `#c:gravels` and + `#c:concrete_powders` (the `c:` namespace being where Forge's tags went), + each optional in case a set is not registered, plus the vanilla loose blocks + by name so it works with none of them. +- Dragon bone burns what it hits: a fake Fire Aspect I on handheld modules, the + way Ice and Fire's own flamed dragonbone sword does. MIAPI's `immolate` is + not that - despite the name it is bonus damage against something that is + already alight, and MIAPI's own wiki marks it unimplemented. Ice and Fire + ships one dragon bone item for all three kinds of dragon, so this cannot be + the fire dragon's alone; the fire dragonscales are the only element-specific + dragon material there is. - Bones are the other half of that. Dragon bone already built grips, handles and hafts - `bone` is on every tool and bow whitelist - and now carries `gem_armor` as a hidden group as well, so a piece can be set into an armour diff --git a/gradle.properties b/gradle.properties index e8081b0..23bee48 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ neo_version=21.1.248 mod_id=cmmodular mod_name=Create/Mekanism Modular -mod_version=1.3.0 +mod_version=1.4.0 mod_group_id=eu.abdelbaki.cmmodular mod_authors=themiro mod_description=Materials from Create, Mekanism, Ice and Fire, Iron's Spells and the Create aerospace addons, for Truly Modular - plus a khopesh blade and a sealed space suit. diff --git a/src/main/java/eu/abdelbaki/cmmodular/ArcanaCore.java b/src/main/java/eu/abdelbaki/cmmodular/ArcanaCore.java new file mode 100644 index 0000000..55cfa3f --- /dev/null +++ b/src/main/java/eu/abdelbaki/cmmodular/ArcanaCore.java @@ -0,0 +1,43 @@ +package eu.abdelbaki.cmmodular; + +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Rarity; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; +import net.neoforged.neoforge.registries.DeferredItem; +import net.neoforged.neoforge.registries.DeferredRegister; + +/** + * The one item this mod adds: an endgame gem that undoes Iron's Spells' three + * limits at once - mana, cooldown and cast time - and multiplies spell power + * tenfold. + * + *

The item is always registered, because an item cannot be added + * conditionally the way a datapack can. Everything that makes it worth having + * is not: its material and its recipe both live in the Iron's Spells pack, so + * without that mod the core is neither craftable nor a material, and the + * attributes it grants are never asked for. + */ +public final class ArcanaCore { + private static final DeferredRegister.Items ITEMS = + DeferredRegister.createItems(CMModular.MOD_ID); + + public static final DeferredItem ITEM = ITEMS.registerSimpleItem( + "arcana_core", new Item.Properties().rarity(Rarity.EPIC).fireResistant().stacksTo(16)); + + private ArcanaCore() { + } + + static void register(IEventBus modBus) { + ITEMS.register(modBus); + modBus.addListener(ArcanaCore::addToCreativeTab); + } + + /** With the other crafting ingredients, which is what it is. */ + private static void addToCreativeTab(BuildCreativeModeTabContentsEvent event) { + if (event.getTabKey() == CreativeModeTabs.INGREDIENTS) { + event.accept(ITEM); + } + } +} diff --git a/src/main/java/eu/abdelbaki/cmmodular/CMModular.java b/src/main/java/eu/abdelbaki/cmmodular/CMModular.java index ea29e0d..b6b0bd2 100644 --- a/src/main/java/eu/abdelbaki/cmmodular/CMModular.java +++ b/src/main/java/eu/abdelbaki/cmmodular/CMModular.java @@ -74,6 +74,9 @@ public class CMModular { FireImmunity.register(modBus); VacuumSeal.register(modBus); WaterBreathing.register(modBus); + SandPhasing.register(modBus); + EquipmentAttributes.register(modBus); + ArcanaCore.register(modBus); modBus.addListener(this::addMaterialPacks); } diff --git a/src/main/java/eu/abdelbaki/cmmodular/EquipmentAttributes.java b/src/main/java/eu/abdelbaki/cmmodular/EquipmentAttributes.java new file mode 100644 index 0000000..906fb78 --- /dev/null +++ b/src/main/java/eu/abdelbaki/cmmodular/EquipmentAttributes.java @@ -0,0 +1,98 @@ +package eu.abdelbaki.cmmodular; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeMap; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +/** + * Keeps an attribute that a piece of equipment grants from quietly going + * missing. + * + *

Modifiers off worn and held items are transient: vanilla reads them when + * the equipment changes and puts them in the entity's attribute map, and that + * map is the only place they live. Change dimension and they can be gone - the + * player is rebuilt on the way through, no equipment change follows, and so + * nothing puts them back until the piece is taken off and put on again. Cinder + * essence's fire immunity is the one that gets noticed, because the Nether is + * exactly where it was wanted. + * + *

Two answers, because a modifier can be missing in two places. The map is + * rebuilt from the equipment after a dimension change or a respawn, which is + * what re-equipping would have done and which fixes every attribute rather than + * only this mod's. And when one of this mod's own attributes is read, the + * equipment is consulted as a fallback - the same data vanilla applies from, so + * the answer is the same whether or not the map ever lost it. + */ +public final class EquipmentAttributes { + private EquipmentAttributes() { + } + + static void register(IEventBus modBus) { + NeoForge.EVENT_BUS.addListener(EquipmentAttributes::onChangedDimension); + NeoForge.EVENT_BUS.addListener(EquipmentAttributes::onRespawn); + } + + private static void onChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + reapply(event.getEntity()); + } + + private static void onRespawn(PlayerEvent.PlayerRespawnEvent event) { + reapply(event.getEntity()); + } + + /** Puts every modifier the worn and held items grant back into the map. */ + private static void reapply(Player player) { + AttributeMap attributes = player.getAttributes(); + for (EquipmentSlot slot : EquipmentSlot.values()) { + ItemStack stack = player.getItemBySlot(slot); + if (stack.isEmpty()) { + continue; + } + Multimap, AttributeModifier> modifiers = HashMultimap.create(); + stack.forEachModifier(slot, modifiers::put); + // Each modifier is removed and added again, so this is a no-op + // wherever one survived and a repair wherever one did not. + attributes.addTransientAttributeModifiers(modifiers); + } + } + + /** + * What one of this mod's attributes is worth on this entity, falling back + * to the equipment when the attribute map has lost it. + * + *

Only additive modifiers are counted, which is all this mod grants. + */ + static double value(LivingEntity entity, Holder attribute) { + AttributeInstance instance = entity.getAttribute(attribute); + double live = instance == null ? 0.0D : instance.getValue(); + if (live > 0.0D) { + return live; + } + double[] worn = {0.0D}; + for (EquipmentSlot slot : EquipmentSlot.values()) { + ItemStack stack = entity.getItemBySlot(slot); + if (stack.isEmpty()) { + continue; + } + stack.forEachModifier(slot, (holder, modifier) -> { + if (holder.value() == attribute.value() + && modifier.operation() == AttributeModifier.Operation.ADD_VALUE) { + worn[0] += modifier.amount(); + } + }); + } + return worn[0]; + } +} diff --git a/src/main/java/eu/abdelbaki/cmmodular/FireImmunity.java b/src/main/java/eu/abdelbaki/cmmodular/FireImmunity.java index 5c5c94a..246e01a 100644 --- a/src/main/java/eu/abdelbaki/cmmodular/FireImmunity.java +++ b/src/main/java/eu/abdelbaki/cmmodular/FireImmunity.java @@ -3,7 +3,6 @@ package eu.abdelbaki.cmmodular; import net.minecraft.core.registries.Registries; import net.minecraft.tags.DamageTypeTags; import net.minecraft.world.entity.ai.attributes.Attribute; -import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.common.NeoForge; @@ -47,8 +46,7 @@ public final class FireImmunity { if (!event.getSource().is(DamageTypeTags.IS_FIRE)) { return; } - AttributeInstance instance = event.getEntity().getAttribute(FIRE_IMMUNITY); - if (instance != null && instance.getValue() >= 1.0D) { + if (EquipmentAttributes.value(event.getEntity(), FIRE_IMMUNITY) >= 1.0D) { // Put the wearer out as well, otherwise they burn harmlessly but // permanently, which reads as a bug. event.getEntity().clearFire(); diff --git a/src/main/java/eu/abdelbaki/cmmodular/SandPhasing.java b/src/main/java/eu/abdelbaki/cmmodular/SandPhasing.java new file mode 100644 index 0000000..f5214f6 --- /dev/null +++ b/src/main/java/eu/abdelbaki/cmmodular/SandPhasing.java @@ -0,0 +1,110 @@ +package eu.abdelbaki.cmmodular; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.RangedAttribute; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent; +import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +/** + * Deathworm chitin lets its bearer move through sand the way the worm it came + * off does. + * + *

Which blocks count is a tag rather than a list, so a pack can widen it: + * {@code cmmodular:sand_like} is the common {@code c:sands} and {@code c:gravels} + * plus the vanilla loose blocks by name, in case the common tags are not there. + * + *

Collision is not an attribute, an event or a property - it is asked of the + * block. So this is the one place the mod needs a mixin, and it is a small one: + * a sand block returns no collision shape to an entity carrying the attribute. + * + *

What it deliberately does not do is drop the bearer through the + * ground. A block entirely below their feet still holds them up, so a desert is + * still something to walk on and a dune is something to walk into: step into a + * dune and you keep the level you were at, jump and the sand under you turns + * solid again and puts you back on top. Without that rule the first sand block + * a wearer stood on would swallow them down to the sandstone and leave them + * there, which is a trap rather than an ability. Sand you are standing inside + * of cannot suffocate you either - that check is below. + */ +public final class SandPhasing { + private static final DeferredRegister ATTRIBUTES = + DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID); + + /** At or above 1 the bearer passes through sand; chitin grants a whole one. */ + public static final DeferredHolder SAND_PHASING = + ATTRIBUTES.register("sand_phasing", id -> new RangedAttribute( + "attribute.cmmodular.sand_phasing", 0.0D, 0.0D, 1.0D).setSyncable(true)); + + /** The blocks that are loose enough to move through. */ + public static final TagKey SAND_LIKE = TagKey.create( + Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CMModular.MOD_ID, "sand_like")); + + private SandPhasing() { + } + + static void register(IEventBus modBus) { + ATTRIBUTES.register(modBus); + modBus.addListener(SandPhasing::attachToEntities); + NeoForge.EVENT_BUS.addListener(SandPhasing::onIncomingDamage); + } + + /** Attributes only exist on an entity if they were added to its type. */ + private static void attachToEntities(EntityAttributeModificationEvent event) { + event.getTypes().forEach(type -> event.add(type, SAND_PHASING)); + } + + /** + * Whether this block should let this entity through. Called from the + * collision mixin for every block anything is about to collide with, so + * the cheap tests come first and the attribute is read straight off the + * entity rather than through {@link EquipmentAttributes}. + */ + public static boolean phasesThrough(BlockState state, BlockPos pos, CollisionContext context) { + if (!(context instanceof EntityCollisionContext entityContext)) { + return false; + } + Entity entity = entityContext.getEntity(); + if (!(entity instanceof LivingEntity) || !state.is(SAND_LIKE)) { + return false; + } + // Anything wholly under the feet is floor, and floor stays solid. + if (pos.getY() + 1 <= entity.getBoundingBox().minY + 1.0E-7D) { + return false; + } + return grants((LivingEntity) entity); + } + + /** Sand you are standing inside of is not sand that can crush you. */ + private static void onIncomingDamage(LivingIncomingDamageEvent event) { + if (!event.getSource().is(DamageTypes.IN_WALL)) { + return; + } + LivingEntity entity = event.getEntity(); + BlockPos eye = BlockPos.containing(entity.getEyePosition()); + if (entity.level().getBlockState(eye).is(SAND_LIKE) + && EquipmentAttributes.value(entity, SAND_PHASING) >= 1.0D) { + event.setCanceled(true); + } + } + + private static boolean grants(LivingEntity entity) { + AttributeInstance instance = entity.getAttribute(SAND_PHASING); + return instance != null && instance.getValue() >= 1.0D; + } +} diff --git a/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java b/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java index a761c7b..79a5a2a 100644 --- a/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java +++ b/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java @@ -2,7 +2,6 @@ package eu.abdelbaki.cmmodular; import net.minecraft.core.registries.Registries; import net.minecraft.world.entity.ai.attributes.Attribute; -import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.neoforged.bus.api.EventPriority; import net.neoforged.bus.api.IEventBus; @@ -56,8 +55,7 @@ public final class VacuumSeal { if (event.canBreathe()) { return; } - AttributeInstance instance = event.getEntity().getAttribute(VACUUM_SEAL); - if (instance != null && instance.getValue() >= 1.0D) { + if (EquipmentAttributes.value(event.getEntity(), VACUUM_SEAL) >= 1.0D) { event.setCanBreathe(true); } } diff --git a/src/main/java/eu/abdelbaki/cmmodular/WaterBreathing.java b/src/main/java/eu/abdelbaki/cmmodular/WaterBreathing.java index c1b59ae..3d850f5 100644 --- a/src/main/java/eu/abdelbaki/cmmodular/WaterBreathing.java +++ b/src/main/java/eu/abdelbaki/cmmodular/WaterBreathing.java @@ -3,7 +3,6 @@ package eu.abdelbaki.cmmodular; import net.minecraft.core.registries.Registries; import net.minecraft.tags.FluidTags; import net.minecraft.world.entity.ai.attributes.Attribute; -import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.neoforged.bus.api.EventPriority; import net.neoforged.bus.api.IEventBus; @@ -54,8 +53,7 @@ public final class WaterBreathing { if (event.canBreathe() || !event.getEntity().isEyeInFluid(FluidTags.WATER)) { return; } - AttributeInstance instance = event.getEntity().getAttribute(WATER_BREATHING); - if (instance != null && instance.getValue() >= 1.0D) { + if (EquipmentAttributes.value(event.getEntity(), WATER_BREATHING) >= 1.0D) { event.setCanBreathe(true); } } diff --git a/src/main/java/eu/abdelbaki/cmmodular/mixin/BlockStateBaseMixin.java b/src/main/java/eu/abdelbaki/cmmodular/mixin/BlockStateBaseMixin.java new file mode 100644 index 0000000..cca2e77 --- /dev/null +++ b/src/main/java/eu/abdelbaki/cmmodular/mixin/BlockStateBaseMixin.java @@ -0,0 +1,40 @@ +package eu.abdelbaki.cmmodular.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import eu.abdelbaki.cmmodular.SandPhasing; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +/** + * The one mixin in this mod, and the only way in: whether a block collides is + * asked of the block, with the entity along for the ride in the collision + * context, and no event or property sits in between. Deathworm chitin answers + * for sand - see {@link SandPhasing}, which owns the decision so that this + * stays a two-line hook into someone else's class. + */ +@Mixin(BlockBehaviour.BlockStateBase.class) +public abstract class BlockStateBaseMixin { + @Inject( + method = "getCollisionShape(Lnet/minecraft/world/level/BlockGetter;" + + "Lnet/minecraft/core/BlockPos;" + + "Lnet/minecraft/world/phys/shapes/CollisionContext;)" + + "Lnet/minecraft/world/phys/shapes/VoxelShape;", + at = @At("HEAD"), + cancellable = true) + private void cmmodular$phaseThroughSand(BlockGetter level, BlockPos pos, + CollisionContext context, + CallbackInfoReturnable callback) { + if (SandPhasing.phasesThrough((BlockState) (Object) this, pos, context)) { + callback.setReturnValue(Shapes.empty()); + } + } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index c3c5547..cd6ba52 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -3,6 +3,11 @@ loaderVersion = "[4,)" license = "MIT" issueTrackerURL = "https://github.com/themiro/create-mekanism-modular/issues" +# Collision is asked of the block, not raised as an event, so deathworm chitin's +# way through sand needs one small mixin. Nothing else here does. +[[mixins]] +config = "cmmodular.mixins.json" + [[mods]] modId = "${mod_id}" version = "${mod_version}" diff --git a/src/main/resources/assets/cmmodular/lang/en_us.json b/src/main/resources/assets/cmmodular/lang/en_us.json index 3e1a17d..7b61277 100644 --- a/src/main/resources/assets/cmmodular/lang/en_us.json +++ b/src/main/resources/assets/cmmodular/lang/en_us.json @@ -2,6 +2,9 @@ "attribute.cmmodular.fire_immunity": "Fire Immunity", "attribute.cmmodular.vacuum_seal": "Vacuum Seal", "attribute.cmmodular.water_breathing": "Water Breathing", + "attribute.cmmodular.sand_phasing": "Sand Phasing", + + "item.cmmodular.arcana_core": "Arcana Core", "miapi.module.cmmodular.blade.khopesh.name": "Khopesh Blade", "miapi.module.cmmodular.pommel.gem_socket.name": "Socketed Pommel", diff --git a/src/main/resources/assets/cmmodular/models/item/arcana_core.json b/src/main/resources/assets/cmmodular/models/item/arcana_core.json new file mode 100644 index 0000000..08f9a58 --- /dev/null +++ b/src/main/resources/assets/cmmodular/models/item/arcana_core.json @@ -0,0 +1,6 @@ +{ + "parent": "item/generated", + "textures": { + "layer0": "cmmodular:items/arcana_core" + } +} diff --git a/src/main/resources/assets/cmmodular/textures/items/arcana_core.png b/src/main/resources/assets/cmmodular/textures/items/arcana_core.png new file mode 100644 index 0000000000000000000000000000000000000000..28d038251ca5b1703bc5480cd9ad6d8b382d3e3e GIT binary patch literal 283 zcmV+$0p$LPP)4p4>0A`uAg(I5!-KvW>P18NUIf>@er0=Bw{W}8{s5`GNN z?arGwZ)SGkM<{s+y{M39lWXV|fhX?d2V)EDlOX&ar002ovPDHLkV1jJ#c~k%Z literal 0 HcmV?d00001 diff --git a/src/main/resources/cmmodular.mixins.json b/src/main/resources/cmmodular.mixins.json new file mode 100644 index 0000000..9cad62d --- /dev/null +++ b/src/main/resources/cmmodular.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "eu.abdelbaki.cmmodular.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "BlockStateBaseMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/data/cmmodular/tags/block/sand_like.json b/src/main/resources/data/cmmodular/tags/block/sand_like.json new file mode 100644 index 0000000..b1e9f80 --- /dev/null +++ b/src/main/resources/data/cmmodular/tags/block/sand_like.json @@ -0,0 +1,14 @@ +{ + "values": [ + { "id": "#c:sands", "required": false }, + { "id": "#c:gravels", "required": false }, + { "id": "#c:concrete_powders", "required": false }, + "minecraft:sand", + "minecraft:red_sand", + "minecraft:suspicious_sand", + "minecraft:gravel", + "minecraft:suspicious_gravel", + "minecraft:soul_sand", + "minecraft:soul_soil" + ] +} diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/bone/dragonbone.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/bone/dragonbone.json index 042da0f..3c42b1b 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/bone/dragonbone.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/bone/dragonbone.json @@ -40,6 +40,11 @@ "properties": { "head": { "fracturing": "10" + }, + "handheld": { + "fake_enchants": { + "minecraft:fire_aspect": 1 + } } }, "items": [ diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_red.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_red.json index 13226d9..b4ed7dd 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_red.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_red.json @@ -40,7 +40,15 @@ ], "properties": { "default": { - "fire_proof": true + "fire_proof": true, + "attributes": [ + { + "attribute": "cmmodular:sand_phasing", + "value": "1", + "operation": "+", + "slot": "any" + } + ] } }, "items": [ diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_white.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_white.json index 4f894b6..3cd5a1e 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_white.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_white.json @@ -40,7 +40,15 @@ ], "properties": { "default": { - "fire_proof": true + "fire_proof": true, + "attributes": [ + { + "attribute": "cmmodular:sand_phasing", + "value": "1", + "operation": "+", + "slot": "any" + } + ] } }, "items": [ diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_yellow.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_yellow.json index 0221325..8a414d4 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_yellow.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/deathworm_chitin_yellow.json @@ -40,7 +40,15 @@ ], "properties": { "default": { - "fire_proof": true + "fire_proof": true, + "attributes": [ + { + "attribute": "cmmodular:sand_phasing", + "value": "1", + "operation": "+", + "slot": "any" + } + ] } }, "items": [ diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_blue.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_blue.json index e42b91d..84a60d4 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_blue.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_blue.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_bronze.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_bronze.json index bdba393..8cb35d1 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_bronze.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_bronze.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_deepblue.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_deepblue.json index 25f81c1..e9c91ea 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_deepblue.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_deepblue.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_green.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_green.json index c7b02ca..ccf3808 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_green.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_green.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_purple.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_purple.json index c219c4b..f03edde 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_purple.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_purple.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_red.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_red.json index 423f998..24c5ba1 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_red.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_red.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_teal.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_teal.json index 124927b..a0e7eae 100644 --- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_teal.json +++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/scute/sea_serpent_scales_teal.json @@ -43,7 +43,33 @@ "armor": { "fake_enchants": { "minecraft:respiration": 1 - } + }, + "attributes": [ + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "head" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "chest" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "legs" + }, + { + "attribute": "miapi:generic.swim_speed", + "value": "0.1", + "operation": "+", + "slot": "feet" + } + ] }, "boot": { "fake_enchants": { diff --git a/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/crystal/arcana_core.json b/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/crystal/arcana_core.json new file mode 100644 index 0000000..d2eaf30 --- /dev/null +++ b/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/crystal/arcana_core.json @@ -0,0 +1,88 @@ +{ + "translation": "Arcana Core ", + "groups": [ + "crystal", + "gem", + "gem_melee", + "gem_armor" + ], + "icon": { + "type": "item", + "item": "cmmodular:arcana_core" + }, + "hardness": 7.6, + "density": 1.2, + "flexibility": 5, + "durability": 3400, + "enchantability": 40, + "toughness": 4, + "tier": 7, + "mining_speed": 12, + "mining_level": "minecraft:incorrect_for_netherite_tool", + "armor_durability": 48, + "armor_toughness": 3.0, + "color": "CD94EF", + "color_palette": { + "type": "grayscale_map", + "colors": { + "24": "13061d", + "68": "250b38", + "107": "250b38", + "150": "8637ba", + "190": "a95cdb", + "216": "cd94ef", + "255": "f0dcff" + }, + "filler": "interpolate" + }, + "textures": [ + "crystal", + "bright" + ], + "properties": { + "default": { + "emissive": { + "sky": 10, + "block": 10 + }, + "attributes": [ + { + "attribute": "irons_spellbooks:max_mana", + "value": "1000000", + "operation": "+", + "slot": "any" + }, + { + "attribute": "irons_spellbooks:mana_regen", + "value": "100", + "operation": "*", + "slot": "any" + }, + { + "attribute": "irons_spellbooks:cooldown_reduction", + "value": "1.0", + "operation": "*", + "slot": "any" + }, + { + "attribute": "irons_spellbooks:cast_time_reduction", + "value": "1.0", + "operation": "*", + "slot": "any" + }, + { + "attribute": "irons_spellbooks:spell_power", + "value": "9.0", + "operation": "*", + "slot": "any" + } + ] + } + }, + "items": [ + { + "item": "cmmodular:arcana_core", + "value": 1.0 + } + ] +} diff --git a/src/main/resources/packs/irons_spellbooks/data/cmmodular/recipe/arcana_core.json b/src/main/resources/packs/irons_spellbooks/data/cmmodular/recipe/arcana_core.json new file mode 100644 index 0000000..b7eabf3 --- /dev/null +++ b/src/main/resources/packs/irons_spellbooks/data/cmmodular/recipe/arcana_core.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "pattern": [ + "ACA", + "CNC", + "ACA" + ], + "key": { + "A": "irons_spellbooks:arcane_essence", + "C": "irons_spellbooks:cinder_essence", + "N": "minecraft:nether_star" + }, + "result": { + "id": "cmmodular:arcana_core", + "count": 1 + } +} diff --git a/tools/generate_materials.py b/tools/generate_materials.py index 390b39c..04a5288 100644 --- a/tools/generate_materials.py +++ b/tools/generate_materials.py @@ -54,11 +54,19 @@ LUMA = np.array([0.2126, 0.7152, 0.0722]) class Jars: - """Open mod jars, indexed by the asset namespaces they contain.""" + """Open mod jars, indexed by the asset namespaces they contain. - def __init__(self, folder: Path): + This mod's own namespace is one of them, served out of the source tree + rather than a jar. Almost every material is made of some other mod's item, + but the arcana core is made of an item added here, and it should be checked + and sampled exactly like the rest rather than being a special case that + escapes both. + """ + + def __init__(self, folder: Path, own: Path | None = None): self.zips = [] self.by_namespace = {} + self.own = own or Path(__file__).parent.parent / "src/main/resources/assets" / NAMESPACE # Addons ship a handful of files under the namespace of the mod they add # to - Applied Mekanistics into ae2, half the Create addons into create - # so a namespace goes to whichever jar has the most of it, which is the @@ -89,6 +97,8 @@ class Jars: def has_item(self, item_id: str) -> bool: namespace, path = item_id.split(":", 1) + if namespace == NAMESPACE: + return (self.own / "models/item" / f"{path}.json").exists() try: _, _, names = self._for(namespace) except LookupError: @@ -102,6 +112,9 @@ class Jars: def texture_of(self, item_id: str) -> str: """Resolve an item id to the texture id its model draws.""" namespace, path = item_id.split(":", 1) + if namespace == NAMESPACE: + model = json.loads((self.own / "models/item" / f"{path}.json").read_text()) + return model["textures"]["layer0"] _, _, names = self._for(namespace) seen = set() model_ref = f"{namespace}:item/{path}" @@ -124,10 +137,13 @@ class Jars: def image(self, texture_id: str) -> Image.Image: ns, path = texture_id.split(":", 1) if ":" in texture_id else ("minecraft", texture_id) - _, zf, _ = self._for(ns) - with zf.open(f"assets/{ns}/textures/{path}.png") as fh: - img = Image.open(fh).convert("RGBA") - img.load() + if ns == NAMESPACE: + img = Image.open(self.own / "textures" / f"{path}.png").convert("RGBA") + else: + _, zf, _ = self._for(ns) + with zf.open(f"assets/{ns}/textures/{path}.png") as fh: + img = Image.open(fh).convert("RGBA") + img.load() # Animated textures stack frames vertically; the first frame is enough. if img.height > img.width: img = img.crop((0, 0, img.width, img.width)) @@ -239,13 +255,15 @@ def namespaces_of(spec): Vanilla is not one of them - there is no Minecraft jar in the folder, and the `minecraft` namespace a mod's resource overrides create is not it. + Neither is this mod's own, which comes out of the source tree and is + therefore always there. """ entries = (spec["items"] if "items" in spec else spec["data"].get("data", {}).get("items", [])) names = {entry["item"].split(":", 1)[0] for entry in entries if "item" in entry} if "palette_from" in spec: names.add(spec["palette_from"].split(":", 1)[0]) - return names - {VANILLA.rstrip(":")} + return names - {VANILLA.rstrip(":"), NAMESPACE} def main(): diff --git a/tools/materials.py b/tools/materials.py index dd0a414..277a84b 100644 --- a/tools/materials.py +++ b/tools/materials.py @@ -84,6 +84,7 @@ PACKS = { # game has no attribute for. See FireImmunity.java and WaterBreathing.java. FIRE_IMMUNITY = "cmmodular:fire_immunity" WATER_BREATHING = "cmmodular:water_breathing" +SAND_PHASING = "cmmodular:sand_phasing" def attribute(attr, value, slot, operation="+"): @@ -421,7 +422,16 @@ MATERIALS = [ tier=4, hardness=6.2, density=4.4, flexibility=3, durability=700, enchantability=12, mining_speed=7, toughness=2, armor_durability=24, groups=("bone",), hidden_groups=("gem_armor",), textures=("rough",), - properties={"head": {"fracturing": "10"}}), + properties={"head": {"fracturing": "10"}, + # Fire on the strike, the way Ice and Fire's own flamed + # dragonbone sword burns what it hits. Fire Aspect rather + # than MIAPI's `immolate`, which despite the name is bonus + # damage against something already burning and is marked + # unimplemented in MIAPI's own wiki. Ice and Fire ships one + # dragon bone item for all three kinds of dragon, so this + # cannot be the fire dragon's alone; the fire dragonscales + # are the only element-specific dragon material there is. + "handheld": {"fake_enchants": {"minecraft:fire_aspect": 1}}}), # Vanilla bone with the numbers pushed to diamond tier; keeps the # fracturing quirk MIAPI gives bone. The `bone` group already builds # grips, handles and hafts wherever Arsenal and Archery take bone; @@ -649,6 +659,41 @@ MATERIALS = [ }, }), + # The one material made of an item this mod adds itself. It lives in the + # Iron's Spells pack even though its ingredient always exists, because + # everything it grants is an Iron's Spells attribute: without that mod + # there is nothing for the material to say. + M("arcana_core", "irons_spellbooks", "crystal", "Arcana Core", + "cmmodular:arcana_core", + [{"item": "cmmodular:arcana_core", "value": 1.0}], + tier=7, hardness=7.6, density=1.2, flexibility=5, durability=3400, + enchantability=40, mining_speed=12, toughness=4, armor_durability=48, + armor_toughness=3.0, groups=("crystal", "gem"), textures=("crystal", "bright"), + properties={ + "default": { + "emissive": {"sky": 10, "block": 10}, + # Iron's Spells limits a caster three ways - a mana pool, a + # cooldown per spell and a cast time - and this is meant to end + # all three, so all three are here. + "attributes": [ + # Mana: a pool nothing can empty, refilled a hundred times + # over. There is no "infinite" to ask for, so this is it. + attribute("irons_spellbooks:max_mana", 1000000, "any"), + attribute("irons_spellbooks:mana_regen", 100, "any", "*"), + # Cooldown and cast time are read as a factor on a base of 1 + # and spent as `time * (2 - value)`, so `*` with 1 puts the + # value at 2 and the time at nothing. Iron's Spells floors + # both at zero, so a second core cannot push them negative. + attribute("irons_spellbooks:cooldown_reduction", 1.0, "any", "*"), + attribute("irons_spellbooks:cast_time_reduction", 1.0, "any", "*"), + # Ten times the power, by the same reading: 1 + 9. + attribute("irons_spellbooks:spell_power", 9.0, "any", "*"), + ], + }, + }), + # Deliberately absurd, and priced that way: four arcane essence, four + # cinder essence and a nether star, all of which are already endgame. + # The mod's three craftable metals, for completeness - ordinary materials # that happen to sit late in its progression. M("mithril", "irons_spellbooks", "metal", "Mithril", @@ -1121,8 +1166,20 @@ def _scales(): armor_durability=32, armor_toughness=2.5, groups=("scute",), hidden_groups=SCALE_CRAFTING_GROUP, textures=("rough", "shiny"), - properties={"armor": {"fake_enchants": {"minecraft:respiration": 1}}, - "boot": {"fake_enchants": {"minecraft:depth_strider": 1}}})) + properties={ + "armor": { + "fake_enchants": {"minecraft:respiration": 1}, + # A serpent's own speed in water. Per worn slot rather than + # one `armor`-wide modifier, so each piece counts once + # where it sits; it stacks per module, so a full suit of + # scales lands near +1 - about what Armory's own scuba set + # is worth, which is the company this belongs in. + "attributes": [ + attribute("miapi:generic.swim_speed", 0.10, slot) + for slot in ("head", "chest", "legs", "feet") + ], + }, + "boot": {"fake_enchants": {"minecraft:depth_strider": 1}}})) for color, label in DEATHWORM_COLORS.items(): out.append(M( f"deathworm_chitin_{color}", "iceandfire", "scute", @@ -1133,7 +1190,16 @@ def _scales(): armor_durability=30, armor_toughness=2.0, groups=("scute",), hidden_groups=SCALE_CRAFTING_GROUP, textures=("rough",), - properties={"default": {"fire_proof": True}})) + properties={"default": { + "fire_proof": True, + # A deathworm swims through dune the way a fish does water, so + # its shell lets its bearer do the same: sand, gravel and the + # rest of `cmmodular:sand_like` stop being something to walk + # into. Slot `any` and capped at 1, as with cinder essence, so + # one piece of chitin anywhere is the whole ability. What holds + # you up is untouched - see SandPhasing.java. + "attributes": [attribute(SAND_PHASING, 1, "any")], + }})) # Desert worms, so heat immunity comes with the shell. return out