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 0000000..28d0382 Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/arcana_core.png differ 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