diff --git a/README.md b/README.md
index d511ea4..d807ac0 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,17 @@
# Create/Mekanism Modular
A [Truly Modular](https://modrinth.com/mod/modular-item-api) addon for Minecraft
-1.21.1 / NeoForge. It adds 124 materials drawn from 20 mods - Create, Mekanism, Ice
+1.21.1 / NeoForge. It adds 126 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 three
-Create aerospace addons - so their metals, gems, scales and essences can all be
+Industrial Foregoing, RFTools, Create Deco, Steampunk Dimension and the Create
+aerospace addons - so their metals, gems, scales and essences can all be
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.
+
Every source mod is optional. Install any subset - the materials for mods you do
not have are never registered.
@@ -52,6 +56,51 @@ MIAPI's internals, `FireImmunity.java` registers a `cmmodular:fire_immunity`
attribute, the material grants it like any other attribute, and a damage handler
watches for it. Neither side knows about the other.
+**Breathing in a vacuum.** The same trick, for the space set - see below.
+
+## Modules
+
+Three of the packs hold modules rather than materials. They are hand-written
+rather than generated, and gated on the Truly Modular addon whose slots they use.
+
+**Khopesh blade** (`packs/arsenal`) fits any `sword_blade` slot, so it is an
+option wherever the plain sword blade is. It is the chopping sword it looks
+like: axe damage off hardness *and* density, a full point of knockback, an axe's
+mining level so it fells trees and strips logs, and a fast swing. The cost is
+everything a hook gives up - shorter reach than a sword, no sweep, and a quarter
+less durability than the blade it replaces.
+
+There was no khopesh to borrow. Nothing in the Truly Modular ecosystem ships
+one, and the khopesh art that exists elsewhere is either another mod's item or a
+retexture of a vanilla sword - neither is a MIAPI module and neither is ours to
+ship, so the blade is drawn here.
+
+**Socketed pommels** (`packs/arsenal`) give the bigger gems somewhere to sit.
+Arsenal does ship a socket pommel, but it takes a small gem, and a sweep of
+Arsenal, Armory, Archery and MIAPI turns up no module anywhere that offers a
+`gem_melee_large` slot - the large gemstone is defined and then has nowhere to
+go. So there are two here: a medium socket, and a great socket for the large
+gem. Each is a heavier counterweight than the last, paid for in swing speed.
+
+**The space set** (`packs/armory`) is Create: Cosmonautics' answer to vacuum,
+rebuilt as modules. Cosmonautics suffocates you above the atmosphere by
+answering NeoForge's `LivingBreatheEvent` with "no", and exempts its own space
+helmet through Create's diving code, which asks whether the worn helmet *is a*
+`DivingHelmetItem`. A modular helmet cannot be one, so the exemption is
+unreachable and the set brings its own: `VacuumSeal.java` registers a
+`cmmodular:vacuum_seal` attribute and answers the same event last.
+
+It takes two modules to reach the seal. The sealed helmet grants 0.5 and the
+life support pack on the chest grants 0.5, and only at 1 does the wearer
+breathe - so it is a suit, not a hat, which is the same trade Cosmonautics makes
+by pairing its helmet with a jetpack. Both come in a socketed version that
+trades 5% armour for a medium gem slot, exactly as Armory's own socket pieces
+do, and each socketed version has its own model and texture with the setting
+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.
+
## Stats
Tinkers' Construct 3 is the reference, re-scaled onto MIAPI's vanilla-derived
@@ -98,6 +147,28 @@ which is what MIAPI's own palettes do.
The sample points were tuned against MIAPI's hand-authored copper palette and
reproduce it to within about 2% per channel.
+## Textures
+
+The modules' own art is hand-drawn and lives where it can be found and changed:
+
+```
+assets/cmmodular/textures/items/ flat 16x16 art - gui icons, the khopesh blade
+assets/cmmodular/textures/equipment/ 32x32 cube nets for the worn armour models
+assets/cmmodular/textures/templates_guide.png
+```
+
+Everything is greyscale, because MIAPI recolours a module's texture through the
+material's palette: the seven shades a texture may use are the brightness stops
+of that palette (24, 68, 107, 150, 190, 216, 255), and a pixel drawn between two
+of them lands somewhere interpolated and muddy.
+
+`textures/*/templates/` holds blanks to draw over - the same size as the real
+thing, with every region a model samples outlined and the seven shades parked in
+a corner no model reads, so they can be picked straight out of the file.
+`templates_guide.png` is a single sheet showing the shades with their values,
+the cube net with each face labelled, and the vanilla armour layout, for when a
+new piece is wanted rather than a repaint of an existing one.
+
## Building
```sh
@@ -116,12 +187,24 @@ textures. The generated JSON is committed, so a plain build does not need this.
python3 tools/generate_materials.py --jars ~/path/to/mod/jars
```
-The folder needs one jar per source mod. 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.
+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
+that silently refuses to load.
+
+A mod whose jar is not there is skipped instead, with a line saying so, and its
+committed files are left untouched - the pack this follows adds and drops mods,
+and neither losing the materials of a mod you no longer have nor being unable to
+regenerate anything without a complete set of jars is useful. Nothing else is
+relaxed: a typo in a mod you *do* have still fails the run. Where several jars
+claim the same asset namespace - Applied Mekanistics writes into `ae2`, half the
+Create addons into `create` - the one with the most files in it wins, which is
+the mod that owns the namespace rather than whichever sorted first.
Requires `pillow` and `numpy`.
+Module JSON and textures are not generated. A run only clears the material
+folders, so the hand-written packs survive it.
+
## Notes
- Titanium's palette comes from Cosmonautics' ingot, which is violet. Air War's
@@ -161,3 +244,36 @@ Requires `pillow` and `numpy`.
material properties they accept and list only `default` plus their own gem
tags. Anything filed under `armor`/`handheld` silently never applies to a
socketed gem. The attribute's own slot is what decides where it counts.
+- Arcane alloy shortens Iron's Spells cooldowns, and that is the point of it:
+ 0.10 held, 0.05 on a helmet, 0.03 on the lower armour, filed under `default`
+ so a suit plated in it counts as well as a staff made of it. Iron's Spells
+ reads the attribute as a factor on a base of 1 and spends it as
+ `cooldown * (2 - value)`, so the operation is `*` - a flat `+` would be added
+ to a base of zero and do nothing - and 0.10 means 10% off the wait. It stacks
+ per module, so a weapon built entirely of arcane alloy is around 40%, against
+ 25% for Iron's Spells' own best staff; a full set adds roughly another 45%,
+ which keeps even an all-arcane player under the 100% where the formula
+ bottoms out. The material
+ is called Arcane Alloy rather than Arcane Steel because Create: Wizardry
+ renames the ingot that way, and Create: Wizardry's arcane sheet and block are
+ the same metal - they arrive as a material extension in that mod's own pack,
+ since naming them directly would break the material for anyone without it.
+- Platinum comes from Create Propulsion: Simulated, the only platinum in the
+ pack. It is soft for its density, so it swings slowly and hits like steel; it
+ is worth mining for gold-grade enchantability that does not fall apart, and it
+ is heat proof because platinum melts at 1768C.
+- Aviation alloy is AeroEngine's, iron superheated with netherite powder. A
+ turbine superalloy: light and heat proof like titanium, harder, worse to
+ enchant. It sits between titanium and titanium alloy, which is where its
+ recipe sits too.
+- Extensions are material files with a different shape - `parent` naming the
+ material to edit and `data` merged onto a copy of it, the same form MIAPI's
+ stained glass uses to vary plain glass. The merge replaces a field rather than
+ appending to it, so an extension that adds an ingredient repeats the ones it
+ keeps; `_extend_with_extra_forms` builds those lists from the material itself.
+- Ice and Fire's amethyst gem is spelled `amythest_gem` in Ice and Fire. The
+ extension named the correct spelling, which is an item that does not exist, so
+ it did nothing. Extensions are item-checked like materials now.
+- Pastel's paltaeria and stratine gems break into eight fragments on an anvil,
+ so a fragment counts as an eighth of a gem, the way a nugget counts as a ninth
+ of an ingot.
diff --git a/gradle.properties b/gradle.properties
index d4a0f0f..12c446b 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.1.0
+mod_version=1.2.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.
+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/CMModular.java b/src/main/java/eu/abdelbaki/cmmodular/CMModular.java
index 1690263..4691cbe 100644
--- a/src/main/java/eu/abdelbaki/cmmodular/CMModular.java
+++ b/src/main/java/eu/abdelbaki/cmmodular/CMModular.java
@@ -33,7 +33,7 @@ import net.neoforged.neoforge.event.AddPackFindersEvent;
public class CMModular {
public static final String MOD_ID = "cmmodular";
- /** A set of materials, and the mod it needs in order to load. */
+ /** A built-in datapack - materials or modules - and the mod it needs. */
private record MaterialPack(String id, String name, String requires) {
boolean present() {
return ModList.get().isLoaded(requires);
@@ -61,10 +61,18 @@ public class CMModular {
new MaterialPack("rftoolsbase", "RFTools Materials", "rftoolsbase"),
new MaterialPack("createdeco", "Create Deco Materials", "createdeco"),
new MaterialPack("crystal_chronicles", "Crystal Chronicles Materials", "crystal_chronicles"),
- new MaterialPack("pastel", "Pastel Materials", "pastel"));
+ new MaterialPack("pastel", "Pastel Materials", "pastel"),
+ new MaterialPack("createpropulsion", "Create Propulsion Materials", "createpropulsion"),
+ new MaterialPack("aeroengineering", "AeroEngine Materials", "aeroengineering"),
+ new MaterialPack("create_wizardry", "Create Wizardry Materials", "create_wizardry"),
+ // Modules, not materials, and gated for the same reason: they name
+ // slots - and inherit modules - that only exist with these installed.
+ new MaterialPack("arsenal", "Arsenal Modules", "tm_arsenal"),
+ new MaterialPack("armory", "Armory Modules", "tm_armory"));
public CMModular(IEventBus modBus) {
FireImmunity.register(modBus);
+ VacuumSeal.register(modBus);
modBus.addListener(this::addMaterialPacks);
}
diff --git a/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java b/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java
new file mode 100644
index 0000000..a761c7b
--- /dev/null
+++ b/src/main/java/eu/abdelbaki/cmmodular/VacuumSeal.java
@@ -0,0 +1,64 @@
+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;
+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;
+
+/**
+ * The space set: a sealed helmet and a life support pack that together let the
+ * wearer breathe where there is nothing to breathe.
+ *
+ *
Create: Cosmonautics suffocates you in space by answering
+ * {@link LivingBreatheEvent} with "no", the same event vanilla uses underwater,
+ * and Create decides who is exempt by asking whether the helmet
+ * is a {@code DivingHelmetItem}. A modular helmet never can be, so
+ * neither mod's own answer can be reused - hence an attribute of this mod's own
+ * and a listener for it, exactly as {@link FireImmunity} does for fire.
+ *
+ *
Half a seal per piece is the point: one module is never enough, so a
+ * player wanting to breathe in a vacuum has to build both halves of the suit,
+ * which is the trade Cosmonautics itself makes with its helmet and jetpack.
+ */
+public final class VacuumSeal {
+ private static final DeferredRegister ATTRIBUTES =
+ DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID);
+
+ /** At or above 1 the wearer always has air; each piece of the set is 0.5. */
+ public static final DeferredHolder VACUUM_SEAL =
+ ATTRIBUTES.register("vacuum_seal", id -> new RangedAttribute(
+ "attribute.cmmodular.vacuum_seal", 0.0D, 0.0D, 1.0D).setSyncable(true));
+
+ private VacuumSeal() {
+ }
+
+ static void register(IEventBus modBus) {
+ ATTRIBUTES.register(modBus);
+ modBus.addListener(VacuumSeal::attachToEntities);
+ // Last, so it answers after whatever said there was no air - Cosmonautics
+ // in space, vanilla underwater.
+ NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, VacuumSeal::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, VACUUM_SEAL));
+ }
+
+ private static void onBreathe(LivingBreatheEvent event) {
+ if (event.canBreathe()) {
+ return;
+ }
+ AttributeInstance instance = event.getEntity().getAttribute(VACUUM_SEAL);
+ if (instance != null && instance.getValue() >= 1.0D) {
+ event.setCanBreathe(true);
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml
index 0dce34b..c3c5547 100644
--- a/src/main/resources/META-INF/neoforge.mods.toml
+++ b/src/main/resources/META-INF/neoforge.mods.toml
@@ -182,3 +182,41 @@ type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
+
+[[dependencies.${mod_id}]]
+modId = "createpropulsion"
+type = "optional"
+versionRange = "[0,)"
+ordering = "AFTER"
+side = "BOTH"
+
+[[dependencies.${mod_id}]]
+modId = "aeroengineering"
+type = "optional"
+versionRange = "[0,)"
+ordering = "AFTER"
+side = "BOTH"
+
+[[dependencies.${mod_id}]]
+modId = "create_wizardry"
+type = "optional"
+versionRange = "[0,)"
+ordering = "AFTER"
+side = "BOTH"
+
+# The two Truly Modular addons that own the slots this mod's modules go in, and
+# the modules they inherit from. Optional like everything else - without them
+# the module packs are simply never registered.
+[[dependencies.${mod_id}]]
+modId = "tm_arsenal"
+type = "optional"
+versionRange = "[0,)"
+ordering = "AFTER"
+side = "BOTH"
+
+[[dependencies.${mod_id}]]
+modId = "tm_armory"
+type = "optional"
+versionRange = "[0,)"
+ordering = "AFTER"
+side = "BOTH"
diff --git a/src/main/resources/assets/cmmodular/lang/en_us.json b/src/main/resources/assets/cmmodular/lang/en_us.json
index 9b9e703..6f136e7 100644
--- a/src/main/resources/assets/cmmodular/lang/en_us.json
+++ b/src/main/resources/assets/cmmodular/lang/en_us.json
@@ -1,3 +1,13 @@
{
- "attribute.cmmodular.fire_immunity": "Fire Immunity"
+ "attribute.cmmodular.fire_immunity": "Fire Immunity",
+ "attribute.cmmodular.vacuum_seal": "Vacuum Seal",
+
+ "miapi.module.cmmodular.blade.khopesh.name": "Khopesh Blade",
+ "miapi.module.cmmodular.pommel.gem_socket.name": "Socketed Pommel",
+ "miapi.module.cmmodular.pommel.great_socket.name": "Great Socketed Pommel",
+
+ "miapi.module.cmmodular.armor.space.helmet.name": "Sealed Helmet",
+ "miapi.module.cmmodular.armor.space.helmet_socket.name": "Sealed Helmet with Socket",
+ "miapi.module.cmmodular.armor.space.life_support.name": "Life Support Pack",
+ "miapi.module.cmmodular.armor.space.life_support_socket.name": "Life Support Pack with Socket"
}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet/default.json b/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet/default.json
new file mode 100644
index 0000000..af95c95
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet/default.json
@@ -0,0 +1,6 @@
+{
+ "parent": "item/generated",
+ "textures": {
+ "layer0": "cmmodular:items/space_helmet"
+ }
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet_socket/default.json b/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet_socket/default.json
new file mode 100644
index 0000000..3d96c95
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/gui/space/helmet_socket/default.json
@@ -0,0 +1,6 @@
+{
+ "parent": "item/generated",
+ "textures": {
+ "layer0": "cmmodular:items/space_helmet_socket"
+ }
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json
new file mode 100644
index 0000000..3907ee4
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json
@@ -0,0 +1,31 @@
+{
+ "comment": "Life support pack, worn. Twin tanks sitting on the back plate; the same 32x32 cube net as the helmet, so both are edited the same way.",
+ "texture_size": [32, 32],
+ "textures": {
+ "1": "cmmodular:equipment/life_support_pack",
+ "particle": "cmmodular:equipment/life_support_pack"
+ },
+ "elements": [
+ {
+ "name": "tanks",
+ "from": [-3.5, -6.5, 2],
+ "to": [3.5, 1.5, 5],
+ "faces": {
+ "north": {"uv": [8, 8, 16, 16], "texture": "#1"},
+ "east": {"uv": [16, 8, 24, 16], "texture": "#1"},
+ "south": {"uv": [24, 8, 32, 16], "texture": "#1"},
+ "west": {"uv": [0, 8, 8, 16], "texture": "#1"},
+ "up": {"uv": [8, 0, 16, 8], "texture": "#1"},
+ "down": {"uv": [16, 0, 24, 8], "texture": "#1"}
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "Life Support Pack",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [0]
+ }
+ ]
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json
new file mode 100644
index 0000000..831866c
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json
@@ -0,0 +1,96 @@
+{
+ "comment": "Life support pack with a gem setting between the tanks, worn. Same box as the plain pack, socket stamped into the back face of the net.",
+ "texture_size": [
+ 32,
+ 32
+ ],
+ "textures": {
+ "1": "cmmodular:equipment/life_support_pack_socket",
+ "particle": "cmmodular:equipment/life_support_pack_socket"
+ },
+ "elements": [
+ {
+ "name": "tanks",
+ "from": [
+ -3.5,
+ -6.5,
+ 2
+ ],
+ "to": [
+ 3.5,
+ 1.5,
+ 5
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 8,
+ 8,
+ 16,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "east": {
+ "uv": [
+ 16,
+ 8,
+ 24,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "south": {
+ "uv": [
+ 24,
+ 8,
+ 32,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "west": {
+ "uv": [
+ 0,
+ 8,
+ 8,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "up": {
+ "uv": [
+ 8,
+ 0,
+ 16,
+ 8
+ ],
+ "texture": "#1"
+ },
+ "down": {
+ "uv": [
+ 16,
+ 0,
+ 24,
+ 8
+ ],
+ "texture": "#1"
+ }
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "Life Support Pack, socketed",
+ "origin": [
+ 0,
+ 0,
+ 0
+ ],
+ "color": 0,
+ "children": [
+ 0
+ ]
+ }
+ ]
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json
new file mode 100644
index 0000000..af2b71e
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json
@@ -0,0 +1,31 @@
+{
+ "comment": "Sealed helmet, worn. One cube around the head, textured from a 32x32 cube net so the faces can be edited without touching a vanilla armour atlas.",
+ "texture_size": [32, 32],
+ "textures": {
+ "1": "cmmodular:equipment/space_helmet",
+ "particle": "cmmodular:equipment/space_helmet"
+ },
+ "elements": [
+ {
+ "name": "dome",
+ "from": [-4.25, -4.25, -4.25],
+ "to": [4.25, 4.25, 4.25],
+ "faces": {
+ "north": {"uv": [8, 8, 16, 16], "texture": "#1"},
+ "east": {"uv": [16, 8, 24, 16], "texture": "#1"},
+ "south": {"uv": [24, 8, 32, 16], "texture": "#1"},
+ "west": {"uv": [0, 8, 8, 16], "texture": "#1"},
+ "up": {"uv": [8, 0, 16, 8], "texture": "#1"},
+ "down": {"uv": [16, 0, 24, 8], "texture": "#1"}
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "Space Helmet",
+ "origin": [0, 0, 0],
+ "color": 0,
+ "children": [0]
+ }
+ ]
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json
new file mode 100644
index 0000000..7728666
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json
@@ -0,0 +1,96 @@
+{
+ "comment": "Sealed helmet with a gem setting on the crown, worn. Same cube as the plain helmet, socket stamped into the top face of the net.",
+ "texture_size": [
+ 32,
+ 32
+ ],
+ "textures": {
+ "1": "cmmodular:equipment/space_helmet_socket",
+ "particle": "cmmodular:equipment/space_helmet_socket"
+ },
+ "elements": [
+ {
+ "name": "dome",
+ "from": [
+ -4.25,
+ -4.25,
+ -4.25
+ ],
+ "to": [
+ 4.25,
+ 4.25,
+ 4.25
+ ],
+ "faces": {
+ "north": {
+ "uv": [
+ 8,
+ 8,
+ 16,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "east": {
+ "uv": [
+ 16,
+ 8,
+ 24,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "south": {
+ "uv": [
+ 24,
+ 8,
+ 32,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "west": {
+ "uv": [
+ 0,
+ 8,
+ 8,
+ 16
+ ],
+ "texture": "#1"
+ },
+ "up": {
+ "uv": [
+ 8,
+ 0,
+ 16,
+ 8
+ ],
+ "texture": "#1"
+ },
+ "down": {
+ "uv": [
+ 16,
+ 0,
+ 24,
+ 8
+ ],
+ "texture": "#1"
+ }
+ }
+ }
+ ],
+ "groups": [
+ {
+ "name": "Space Helmet, socketed",
+ "origin": [
+ 0,
+ 0,
+ 0
+ ],
+ "color": 0,
+ "children": [
+ 0
+ ]
+ }
+ ]
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/sword/blade/khopesh/default.json b/src/main/resources/assets/cmmodular/models/item/sword/blade/khopesh/default.json
new file mode 100644
index 0000000..f3f536e
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/sword/blade/khopesh/default.json
@@ -0,0 +1,6 @@
+{
+ "parent": "item/generated",
+ "textures": {
+ "layer0": "cmmodular:items/khopesh_blade"
+ }
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/sword/pommel/gem_socket/default.json b/src/main/resources/assets/cmmodular/models/item/sword/pommel/gem_socket/default.json
new file mode 100644
index 0000000..2e568d5
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/sword/pommel/gem_socket/default.json
@@ -0,0 +1,6 @@
+{
+ "parent": "item/generated",
+ "textures": {
+ "layer0": "cmmodular:items/pommel_gem_socket"
+ }
+}
diff --git a/src/main/resources/assets/cmmodular/models/item/sword/pommel/great_socket/default.json b/src/main/resources/assets/cmmodular/models/item/sword/pommel/great_socket/default.json
new file mode 100644
index 0000000..590529c
--- /dev/null
+++ b/src/main/resources/assets/cmmodular/models/item/sword/pommel/great_socket/default.json
@@ -0,0 +1,6 @@
+{
+ "parent": "item/generated",
+ "textures": {
+ "layer0": "cmmodular:items/pommel_great_socket"
+ }
+}
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack.png b/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack.png
new file mode 100644
index 0000000..ae382e6
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack_socket.png
new file mode 100644
index 0000000..a4f722c
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/life_support_pack_socket.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/space_helmet.png b/src/main/resources/assets/cmmodular/textures/equipment/space_helmet.png
new file mode 100644
index 0000000..65b74f6
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/space_helmet.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/space_helmet_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/space_helmet_socket.png
new file mode 100644
index 0000000..08b6d56
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/space_helmet_socket.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/templates/armor_layer.png b/src/main/resources/assets/cmmodular/textures/equipment/templates/armor_layer.png
new file mode 100644
index 0000000..3a8f00d
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/templates/armor_layer.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/equipment/templates/cube_net.png b/src/main/resources/assets/cmmodular/textures/equipment/templates/cube_net.png
new file mode 100644
index 0000000..9b794fb
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/equipment/templates/cube_net.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/khopesh_blade.png b/src/main/resources/assets/cmmodular/textures/items/khopesh_blade.png
new file mode 100644
index 0000000..6c764ba
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/khopesh_blade.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/pommel_gem_socket.png b/src/main/resources/assets/cmmodular/textures/items/pommel_gem_socket.png
new file mode 100644
index 0000000..3cb3451
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/pommel_gem_socket.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/pommel_great_socket.png b/src/main/resources/assets/cmmodular/textures/items/pommel_great_socket.png
new file mode 100644
index 0000000..24c31de
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/pommel_great_socket.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/space_helmet.png b/src/main/resources/assets/cmmodular/textures/items/space_helmet.png
new file mode 100644
index 0000000..588cdbb
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/space_helmet.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/space_helmet_socket.png b/src/main/resources/assets/cmmodular/textures/items/space_helmet_socket.png
new file mode 100644
index 0000000..fed04a4
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/space_helmet_socket.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/items/templates/icon.png b/src/main/resources/assets/cmmodular/textures/items/templates/icon.png
new file mode 100644
index 0000000..8048a96
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/items/templates/icon.png differ
diff --git a/src/main/resources/assets/cmmodular/textures/templates_guide.png b/src/main/resources/assets/cmmodular/textures/templates_guide.png
new file mode 100644
index 0000000..1b01717
Binary files /dev/null and b/src/main/resources/assets/cmmodular/textures/templates_guide.png differ
diff --git a/src/main/resources/packs/aeroengineering/data/cmmodular/miapi/materials/metal/aviation_alloy.json b/src/main/resources/packs/aeroengineering/data/cmmodular/miapi/materials/metal/aviation_alloy.json
new file mode 100644
index 0000000..eb70e64
--- /dev/null
+++ b/src/main/resources/packs/aeroengineering/data/cmmodular/miapi/materials/metal/aviation_alloy.json
@@ -0,0 +1,64 @@
+{
+ "translation": "Aviation Alloy ",
+ "groups": [
+ "metal"
+ ],
+ "icon": {
+ "type": "item",
+ "item": "aeroengineering:aviation_alloy_ingot"
+ },
+ "hardness": 6.6,
+ "density": 2.4,
+ "flexibility": 4,
+ "durability": 1650,
+ "enchantability": 14,
+ "toughness": 3,
+ "tier": 5,
+ "mining_speed": 10,
+ "mining_level": "minecraft:incorrect_for_netherite_tool",
+ "armor_durability": 35,
+ "armor_toughness": 2.0,
+ "color": "889295",
+ "color_palette": {
+ "type": "grayscale_map",
+ "colors": {
+ "24": "1f2122",
+ "68": "3b4041",
+ "107": "4f575a",
+ "150": "575e60",
+ "190": "666f72",
+ "216": "889295",
+ "255": "a5aeb0"
+ },
+ "filler": "interpolate"
+ },
+ "textures": [
+ "metallic",
+ "bright"
+ ],
+ "properties": {
+ "default": {
+ "fire_proof": true
+ },
+ "handheld": {
+ "attributes": [
+ {
+ "attribute": "generic.attack_speed",
+ "value": "0.08",
+ "operation": "+",
+ "slot": "mainhand"
+ }
+ ]
+ }
+ },
+ "items": [
+ {
+ "item": "aeroengineering:aviation_alloy_ingot",
+ "value": 1.0
+ },
+ {
+ "item": "aeroengineering:aviation_alloy_sheet",
+ "value": 1.0
+ }
+ ]
+}
diff --git a/src/main/resources/packs/aeroengineering/pack.mcmeta b/src/main/resources/packs/aeroengineering/pack.mcmeta
new file mode 100644
index 0000000..0179fc6
--- /dev/null
+++ b/src/main/resources/packs/aeroengineering/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "AeroEngine Materials for Truly Modular",
+ "pack_format": 48
+ }
+}
diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json
new file mode 100644
index 0000000..bcacefd
--- /dev/null
+++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json
@@ -0,0 +1,52 @@
+{
+ "parent": "tm_armory:armor/default/helmet",
+ "data": {
+ "merge": {
+ "module_tag": [
+ "space",
+ "sealed"
+ ],
+ "attributes": [
+ {
+ "attribute": "cmmodular:vacuum_seal",
+ "value": "0.5",
+ "operation": "+",
+ "slot": "head"
+ }
+ ]
+ },
+ "replace": {
+ "model": [
+ {
+ "path": "cmmodular:models/item/armor/model/helmet/space/[material.texture].json",
+ "trim_mode": "ARMOR_LAYER_ONE",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "head"
+ }
+ },
+ {
+ "path": "cmmodular:models/item/armor/gui/space/helmet/[material.texture].json",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "item"
+ }
+ }
+ ],
+ "priority": 4,
+ "display_name": "miapi.module.cmmodular.armor.space.helmet.name",
+ "allowed_material": {
+ "allowedMaterials": [
+ "metal",
+ "crystal",
+ "glass"
+ ],
+ "cost": 6
+ }
+ }
+ }
+}
diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json
new file mode 100644
index 0000000..a792f89
--- /dev/null
+++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json
@@ -0,0 +1,68 @@
+{
+ "parent": "cmmodular:armor/space/helmet",
+ "data": {
+ "merge": {
+ "module_tag": [
+ "socket_armor"
+ ],
+ "attributes": [
+ {
+ "attribute": "minecraft:generic.armor",
+ "value": "-0.05",
+ "operation": "*",
+ "slot": "head"
+ }
+ ]
+ },
+ "replace": {
+ "model": [
+ {
+ "path": "cmmodular:models/item/armor/model/helmet/space_socket/[material.texture].json",
+ "trim_mode": "ARMOR_LAYER_ONE",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "head"
+ }
+ },
+ {
+ "path": "cmmodular:models/item/armor/gui/space/helmet_socket/[material.texture].json",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "item"
+ }
+ }
+ ],
+ "slots": {
+ "hat": {
+ "allowed": [
+ "hat",
+ "sub_hat"
+ ],
+ "translationKey": "miapi.slot.hat.name",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": -1, "z": 0},
+ "scale": {"x": 1.1, "y": 1.1, "z": 1.1},
+ "origin": "head"
+ }
+ },
+ "gem": {
+ "allowed": [
+ "gem_armor_medium"
+ ],
+ "translationKey": "miapi.slot.gemstone.medium",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 3, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ }
+ },
+ "display_name": "miapi.module.cmmodular.armor.space.helmet_socket.name"
+ }
+ }
+}
diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json
new file mode 100644
index 0000000..4d0ef65
--- /dev/null
+++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json
@@ -0,0 +1,42 @@
+{
+ "parent": "tm_armory:armor/default/back_chest",
+ "data": {
+ "merge": {
+ "module_tag": [
+ "space",
+ "sealed"
+ ],
+ "attributes": [
+ {
+ "attribute": "cmmodular:vacuum_seal",
+ "value": "0.5",
+ "operation": "+",
+ "slot": "chest"
+ }
+ ]
+ },
+ "replace": {
+ "model": [
+ {
+ "path": "cmmodular:models/item/armor/model/chest_back/space/[material.texture].json",
+ "trim_mode": "ARMOR_LAYER_ONE",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "body"
+ }
+ }
+ ],
+ "display_name": "miapi.module.cmmodular.armor.space.life_support.name",
+ "allowed_material": {
+ "allowedMaterials": [
+ "metal",
+ "crystal",
+ "glass"
+ ],
+ "cost": 4
+ }
+ }
+ }
+}
diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json
new file mode 100644
index 0000000..9441183
--- /dev/null
+++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json
@@ -0,0 +1,61 @@
+{
+ "parent": "cmmodular:armor/space/life_support",
+ "data": {
+ "merge": {
+ "module_tag": [
+ "socket_armor"
+ ],
+ "attributes": [
+ {
+ "attribute": "minecraft:generic.armor",
+ "value": "-0.05",
+ "operation": "*",
+ "slot": "chest"
+ }
+ ]
+ },
+ "replace": {
+ "model": [
+ {
+ "path": "cmmodular:models/item/armor/model/chest_back/space_socket/[material.texture].json",
+ "trim_mode": "ARMOR_LAYER_ONE",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": 0, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1},
+ "origin": "body"
+ }
+ }
+ ],
+ "slots": {
+ "banner-back": {
+ "allowed": [
+ "cosmetic_4x8",
+ "banner_4x8"
+ ],
+ "slotType": "cosmetic",
+ "translationKey": "miapi.slot.chest_back_center.name",
+ "transform": {
+ "rotation": {"x": 180, "y": 0, "z": 180},
+ "translation": {"x": 0, "y": 1.7, "z": 2.35},
+ "scale": {"x": 1, "y": 0.775, "z": 1.1},
+ "origin": "body"
+ }
+ },
+ "gem": {
+ "allowed": [
+ "gem_armor_medium"
+ ],
+ "translationKey": "miapi.slot.gemstone.medium",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": 0, "y": -1, "z": 0},
+ "scale": {"x": 1.1, "y": 1.1, "z": 1.1},
+ "origin": "body"
+ }
+ }
+ },
+ "display_name": "miapi.module.cmmodular.armor.space.life_support_socket.name"
+ }
+ }
+}
diff --git a/src/main/resources/packs/armory/pack.mcmeta b/src/main/resources/packs/armory/pack.mcmeta
new file mode 100644
index 0000000..2000eb7
--- /dev/null
+++ b/src/main/resources/packs/armory/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "Armory Modules for Truly Modular",
+ "pack_format": 48
+ }
+}
diff --git a/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/blade/khopesh.json b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/blade/khopesh.json
new file mode 100644
index 0000000..d5172e6
--- /dev/null
+++ b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/blade/khopesh.json
@@ -0,0 +1,155 @@
+{
+ "display_name": "miapi.module.cmmodular.blade.khopesh.name",
+ "model": [
+ {
+ "path": "cmmodular:models/item/sword/blade/khopesh/[material.texture].json",
+ "color": "[material.color]",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": -1, "y": -1, "z": -0.001},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ }
+ ],
+ "is_weapon": "true",
+ "priority": 3,
+ "repair_priority": -1,
+ "allowed_in_slots": [
+ "sword_blade"
+ ],
+ "module_stats": {
+ "blade_size": 1
+ },
+ "allowed_material": {
+ "allowedMaterials": [
+ "wood",
+ "metal",
+ "flint",
+ "bone",
+ "stone",
+ "crystal",
+ "glass"
+ ],
+ "cost": 2
+ },
+ "material_indication": {
+ "durability": 1,
+ "hardness": {
+ "strength": 3,
+ "info": {
+ "type": "translatable",
+ "translate": "tm_arsenal.material_indication.blade.damage",
+ "color": "dark_gray"
+ }
+ },
+ "density": {
+ "strength": 2,
+ "info": {
+ "type": "translatable",
+ "translate": "tm_arsenal.material_indication.blade.damage",
+ "color": "dark_gray"
+ }
+ },
+ "mining_speed": {
+ "strength": 1,
+ "info": {
+ "type": "translatable",
+ "translate": "tm_arsenal.material_indication.default.mining_speed",
+ "color": "dark_gray"
+ }
+ }
+ },
+ "enchantments": {
+ "allowed": [
+ "minecraft:enchantable/sword",
+ "minecraft:enchantable/fire_aspect",
+ "minecraft:enchantable/sharp_weapon",
+ "minecraft:enchantable/weapon",
+ "minecraft:enchantable/mining",
+ "minecraft:enchantable/mining_loot"
+ ]
+ },
+ "mining_level": {
+ "axe": {
+ "allowed": [
+ "#minecraft:mineable/axe"
+ ],
+ "use_material": true,
+ "speed": "[material.mining_speed] * 0.75"
+ },
+ "sword": {
+ "allowed": [
+ "#minecraft:sword_efficient"
+ ],
+ "use_material": false,
+ "speed": "1.5"
+ },
+ "cobweb": {
+ "allowed": [
+ [
+ "minecraft:cobweb"
+ ]
+ ],
+ "use_material": false,
+ "correct_for_drops": true,
+ "speed": "15"
+ }
+ },
+ "ability_context": {
+ "axe_ability": {}
+ },
+ "attributes": [
+ {
+ "attribute": "generic.attack_damage",
+ "value": "[material.hardness]*0.9 + [material.density]*0.5",
+ "operation": "+",
+ "slot": "mainhand"
+ },
+ {
+ "attribute": "generic.attack_speed",
+ "value": "0.15",
+ "operation": "+",
+ "slot": "mainhand"
+ },
+ {
+ "attribute": "generic.attack_knockback",
+ "value": "1",
+ "operation": "+",
+ "slot": "mainhand"
+ },
+ {
+ "attribute": "miapi:generic.attack_range",
+ "value": "-0.4",
+ "operation": "+",
+ "slot": "mainhand"
+ },
+ {
+ "attribute": "miapi:generic.shield_break",
+ "value": "4",
+ "operation": "+",
+ "slot": "mainhand"
+ }
+ ],
+ "durability": "[material.durability] * 0.2",
+ "better_combat_config": "bettercombat:axe",
+ "item_id": "miapi:modular_sword",
+ "fake_item_tag": [
+ "minecraft:swords",
+ "c:swords",
+ "minecraft:axes",
+ "c:axes",
+ "c:tools",
+ "c:tool/axe"
+ ],
+ "module_tag": [
+ "default",
+ "handheld",
+ "blade",
+ "head",
+ "blade",
+ "blade_medium",
+ "axe",
+ "polearm_glaive",
+ "sharp"
+ ]
+}
diff --git a/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/gem_socket.json b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/gem_socket.json
new file mode 100644
index 0000000..50fb6bf
--- /dev/null
+++ b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/gem_socket.json
@@ -0,0 +1,59 @@
+{
+ "display_name": "miapi.module.cmmodular.pommel.gem_socket.name",
+ "model": {
+ "path": "cmmodular:models/item/sword/pommel/gem_socket/[material.texture].json",
+ "color": "[material.color]",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": -4, "y": -5, "z": 0.001},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ },
+ "allowed_in_slots": [
+ "sword_pommel"
+ ],
+ "slots": {
+ "socket_medium": {
+ "allowed": [
+ "gem_melee_medium"
+ ],
+ "translationKey": "miapi.slot.gem.name",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": -5, "y": -5, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ }
+ },
+ "attributes": [
+ {
+ "attribute": "generic.attack_speed",
+ "value": "-0.06",
+ "operation": "*",
+ "slot": "mainhand"
+ }
+ ],
+ "priority": 0,
+ "repair_priority": 0,
+ "durability": "[material.durability]* 0.25",
+ "material_indication": {
+ "durability": 1
+ },
+ "allowed_material": {
+ "allowedMaterials": [
+ "wood",
+ "metal",
+ "flint",
+ "bone",
+ "stone",
+ "rod",
+ "crystal",
+ "glass"
+ ],
+ "cost": 2
+ },
+ "module_tag": [
+ "default",
+ "handheld"
+ ]
+}
diff --git a/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/great_socket.json b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/great_socket.json
new file mode 100644
index 0000000..b00c411
--- /dev/null
+++ b/src/main/resources/packs/arsenal/data/cmmodular/miapi/modules/pommel/great_socket.json
@@ -0,0 +1,57 @@
+{
+ "display_name": "miapi.module.cmmodular.pommel.great_socket.name",
+ "model": {
+ "path": "cmmodular:models/item/sword/pommel/great_socket/[material.texture].json",
+ "color": "[material.color]",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": -4, "y": -5, "z": 0.001},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ },
+ "allowed_in_slots": [
+ "sword_pommel"
+ ],
+ "slots": {
+ "socket_large": {
+ "allowed": [
+ "gem_melee_large"
+ ],
+ "translationKey": "miapi.slot.gem.name",
+ "transform": {
+ "rotation": {"x": 0, "y": 0, "z": 0},
+ "translation": {"x": -5, "y": -5, "z": 0},
+ "scale": {"x": 1, "y": 1, "z": 1}
+ }
+ }
+ },
+ "attributes": [
+ {
+ "attribute": "generic.attack_speed",
+ "value": "-0.12",
+ "operation": "*",
+ "slot": "mainhand"
+ }
+ ],
+ "priority": 0,
+ "repair_priority": 0,
+ "durability": "[material.durability]* 0.3",
+ "material_indication": {
+ "durability": 1
+ },
+ "allowed_material": {
+ "allowedMaterials": [
+ "metal",
+ "flint",
+ "bone",
+ "stone",
+ "crystal",
+ "glass"
+ ],
+ "cost": 3
+ },
+ "module_tag": [
+ "default",
+ "handheld"
+ ]
+}
diff --git a/src/main/resources/packs/arsenal/pack.mcmeta b/src/main/resources/packs/arsenal/pack.mcmeta
new file mode 100644
index 0000000..71feed1
--- /dev/null
+++ b/src/main/resources/packs/arsenal/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "Arsenal Modules for Truly Modular",
+ "pack_format": 48
+ }
+}
diff --git a/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/arcane_metal.json b/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/arcane_metal.json
new file mode 100644
index 0000000..c435920
--- /dev/null
+++ b/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/arcane_metal.json
@@ -0,0 +1,19 @@
+{
+ "parent": "cmmodular:metal/arcane_metal",
+ "data": {
+ "items": [
+ {
+ "item": "irons_spellbooks:arcane_ingot",
+ "value": 1.0
+ },
+ {
+ "item": "create_wizardry:arcane_sheet",
+ "value": 1.0
+ },
+ {
+ "item": "create_wizardry:arcane_block",
+ "value": 9.0
+ }
+ ]
+ }
+}
diff --git a/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/mithril.json b/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/mithril.json
new file mode 100644
index 0000000..5a7ec14
--- /dev/null
+++ b/src/main/resources/packs/create_wizardry/data/cmmodular/miapi/materials/metal/mithril.json
@@ -0,0 +1,27 @@
+{
+ "parent": "cmmodular:metal/mithril",
+ "data": {
+ "items": [
+ {
+ "item": "irons_spellbooks:mithril_ingot",
+ "value": 1.0
+ },
+ {
+ "tag": "c:ingots/mithril",
+ "value": 1.0
+ },
+ {
+ "tag": "c:nuggets/mithril",
+ "value": 0.11112
+ },
+ {
+ "tag": "c:storage_blocks/mithril",
+ "value": 9.0
+ },
+ {
+ "item": "create_wizardry:mithril_nugget",
+ "value": 0.11112
+ }
+ ]
+ }
+}
diff --git a/src/main/resources/packs/create_wizardry/pack.mcmeta b/src/main/resources/packs/create_wizardry/pack.mcmeta
new file mode 100644
index 0000000..c193cf9
--- /dev/null
+++ b/src/main/resources/packs/create_wizardry/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "Create Wizardry Materials for Truly Modular",
+ "pack_format": 48
+ }
+}
diff --git a/src/main/resources/packs/createpropulsion/data/cmmodular/miapi/materials/metal/platinum.json b/src/main/resources/packs/createpropulsion/data/cmmodular/miapi/materials/metal/platinum.json
new file mode 100644
index 0000000..32a7d90
--- /dev/null
+++ b/src/main/resources/packs/createpropulsion/data/cmmodular/miapi/materials/metal/platinum.json
@@ -0,0 +1,84 @@
+{
+ "translation": "Platinum ",
+ "groups": [
+ "metal"
+ ],
+ "icon": {
+ "type": "item",
+ "item": "createpropulsion:platinum_ingot"
+ },
+ "hardness": 5.0,
+ "density": 5.7,
+ "flexibility": 3,
+ "durability": 680,
+ "enchantability": 24,
+ "toughness": 2,
+ "tier": 4,
+ "mining_speed": 7,
+ "mining_level": "minecraft:incorrect_for_diamond_tool",
+ "armor_durability": 24,
+ "armor_toughness": 1.0,
+ "color": "ADCCDE",
+ "color_palette": {
+ "type": "grayscale_map",
+ "colors": {
+ "24": "1b2731",
+ "68": "334b5e",
+ "107": "547086",
+ "150": "6896a4",
+ "190": "8dabc2",
+ "216": "adccde",
+ "255": "dbf5e9"
+ },
+ "filler": "interpolate"
+ },
+ "textures": [
+ "metallic",
+ "shiny"
+ ],
+ "properties": {
+ "default": {
+ "fire_proof": true
+ },
+ "handheld": {
+ "attributes": [
+ {
+ "attribute": "generic.attack_speed",
+ "value": "-0.08",
+ "operation": "+",
+ "slot": "mainhand"
+ }
+ ]
+ }
+ },
+ "items": [
+ {
+ "item": "createpropulsion:platinum_ingot",
+ "value": 1.0
+ },
+ {
+ "item": "createpropulsion:platinum_nugget",
+ "value": 0.11112
+ },
+ {
+ "item": "createpropulsion:platinum_sheet",
+ "value": 1.0
+ },
+ {
+ "item": "createpropulsion:platinum_block",
+ "value": 9.0
+ },
+ {
+ "tag": "c:ingots/platinum",
+ "value": 1.0
+ },
+ {
+ "tag": "c:nuggets/platinum",
+ "value": 0.11112
+ },
+ {
+ "tag": "c:storage_blocks/platinum",
+ "value": 9.0
+ }
+ ]
+}
diff --git a/src/main/resources/packs/createpropulsion/pack.mcmeta b/src/main/resources/packs/createpropulsion/pack.mcmeta
new file mode 100644
index 0000000..330e10f
--- /dev/null
+++ b/src/main/resources/packs/createpropulsion/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "Create Propulsion Materials for Truly Modular",
+ "pack_format": 48
+ }
+}
diff --git a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/crystal/amethyst.json b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/crystal/amethyst.json
index 35fb7d0..798508a 100644
--- a/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/crystal/amethyst.json
+++ b/src/main/resources/packs/iceandfire/data/cmmodular/miapi/materials/crystal/amethyst.json
@@ -1,9 +1,23 @@
{
- "key": "miapi:crystal/amethyst",
- "items": [
- {
- "item": "iceandfire:amethyst_gem",
- "value": 1.0
- }
- ]
+ "parent": "miapi:crystal/amethyst",
+ "data": {
+ "items": [
+ {
+ "item": "minecraft:amethyst_shard",
+ "value": 0.25
+ },
+ {
+ "item": "minecraft:amethyst_cluster",
+ "value": 0.25
+ },
+ {
+ "item": "minecraft:amethyst_block",
+ "value": 1.0
+ },
+ {
+ "item": "iceandfire:amythest_gem",
+ "value": 0.25
+ }
+ ]
+ }
}
diff --git a/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/metal/arcane_metal.json b/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/metal/arcane_metal.json
index f5726d3..3a08da5 100644
--- a/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/metal/arcane_metal.json
+++ b/src/main/resources/packs/irons_spellbooks/data/cmmodular/miapi/materials/metal/arcane_metal.json
@@ -1,5 +1,5 @@
{
- "translation": "Arcane Steel ",
+ "translation": "Arcane Alloy ",
"groups": [
"metal"
],
@@ -36,8 +36,44 @@
"metallic"
],
"properties": {
- "handheld": {
+ "default": {
"attributes": [
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.1",
+ "operation": "*",
+ "slot": "mainhand"
+ },
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.05",
+ "operation": "*",
+ "slot": "head"
+ },
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.04",
+ "operation": "*",
+ "slot": "chest"
+ },
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.03",
+ "operation": "*",
+ "slot": "legs"
+ },
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.03",
+ "operation": "*",
+ "slot": "feet"
+ },
+ {
+ "attribute": "irons_spellbooks:cooldown_reduction",
+ "value": "0.03",
+ "operation": "*",
+ "slot": "offhand"
+ },
{
"attribute": "irons_spellbooks:spell_power",
"value": "0.05",
diff --git a/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/paltaeria_gem.json b/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/paltaeria_gem.json
index 5679de7..2f8ea08 100644
--- a/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/paltaeria_gem.json
+++ b/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/paltaeria_gem.json
@@ -43,6 +43,10 @@
{
"item": "pastel:paltaeria_gem",
"value": 1.0
+ },
+ {
+ "item": "pastel:paltaeria_fragments",
+ "value": 0.125
}
]
}
diff --git a/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/stratine_gem.json b/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/stratine_gem.json
index 0d2d735..353efbd 100644
--- a/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/stratine_gem.json
+++ b/src/main/resources/packs/pastel/data/cmmodular/miapi/materials/crystal/stratine_gem.json
@@ -43,6 +43,10 @@
{
"item": "pastel:stratine_gem",
"value": 1.0
+ },
+ {
+ "item": "pastel:stratine_fragments",
+ "value": 0.125
}
]
}
diff --git a/tools/generate_materials.py b/tools/generate_materials.py
index 71a5c0d..211badc 100644
--- a/tools/generate_materials.py
+++ b/tools/generate_materials.py
@@ -6,19 +6,21 @@ the mod that adds it, so a material always looks like the thing it is made of.
python3 tools/generate_materials.py --jars ~/mods
-The jar directory needs one jar per source mod (Mekanism, Create, Create: The
-Air War, Create: Cosmonautics, Ice and Fire). Generated files are committed, so
-this only needs re-running when a material definition or a source texture
-changes.
+Point --jars at the pack's mods folder. Generated files are committed, so this
+only needs re-running when a material definition or a source texture changes.
Every item id in materials.py is checked against the jars; a typo fails the run
-rather than shipping a material that silently refuses to load.
+rather than shipping a material that silently refuses to load. A mod whose jar
+is not in the folder is skipped instead, keeping its committed files, so the set
+of jars does not have to be complete to regenerate the rest.
+
+Only the material folders are rewritten. The hand-written module JSON that lives
+in the same packs is left alone.
"""
import argparse
import json
import re
-import shutil
import sys
import zipfile
@@ -31,6 +33,7 @@ sys.path.insert(0, str(Path(__file__).parent))
from materials import EXTENSIONS, MATERIALS, PACKS # noqa: E402
NAMESPACE = "cmmodular"
+VANILLA = "minecraft:"
PACK_FORMAT = 48 # 1.21.1
# Brightness levels of MIAPI's grayscale templates. Each gets a colour; values
@@ -56,14 +59,24 @@ class Jars:
def __init__(self, folder: Path):
self.zips = []
self.by_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
+ # mod that actually owns it rather than whichever sorted first.
+ counts = {}
for path in sorted(folder.glob("*.jar")):
zf = zipfile.ZipFile(path)
names = zf.namelist()
self.zips.append((path.name, zf, set(names)))
+ here = {}
for name in names:
m = re.match(r"assets/([^/]+)/", name)
if m:
- self.by_namespace.setdefault(m.group(1), (path.name, zf, set(names)))
+ here[m.group(1)] = here.get(m.group(1), 0) + 1
+ for namespace, count in here.items():
+ if count > counts.get(namespace, 0):
+ counts[namespace] = count
+ self.by_namespace[namespace] = (path.name, zf, set(names))
def _for(self, namespace):
hit = self.by_namespace.get(namespace)
@@ -199,6 +212,38 @@ def check_items(spec, jars: Jars):
return problems
+def check_extension_items(ext, jars: Jars):
+ """Same check for extensions - they name items too, and used to escape it."""
+ problems = []
+ for entry in ext["data"].get("data", {}).get("items", []):
+ item = entry.get("item", "")
+ if item.startswith(VANILLA):
+ continue
+ if item and not jars.has_item(item):
+ problems.append(f"{ext['path']}: no such item {item}")
+ return problems
+
+
+def materials_path(out: Path, spec) -> Path:
+ """Where a material or extension is written."""
+ path = spec["path"] if "path" in spec else f"{spec['group']}/{spec['name']}"
+ return out / spec["pack"] / "data" / NAMESPACE / "miapi" / "materials" / f"{path}.json"
+
+
+def namespaces_of(spec):
+ """Every namespace a material or extension needs a jar for.
+
+ 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.
+ """
+ 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(":")}
+
+
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -212,20 +257,45 @@ def main():
jars = Jars(args.jars)
print(f"namespaces found: {', '.join(sorted(jars.by_namespace))}\n")
+ # A mod whose jar is not in the folder is skipped rather than fatal, and its
+ # committed file is left alone: the pack this addon follows drops and adds
+ # mods, and losing every material of a mod you no longer have - or being
+ # unable to regenerate anything without a complete set of jars - is worse
+ # than regenerating what you can. Anything the jars *can* be checked against
+ # is still checked, so a typo in a mod you do have still fails the run.
+ def present(spec):
+ return namespaces_of(spec) <= set(jars.by_namespace)
+
+ materials = [spec for spec in MATERIALS if present(spec)]
+ extensions = [ext for ext in EXTENSIONS if present(ext)]
+ skipped = [spec for spec in MATERIALS if not present(spec)]
+ skipped += [ext for ext in EXTENSIONS if not present(ext)]
+ if skipped:
+ missing = sorted({ns for spec in skipped for ns in namespaces_of(spec)
+ if ns not in jars.by_namespace})
+ print(f"no jar for {', '.join(missing)} - leaving "
+ f"{len(skipped)} committed file(s) as they are\n")
+
problems = []
- for spec in MATERIALS:
+ for spec in materials:
problems += check_items(spec, jars)
+ for ext in extensions:
+ problems += check_extension_items(ext, jars)
if problems:
print("aborting, item ids do not check out:", file=sys.stderr)
for p in problems:
print(" " + p, file=sys.stderr)
return 1
- # Wipe first. A material that moves between packs would otherwise leave a
- # stale copy behind, and two files claiming the same material id is a silent
- # mess to debug.
- if args.out.exists():
- shutil.rmtree(args.out)
+ # Clear out the material files first. A material that moves between packs
+ # would otherwise leave a stale copy behind, and two files claiming the same
+ # material id is a silent mess to debug. Only the generated material folders
+ # go: the packs also hold hand-written module JSON, and the files of the
+ # skipped mods above have to survive a run that cannot rebuild them.
+ keep = {materials_path(args.out, spec) for spec in skipped}
+ for stale in sorted(args.out.glob(f"*/data/{NAMESPACE}/miapi/materials/**/*.json")):
+ if stale not in keep:
+ stale.unlink()
written = 0
for pack_id, pack in PACKS.items():
@@ -239,23 +309,21 @@ def main():
root.mkdir(parents=True, exist_ok=True)
(root / "pack.mcmeta").write_text(json.dumps(mcmeta, indent=2) + "\n")
- for spec in MATERIALS:
+ for spec in materials:
material = build_material(spec, jars)
- path = (args.out / spec["pack"] / "data" / NAMESPACE / "miapi" / "materials"
- / spec["group"] / f"{spec['name']}.json")
+ path = materials_path(args.out, spec)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(material, indent=2) + "\n")
written += 1
swatch = " ".join(material["color_palette"]["colors"][str(s)] for s in STOPS)
- print(f" {spec['pack']:<13} {NAMESPACE}:{spec['group']}/{spec['name']:<28} {swatch}")
+ print(f" {spec['pack']:<16} {NAMESPACE}:{spec['group']}/{spec['name']:<28} {swatch}")
- for ext in EXTENSIONS:
- path = (args.out / ext["pack"] / "data" / NAMESPACE / "miapi" / "materials"
- / f"{ext['path']}.json")
+ for ext in extensions:
+ path = materials_path(args.out, ext)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(ext["data"], indent=2) + "\n")
written += 1
- print(f" {ext['pack']:<13} extends {ext['data']['key']}")
+ print(f" {ext['pack']:<16} extends {ext['data']['parent']}")
print(f"\n{written} files written to {args.out}")
return 0
diff --git a/tools/materials.py b/tools/materials.py
index 9ebdfe2..5c40a64 100644
--- a/tools/materials.py
+++ b/tools/materials.py
@@ -67,6 +67,17 @@ PACKS = {
"crystal_chronicles": {"mod": "crystal_chronicles",
"name": "Crystal Chronicles Materials"},
"pastel": {"mod": "pastel", "name": "Pastel Materials"},
+ "createpropulsion": {"mod": "createpropulsion",
+ "name": "Create Propulsion Materials"},
+ "aeroengineering": {"mod": "aeroengineering", "name": "AeroEngine Materials"},
+ "create_wizardry": {"mod": "create_wizardry", "name": "Create Wizardry Materials"},
+ # Modules rather than materials, but the same problem: a module that names
+ # Arsenal's or Armory's slots - or inherits one of their modules - is only
+ # useful when that mod is installed. Nothing is generated into these two;
+ # they hold the hand-written module JSON under packs/arsenal and
+ # packs/armory, and are listed here so their pack.mcmeta is written.
+ "arsenal": {"mod": "tm_arsenal", "name": "Arsenal Modules"},
+ "armory": {"mod": "tm_armory", "name": "Armory Modules"},
}
# Attribute this mod registers itself, so a material can grant something the
@@ -341,6 +352,38 @@ MATERIALS = [
enchantability=12, mining_speed=10, toughness=3, armor_durability=36,
armor_toughness=2.0, properties={"default": {"fire_proof": True}}),
+ # Create Propulsion: Simulated's own ore, and the only platinum in the pack.
+ M("platinum", "createpropulsion", "metal", "Platinum",
+ "createpropulsion:platinum_ingot",
+ [{"item": "createpropulsion:platinum_ingot", "value": 1.0},
+ {"item": "createpropulsion:platinum_nugget", "value": NUGGET},
+ {"item": "createpropulsion:platinum_sheet", "value": 1.0},
+ {"item": "createpropulsion:platinum_block", "value": BLOCK}],
+ tier=4, hardness=5.0, density=5.7, flexibility=3, durability=680,
+ enchantability=24, mining_speed=7, toughness=2, armor_durability=24,
+ armor_toughness=1.0, textures=("metallic", "shiny"),
+ properties={"default": {"fire_proof": True},
+ "handheld": speed(-0.08)}),
+ # Real platinum: nearly as dense as osmium, but soft, so it swings slow
+ # and hits no harder than steel. What it is actually for is the precious
+ # metal's bargain - gold-grade enchantability without gold's fragility -
+ # and it melts at 1768C, hence the heat proofing.
+
+ # -------------------------------------------------------------- AeroEngine
+ M("aviation_alloy", "aeroengineering", "metal", "Aviation Alloy",
+ "aeroengineering:aviation_alloy_ingot",
+ [{"item": "aeroengineering:aviation_alloy_ingot", "value": 1.0},
+ {"item": "aeroengineering:aviation_alloy_sheet", "value": 1.0}],
+ tier=5, hardness=6.6, density=2.4, flexibility=4, durability=1650,
+ enchantability=14, mining_speed=10, toughness=3, armor_durability=35,
+ armor_toughness=2.0, textures=("metallic", "bright"),
+ properties={"default": {"fire_proof": True},
+ "handheld": speed(0.08)}),
+ # Iron superheated with netherite powder: a turbine superalloy. Light and
+ # heat-proof like titanium, harder and more flexible, worse to enchant -
+ # it sits between titanium and titanium alloy, which is where its recipe
+ # sits too.
+
# ------------------------------------------------------- Create: The Air War
M("sulfur", "air_war", "crystal", "Sulfur", "create_the_air_wars:raw_sulfur",
[{"item": "create_the_air_wars:raw_sulfur", "value": 1.0},
@@ -589,14 +632,44 @@ MATERIALS = [
properties={"handheld": speed(0.12)}),
# Light enough to swing fast and enchant well, in the Tolkien sense.
- M("arcane_metal", "irons_spellbooks", "metal", "Arcane Steel",
+ # "Arcane Alloy" rather than Arcane Steel: Create: Wizardry ships a resource
+ # pack that renames the ingot, and that is what the item is called in game.
+ M("arcane_metal", "irons_spellbooks", "metal", "Arcane Alloy",
"irons_spellbooks:arcane_ingot",
[{"item": "irons_spellbooks:arcane_ingot", "value": 1.0}],
tier=5, hardness=6.2, density=2.8, flexibility=3, durability=1700,
enchantability=26, mining_speed=9, toughness=3, armor_durability=34,
armor_toughness=2.0,
- properties={"handheld": {"attributes": [
- attribute("irons_spellbooks:spell_power", 0.05, "mainhand", "*")]}}),
+ # The one thing arcane alloy is for: spells come back faster. Cooldown
+ # reduction is the whole reason to build out of it rather than out of
+ # mithril, so it is deliberately the strongest effect on any metal here.
+ #
+ # Under `default`, not `handheld`, so armour plated in it counts too - a
+ # caster wants the set, not just the sword. Per-slot rather than one
+ # `armor`-wide modifier, for the same reasons as arcane essence: a helmet
+ # can be worth more than boots, and a weapon reads as a held bonus
+ # instead of claiming something "when worn".
+ #
+ # Iron's Spells treats the attribute as a factor on a base of 1, hence the
+ # `*` operation - a flat `+` would be added to a base of zero and do
+ # nothing - and spends it as `cooldown * (2 - value)`. So +0.10 is 10% off
+ # the wait, and +1.00 in total would be instant casting.
+ #
+ # These stack per module, and that is what sets the numbers. A weapon is
+ # about four modules, so one built entirely of arcane alloy is 40% - well
+ # past the 25% of Iron's Spells' own best staff, which is the point. The
+ # armour values are smaller because a full set is a dozen modules: all of
+ # it in arcane alloy lands near 45%, so even a player wearing the set and
+ # holding the staff stays under the 100% the formula bottoms out at.
+ properties={"default": {"attributes": [
+ attribute("irons_spellbooks:cooldown_reduction", 0.10, "mainhand", "*"),
+ attribute("irons_spellbooks:cooldown_reduction", 0.05, "head", "*"),
+ attribute("irons_spellbooks:cooldown_reduction", 0.04, "chest", "*"),
+ attribute("irons_spellbooks:cooldown_reduction", 0.03, "legs", "*"),
+ attribute("irons_spellbooks:cooldown_reduction", 0.03, "feet", "*"),
+ attribute("irons_spellbooks:cooldown_reduction", 0.03, "offhand", "*"),
+ attribute("irons_spellbooks:spell_power", 0.05, "mainhand", "*"),
+ ]}}),
# ------------------------------------------------- Immersive Engineering
# Steel, lead, uranium and silver are not here - Immersive Engineering adds
@@ -1064,14 +1137,26 @@ _GEM_BAND = {
}
+# Nuggets, in effect: Pastel's two float-gems break into eight fragments on an
+# anvil and nine come back out of a floatblock, so a fragment is an eighth of a
+# gem and worth accepting as one.
+GEM_FRAGMENTS = {
+ "paltaeria_gem": ("pastel:paltaeria_fragments", 0.125),
+ "stratine_gem": ("pastel:stratine_fragments", 0.125),
+}
+
+
def _gem_family(pack, namespace, table):
out = []
for item, (tier, props) in table.items():
label = item.replace("_shard", "").replace("_crystal", "").replace("_gem", "")
+ items = [{"item": f"{namespace}:{item}", "value": 1.0}]
+ if item in GEM_FRAGMENTS:
+ fragment, value = GEM_FRAGMENTS[item]
+ items.append({"item": fragment, "value": value})
out.append(M(
item, pack, "crystal", label.replace("_", " ").title(),
- f"{namespace}:{item}",
- [{"item": f"{namespace}:{item}", "value": 1.0}],
+ f"{namespace}:{item}", items,
tier=tier, groups=("crystal", "gem"), textures=("crystal", "bright"),
properties={"default": props} if props else None,
**_GEM_BAND[tier]))
@@ -1100,21 +1185,50 @@ for _material in MATERIALS:
g for g in GEM_SOCKET_GROUPS if g not in _material["groups"]]
-# Extensions add data to a material that already exists rather than replacing it.
-# Ice and Fire's amethyst is the same stone MIAPI already ships, so it just
-# becomes another way to craft it.
+# Extensions edit a material that already exists instead of adding a new one.
+# The file goes in the materials folder like any other and is told apart by its
+# shape: `parent` names the material it edits and `data` is merged onto a copy
+# of it - the same form MIAPI's own stained glass uses to vary plain glass.
+#
+# The merge overwrites whole fields rather than appending to them, so an
+# extension that adds an ingredient has to repeat the ones it wants to keep.
EXTENSIONS = [
+ # Ice and Fire's amethyst is the same stone MIAPI already ships, so it just
+ # becomes another way to craft it. The item id really is spelled
+ # "amythest_gem" - that typo is in Ice and Fire itself.
{
"pack": "iceandfire",
"path": "crystal/amethyst",
"data": {
- "key": "miapi:crystal/amethyst",
- "items": [{"item": "iceandfire:amethyst_gem", "value": 1.0}],
+ "parent": "miapi:crystal/amethyst",
+ "data": {
+ "items": [
+ {"item": "minecraft:amethyst_shard", "value": 0.25},
+ {"item": "minecraft:amethyst_cluster", "value": 0.25},
+ {"item": "minecraft:amethyst_block", "value": 1.0},
+ {"item": "iceandfire:amythest_gem", "value": 0.25},
+ ],
+ },
},
},
]
+# Forms of a material that a *different* mod adds. Create: Wizardry presses
+# Iron's Spells' arcane ingots into sheets and stacks them into blocks, and adds
+# the mithril nugget the base mod never had, so all of those are the same metal.
+#
+# They cannot simply be listed on the material: it lives in the Iron's Spells
+# pack, and an item id belonging to a mod that is not installed stops the whole
+# material from loading. An extension in Create: Wizardry's own pack is only
+# read when that mod is there.
+EXTRA_ITEM_FORMS = {
+ "arcane_metal": [("create_wizardry:arcane_sheet", 1.0),
+ ("create_wizardry:arcane_block", BLOCK)],
+ "mithril": [("create_wizardry:mithril_nugget", NUGGET)],
+}
+
+
# ------------------------------------------------- identical materials, one entry
# Several of these metals and gems are added by more than one mod. Every
# variant should behave as the same material rather than as a near-duplicate,
@@ -1143,7 +1257,7 @@ COMMON_TAGS = {
"energetic_alloy": "energetic_alloy", "vibrant_alloy": "vibrant_alloy",
"conductive_alloy": "conductive_alloy", "pulsating_alloy": "pulsating_alloy",
"redstone_alloy": "redstone_alloy", "soularium": "soularium",
- "mithril": "mithril",
+ "mithril": "mithril", "platinum": "platinum",
}
COMMON_GEM_TAGS = {
@@ -1178,3 +1292,30 @@ def _accept_every_provider():
_accept_every_provider()
+
+
+def _extend_with_extra_forms(pack="create_wizardry"):
+ """Turn EXTRA_ITEM_FORMS into material extensions.
+
+ Built after the common tags are appended, and out of the material's own item
+ list, because the merge replaces the ingredient list rather than adding to
+ it - everything the material already accepted has to be carried over.
+ """
+ by_name = {material["name"]: material for material in MATERIALS}
+ for name, extras in EXTRA_ITEM_FORMS.items():
+ base = by_name[name]
+ path = f"{base['group']}/{name}"
+ EXTENSIONS.append({
+ "pack": pack,
+ "path": path,
+ "data": {
+ "parent": f"cmmodular:{path}",
+ "data": {
+ "items": base["items"] + [{"item": item, "value": value}
+ for item, value in extras],
+ },
+ },
+ })
+
+
+_extend_with_extra_forms()