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 <noreply@anthropic.com>
gem-slots-and-the-fangs-element
Amir Alexander Abdelbaki 2026-08-14 12:02:19 +02:00
parent dcf3b7e9c3
commit 29f78fa721
31 changed files with 839 additions and 35 deletions

View File

@ -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/<mod>/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

View File

@ -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.

View File

@ -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.
*
* <p>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> 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);
}
}
}

View File

@ -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);
}

View File

@ -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.
*
* <p>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.
*
* <p>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<Holder<Attribute>, 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.
*
* <p>Only additive modifiers are counted, which is all this mod grants.
*/
static double value(LivingEntity entity, Holder<Attribute> 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];
}
}

View File

@ -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();

View File

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>What it deliberately does <em>not</em> 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<Attribute> 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<Attribute, Attribute> 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<Block> 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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<VoxelShape> callback) {
if (SandPhasing.phasesThrough((BlockState) (Object) this, pos, context)) {
callback.setReturnValue(Shapes.empty());
}
}
}

View File

@ -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}"

View File

@ -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",

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/arcana_core"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,12 @@
{
"required": true,
"minVersion": "0.8",
"package": "eu.abdelbaki.cmmodular.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"BlockStateBaseMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@ -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"
]
}

View File

@ -40,6 +40,11 @@
"properties": {
"head": {
"fracturing": "10"
},
"handheld": {
"fake_enchants": {
"minecraft:fire_aspect": 1
}
}
},
"items": [

View File

@ -40,7 +40,15 @@
],
"properties": {
"default": {
"fire_proof": true
"fire_proof": true,
"attributes": [
{
"attribute": "cmmodular:sand_phasing",
"value": "1",
"operation": "+",
"slot": "any"
}
]
}
},
"items": [

View File

@ -40,7 +40,15 @@
],
"properties": {
"default": {
"fire_proof": true
"fire_proof": true,
"attributes": [
{
"attribute": "cmmodular:sand_phasing",
"value": "1",
"operation": "+",
"slot": "any"
}
]
}
},
"items": [

View File

@ -40,7 +40,15 @@
],
"properties": {
"default": {
"fire_proof": true
"fire_proof": true,
"attributes": [
{
"attribute": "cmmodular:sand_phasing",
"value": "1",
"operation": "+",
"slot": "any"
}
]
}
},
"items": [

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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
}
]
}

View File

@ -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
}
}

View File

@ -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,6 +137,9 @@ class Jars:
def image(self, texture_id: str) -> Image.Image:
ns, path = texture_id.split(":", 1) if ":" in texture_id else ("minecraft", texture_id)
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")
@ -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():

View File

@ -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,7 +1166,19 @@ 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}},
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(
@ -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