Let scales and bones build armour, and add sea serpent fang

Every scale material was in the "scute" group, and nothing in Truly
Modular accepts it - not Armory's armour, not Arsenal's or Archery's
parts. MIAPI's own turtle and armadillo scutes are stuck the same way.
So the 22 scale materials now carry "bone" as a hidden group: matched
when a module decides what it will accept, ignored when the workbench
decides what heading to file the material under, which is how Armory
hands its own gem slots vanilla materials. That is every piece of
armour except the scuba set, which takes fabric/metal/glass, plus tool
and bow parts.

Dragon bone gains "gem_armor" the same way, so it can be set into an
armour socket rather than only shaped into the armour; it already built
grips and handles as a bone.

Sea serpent fang is new - Ice and Fire has no sea serpent bone, and the
fang is what a serpent leaves besides its scales. It grants water
breathing, which is what Ice and Fire's own Tide Guardian armour does
and what nothing in MIAPI can express: WaterBreathing.java registers an
attribute and answers LivingBreatheEvent, exactly as FireImmunity and
VacuumSeal do, but only with the bearer's eyes in water so a fang is
not half a spacesuit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gem-slots-and-the-fangs-element
Amir Alexander Abdelbaki 2026-08-14 11:39:02 +02:00
parent 4e470110c2
commit dcf3b7e9c3
31 changed files with 282 additions and 10 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 126 materials drawn from 22 mods - Create, Mekanism, Ice
1.21.1 / NeoForge. It adds 127 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
@ -58,6 +58,10 @@ watches for it. Neither side knows about the other.
**Breathing in a vacuum.** The same trick, for the space set - see below.
**Breathing underwater.** And again, for sea serpent fang: `WaterBreathing.java`
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.
## Modules
Three of the packs hold modules rather than materials. They are hand-written
@ -213,6 +217,23 @@ 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.
- 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
socket instead of only shaped into the armour. Sea serpent fang is new and
does both: Ice and Fire has no sea serpent bone, and the fang is what a
serpent leaves behind besides its scales. It grants water breathing, which is
what Ice and Fire's own Tide Guardian armour does, from slot `any` - so one
fang socketed, worn or held is enough, and a second neither adds nor dilutes.
- Scales are in the `scute` group, and no module in Truly Modular accepts that
group - not Armory's armour, not Arsenal's or Archery's parts, and MIAPI's own
turtle and armadillo scutes are stuck the same way. So each scale material
carries `bone` as a hidden group: matched when a module decides what it will
accept, ignored when the workbench decides what heading to file the material
under, which is how Armory hands its own gem slots vanilla materials. Bone is
on every armour whitelist except the scuba set - that one takes
fabric/metal/glass, being a sealed suit rather than a plated one - so a scale
builds every other piece of armour, and tool and bow parts besides.
- Interstellar Expansion is mostly machinery. Only three of its items are a
material rather than a component; solid E-710 is left out because it is rocket
fuel.

View File

@ -7,7 +7,7 @@ neo_version=21.1.248
mod_id=cmmodular
mod_name=Create/Mekanism Modular
mod_version=1.2.0
mod_version=1.3.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

@ -73,6 +73,7 @@ public class CMModular {
public CMModular(IEventBus modBus) {
FireImmunity.register(modBus);
VacuumSeal.register(modBus);
WaterBreathing.register(modBus);
modBus.addListener(this::addMaterialPacks);
}

View File

@ -0,0 +1,62 @@
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;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent;
import net.neoforged.neoforge.event.entity.living.LivingBreatheEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
/**
* Sea serpent fang lets its bearer breathe underwater, which is what Ice and
* Fire's own Tide Guardian armour does and the only reason to hunt the thing.
*
* <p>There is no attribute for it and MIAPI has no property for it either - the
* closest data can get is a fake Respiration enchantment, which only postpones
* drowning. So this follows the pattern {@link FireImmunity} and
* {@link VacuumSeal} already set: an attribute of this mod's own that the
* material grants like any other, and a listener here that watches for it.
*
* <p>It answers the same event {@link VacuumSeal} does but only underwater, so
* a fang is a fang and not half a spacesuit.
*/
public final class WaterBreathing {
private static final DeferredRegister<Attribute> ATTRIBUTES =
DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID);
/** At or above 1 the bearer never drowns; the fang grants a whole one. */
public static final DeferredHolder<Attribute, Attribute> WATER_BREATHING =
ATTRIBUTES.register("water_breathing", id -> new RangedAttribute(
"attribute.cmmodular.water_breathing", 0.0D, 0.0D, 1.0D).setSyncable(true));
private WaterBreathing() {
}
static void register(IEventBus modBus) {
ATTRIBUTES.register(modBus);
modBus.addListener(WaterBreathing::attachToEntities);
// Last, so it answers after whatever said there was no air.
NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, WaterBreathing::onBreathe);
}
/** 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, WATER_BREATHING));
}
private static void onBreathe(LivingBreatheEvent event) {
if (event.canBreathe() || !event.getEntity().isEyeInFluid(FluidTags.WATER)) {
return;
}
AttributeInstance instance = event.getEntity().getAttribute(WATER_BREATHING);
if (instance != null && instance.getValue() >= 1.0D) {
event.setCanBreathe(true);
}
}
}

View File

@ -1,6 +1,7 @@
{
"attribute.cmmodular.fire_immunity": "Fire Immunity",
"attribute.cmmodular.vacuum_seal": "Vacuum Seal",
"attribute.cmmodular.water_breathing": "Water Breathing",
"miapi.module.cmmodular.blade.khopesh.name": "Khopesh Blade",
"miapi.module.cmmodular.pommel.gem_socket.name": "Socketed Pommel",

View File

@ -3,6 +3,9 @@
"groups": [
"bone"
],
"hidden_groups": [
"gem_armor"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonbone"

View File

@ -0,0 +1,64 @@
{
"translation": "Sea Serpent Fang ",
"groups": [
"bone"
],
"hidden_groups": [
"gem_armor"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_fang"
},
"hardness": 6.0,
"density": 3.0,
"flexibility": 4,
"durability": 760,
"enchantability": 16,
"toughness": 2,
"tier": 4,
"mining_speed": 7,
"mining_level": "minecraft:incorrect_for_diamond_tool",
"armor_durability": 26,
"color": "C8C8C9",
"color_palette": {
"type": "grayscale_map",
"colors": {
"24": "12131d",
"68": "222538",
"107": "525f68",
"150": "525f68",
"190": "8c8c8c",
"216": "c8c8c9",
"255": "e2fffa"
},
"filler": "interpolate"
},
"textures": [
"rough",
"shiny"
],
"properties": {
"default": {
"attributes": [
{
"attribute": "cmmodular:water_breathing",
"value": "1",
"operation": "+",
"slot": "any"
}
]
},
"boot": {
"fake_enchants": {
"minecraft:depth_strider": 1
}
}
},
"items": [
{
"item": "iceandfire:sea_serpent_fang",
"value": 1.0
}
]
}

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:deathworm_chitin_red"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:deathworm_chitin_white"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:deathworm_chitin_yellow"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_amethyst"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_black"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_blue"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_bronze"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_copper"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_electric"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_gray"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_green"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_red"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_sapphire"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_silver"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonscales_white"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_blue"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_bronze"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_deepblue"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_green"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_purple"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_red"

View File

@ -3,6 +3,9 @@
"groups": [
"scute"
],
"hidden_groups": [
"bone"
],
"icon": {
"type": "item",
"item": "iceandfire:sea_serpent_scales_teal"

View File

@ -183,6 +183,10 @@ def build_material(spec, jars: Jars):
out = {
"translation": spec["translation"] + " ", # trailing space: "Steel Sword"
"groups": spec["groups"],
# Counts for what a module will accept, but is not a heading the
# workbench files the material under. See SCALE_CRAFTING_GROUP.
**({"hidden_groups": spec["hidden_groups"]}
if spec.get("hidden_groups") else {}),
"icon": {"type": "item", "item": spec["icon"]},
}
out.update(spec["stats"])

View File

@ -80,9 +80,10 @@ PACKS = {
"armory": {"mod": "tm_armory", "name": "Armory Modules"},
}
# Attribute this mod registers itself, so a material can grant something the
# game has no attribute for. See FireImmunity.java.
# Attributes this mod registers itself, so a material can grant something the
# game has no attribute for. See FireImmunity.java and WaterBreathing.java.
FIRE_IMMUNITY = "cmmodular:fire_immunity"
WATER_BREATHING = "cmmodular:water_breathing"
def attribute(attr, value, slot, operation="+"):
@ -91,7 +92,7 @@ def attribute(attr, value, slot, operation="+"):
def M(name, pack, group, translation, palette_from, items, tier, hardness, density,
flexibility, durability, enchantability, mining_speed, *, groups=None,
toughness=0, armor_durability=None, armor_toughness=None,
hidden_groups=None, toughness=0, armor_durability=None, armor_toughness=None,
knockback_resistance=None, textures=("metallic",), properties=None,
icon=None, mining_level=None):
"""One material. `palette_from` is the item whose texture seeds the palette."""
@ -104,6 +105,7 @@ def M(name, pack, group, translation, palette_from, items, tier, hardness, densi
"icon": icon or palette_from,
"items": items,
"groups": list(groups) if groups else [group],
"hidden_groups": list(hidden_groups) if hidden_groups else [],
"textures": list(textures),
"properties": properties or {},
"stats": {
@ -418,10 +420,36 @@ MATERIALS = [
{"item": "iceandfire:dragon_bone_block", "value": 4.0}],
tier=4, hardness=6.2, density=4.4, flexibility=3, durability=700,
enchantability=12, mining_speed=7, toughness=2, armor_durability=24,
groups=("bone",), textures=("rough",),
groups=("bone",), hidden_groups=("gem_armor",), textures=("rough",),
properties={"head": {"fracturing": "10"}}),
# Vanilla bone with the numbers pushed to diamond tier; keeps the
# fracturing quirk MIAPI gives bone.
# fracturing quirk MIAPI gives bone. The `bone` group already builds
# grips, handles and hafts wherever Arsenal and Archery take bone;
# `gem_armor` on top of it lets a piece sit in an armour socket too, so a
# dragon's bones can be set into a suit rather than only shaped into one.
M("sea_serpent_fang", "iceandfire", "bone", "Sea Serpent Fang",
"iceandfire:sea_serpent_fang",
[{"item": "iceandfire:sea_serpent_fang", "value": 1.0}],
tier=4, hardness=6.0, density=3.0, flexibility=4, durability=760,
enchantability=16, mining_speed=7, toughness=2, armor_durability=26,
groups=("bone",), hidden_groups=("gem_armor",), textures=("rough", "shiny"),
properties={
"default": {
"attributes": [
# Ice and Fire's own sea serpent armour grants water
# breathing, and that is the whole reason to hunt one. Slot
# `any`, capped at 1 and read as "at or above 1" by the
# handler, so one fang anywhere - socketed, worn or held - is
# enough and a second neither adds nor dilutes, exactly as
# cinder essence does for fire.
attribute(WATER_BREATHING, 1, "any"),
],
},
"boot": {"fake_enchants": {"minecraft:depth_strider": 1}},
}),
# Lighter and springier than dragon bone and quicker to enchant, but
# softer: a serpent's tooth, not a wyrm's femur.
M("witherbone", "iceandfire", "bone", "Witherbone", "iceandfire:witherbone",
[{"item": "iceandfire:witherbone", "value": 1.0},
@ -1042,6 +1070,25 @@ DRAGON_SCALE_PROPERTIES = {
SEA_SERPENT_COLORS = ["blue", "bronze", "deepblue", "green", "purple", "red", "teal"]
DEATHWORM_COLORS = {"red": "Red", "white": "White", "yellow": "Tan"}
# No module anywhere accepts the "scute" group. Armory's armour whitelists
# wood/fabric/metal/flint/bone/stone/crystal/glass, Arsenal's and Archery's
# parts much the same, and none of the four lists "scute" - so a scale material
# was a material you could look at and never build with, the same trap the
# gem groups below were in. MIAPI's own turtle and armadillo scutes are stuck
# there too, so this is not something the scales were doing wrong.
#
# Bone is the group they join: it is on every armour whitelist except the scuba
# set (fabric/metal/glass - a sealed suit, not a plated one), it is what a hard
# organic plate is nearest to, and it carries them into tool and bow parts as
# well, which suits stats that were always meant to be usable and unremarkable
# in hand rather than unusable.
#
# It goes in `hidden_groups` rather than `groups`: MIAPI matches a module's
# whitelist against both, but only `groups` decides where the material is filed
# in the workbench, so a dragonscale still lists as a scale rather than as a
# bone. Armory itself does this to hand its gem slots vanilla materials.
SCALE_CRAFTING_GROUP = ("bone",)
def _scales():
out = []
@ -1057,7 +1104,8 @@ def _scales():
enchantability=15, mining_speed=7, toughness=4,
armor_durability=44, armor_toughness=3.5,
mining_level=MINING_LEVEL[4],
groups=("scute",), textures=("rough", "shiny"),
groups=("scute",), hidden_groups=SCALE_CRAFTING_GROUP,
textures=("rough", "shiny"),
properties=DRAGON_SCALE_PROPERTIES[element]))
# Armour-first, like Tinkers' ancient hide: high toughness and
# armour durability, unremarkable as a cutting edge.
@ -1071,7 +1119,8 @@ def _scales():
tier=4, hardness=5.4, density=2.4, flexibility=5, durability=950,
enchantability=16, mining_speed=6, toughness=3,
armor_durability=32, armor_toughness=2.5,
groups=("scute",), textures=("rough", "shiny"),
groups=("scute",), hidden_groups=SCALE_CRAFTING_GROUP,
textures=("rough", "shiny"),
properties={"armor": {"fake_enchants": {"minecraft:respiration": 1}},
"boot": {"fake_enchants": {"minecraft:depth_strider": 1}}}))
for color, label in DEATHWORM_COLORS.items():
@ -1082,7 +1131,8 @@ def _scales():
tier=4, hardness=5.6, density=2.2, flexibility=4, durability=880,
enchantability=13, mining_speed=6, toughness=3,
armor_durability=30, armor_toughness=2.0,
groups=("scute",), textures=("rough",),
groups=("scute",), hidden_groups=SCALE_CRAFTING_GROUP,
textures=("rough",),
properties={"default": {"fire_proof": True}}))
# Desert worms, so heat immunity comes with the shell.
return out