Compare commits

...

23 Commits

Author SHA1 Message Date
Amir Alexander Abdelbaki 8f253a848a Write every model that moved, and say which ones are waiting
`Write to source` saved the selected row and only that. Nudge three things into
place, click the third and press it, and two of them were still sitting in
memory with nothing on screen to say so - which reads exactly like a button
that does not save, because the file you go and look at is usually not the one
that was selected when you pressed it.

So it writes everything that has moved. A row whose model no longer matches
what its module declares is marked with a `*` until it has been written, so
what is pending is on the list rather than in your memory, and the report says
how many files were written and which. A model no module names - a sword part,
a gem case, an icon of its own - has nowhere to put a transform, and is now
named in that report rather than passed over in silence: it keeps its mark,
because it really is moved and really is unsaved.

Nothing about the writing itself changed. It was tested one model at a time,
which is the one case where the old behaviour was indistinguishable from the
new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:31:00 +02:00
Amir Alexander Abdelbaki 8e5178ece6 A hue per object, and faces that cannot be confused with their opposite
Untextured, every model came out the same red. The hue was the box's index
within its model and almost every model here is one box, so a scene of eight
was eight objects sharing hue zero - the one thing the colour was supposed to
tell you was the one thing it did not. So `face_colour` takes a hue now, and
the quickstart hands each model its own: successive golden-ratio turns around
the wheel, so any two are far apart and the third is not squeezed between them,
indexed on the model's place in the whole list rather than on what happens to
be visible. The boxes of one model spread a tenth of a turn either side, near
enough to read as one object and far enough to count. Left alone the hue is
what it always was - the wheel split per shape - so ARMOUR_GUI and the unwrap
templates are unchanged.

The shades were the other half. Six faces were sharing three saturations, and
east and west were separated by nothing but a fifth of a value - which is the
worst place to be thin, because opposite faces are the pair you can only ever
see one of at a time and so have nothing to compare against. Each face now sits
at its own rung of both ladders, and each opposite pair is far apart on both:
up is pale and brightest against a down that is vivid and darkest, north vivid
and bright against a washed, dim south, west the palest against a vivid,
mid-dark east.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:23:45 +02:00
Amir Alexander Abdelbaki 44bd5e597c model adjustments 2026-08-24 21:17:12 +02:00
Amir Alexander Abdelbaki 6dba9d9f24 Two buttons that write: the transform back, and the UVs afresh
The quickstart could show that a model was in the wrong place and then leave you
to retype the number somewhere else. `Write to source` closes that: the panel's
translation and rotation go back into the module entry they were read from,
with its scale and its origin left as the module had them, because placement
lives in the module rather than in the geometry. A model no module names - a
sword part, an icon of its own - has nowhere to write to and says so instead of
guessing at a file. Whole numbers stay whole, so an axis a drag never touched
comes back reading `0` rather than `0.0`.

`Unwrap UVs` is the other half, and it is the same unwrap ARMOUR_GUI does when
it saves: every face gets a rectangle of its own, the atlas size is written
beside the boxes, and the faces keep the texture key they already name rather
than being pointed at `#0`. A template is painted to match only where there is
no texture yet - a guide is scaffolding, and overwriting art someone has
painted because the boxes moved would be the tool destroying the work it exists
for. A sprite has no boxes to cut, and says that rather than writing an empty
atlas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 21:01:16 +02:00
Amir Alexander Abdelbaki 9449af92b1 Placeholder bezels for the small and great cases
The medium case has had art of its own since the model adjustments - a bezel
around an empty socket, on the greys a material's palette maps - even though
the module wears MIAPI's gemstone model at the moment. The two new sizes had
nothing at all, so here are their blanks, drawn to the same rules: the top and
left edges catch the light, the right edge is the shadow, the bottom is the mid
tone, and the socket in the middle stays the darkest stop so it reads as empty
whatever is in it. Three pixels across for the small, six for the great, either
side of the medium's four.

They are placeholders in the sense the templates are: the right size, the right
palette, and something to draw over rather than finished art. Each gets the
model beside it that the medium one has, so wiring a case to its own picture is
a path change in the module and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:55:25 +02:00
Amir Alexander Abdelbaki 1f1ca94a17 An Apothic case in every gem size, weapons included
A gem slot takes what its `allowed` list names, and until now the case named
only `gem_armor_medium` - so Apotheosis could be socketed into a chestplate and
not into a sword, though Apotheosis itself sockets gems into either. The
generic case now names `gem_melee_medium` beside it and carries the matching
`gem_melee` tags, which is the whole of what makes it fit the socketed pommel.

Size is the other half of fitting, and a case that is one size fits one slot.
So there are three: the medium one that was here, a great case for the
`gem_melee_large` slot the great pommel offers, and a small case for
`gem_melee_small` - Arsenal's own socket pommel, and the dual socket guard that
carries two of them. The small one names `gem_armor_small` as well, because a
small slot is a small slot wherever it was cut and a key nothing asks for costs
nothing. Each wears MIAPI's gemstone model at its own size, as the medium one
has since the limb gems went in.

`ApothicSockets` counts cases rather than pieces, so the new ids go on its
list and nothing else changes: a sword with a case in its pommel reports one
socket the way a chestplate does, and a guard holding two small cases reports
two. All three are filed under `packs/apotheosis` rather than with the pommels,
because a case without Apotheosis is an item that grants nothing, while a case
without Arsenal is one that simply never fits anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:53:30 +02:00
Amir Alexander Abdelbaki e7b211932f A viewport that draws, and a front door that opens with no arguments
The armour tools would not start and then would not draw, for two reasons that
looked like one. pyvista and pyvistaqt are packaged by hardly any distribution,
so `guiplatform` now keeps a virtualenv at `tools/.venv` and re-runs the tool
inside it when the interpreter it was started with cannot find the stack -
built with `--system-site-packages`, so the distribution's Qt stays the Qt in
use. Only an absent module sends us there; a PySide6 that will not load is a
system problem the venv shares, and it still gets its own message.

The black viewport was the other half, and it was ours. Handing pyvistaqt a
`vtkGenericOpenGLRenderWindow` is the right idea for VTK's C++ widget and wrong
for this one: the Python `QVTKRenderWindowInteractor` paints from `paintEvent`,
never implements `paintGL`, never binds Qt's framebuffer and never makes a
context current, so the render window it was given drew into no context at all.
Nothing raised - VTK would not even read its own buffer back. pyvistaqt makes
its own render window now, which is the only one it knows how to drive, and the
probe builds its viewport the same way the tools do rather than a way that
always failed. That probe had been timing out on every launch for the same
reason, so Wayland could never be chosen anywhere; it now says what actually
went wrong, X protocol errors included.

ARMOUR_QUICKSTART is the front door: no arguments, every model in the tree -
worn armour, icons, sword parts, the loose item models - listed with the first
one showing, and a jar's models listed after ours when one is passed, because a
socket is geometry cut into a plate that lives in Armory's jar. Where each
model goes comes from the mod's own module data, since the `origin` a module
names is the part MIAPI draws it under; without Armory's slot transforms a worn
model is flipped onto its part but not offset along it, and it says so.

The left panel is that model's MIAPI transform. Three arrows on the model's
origin move it, a toggle swaps them for three rotation rings, and the numbers
are the ones a module writes - a drag is unwound back through the pivot and the
flip before it reaches them, so what the panel reads is what goes in the JSON.
Nothing here writes: the number is what you leave with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:53:07 +02:00
Amir Alexander Abdelbaki 7442ee7ac7 A front door for the armour editor, and names that say what to run
Running it needed a scene and a jar path known in advance, which is a poor way
to meet a tool. With no scene named it now asks: the four scenes listed with
their own docstring's first line as the description, so the blurb cannot drift
from what the scene builds, and the sources pre-filled with the repo's own
resources first and Armory's jar after, found by looking where the pack's jars
actually live. Textures, wearer, material variant and gem size are there too.

The dialog is Qt and nothing else. The viewport it leads to is a plotter with a
window of its own, so there is no shared GL context to get wrong and no reason
to sit through the Wayland probe before a scene has been picked; the
QApplication is shut down before the viewport opens rather than left to run a
second event loop. `--shot` still insists on a scene, because it renders
without a window and an unattended render should not stop to ask.

Widgets carry object names. That began as a way to drive them from a test after
addressing them by index quietly drove the wrong ones, and it is right anyway -
an index moves the next time a row is added above it.

The three tools you start are ALL CAPS now and the libraries are not, so the
directory says which is which. main() also stops adding the repo's resources a
second time when the dialog has already listed them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:18:33 +02:00
Amir Alexander Abdelbaki 742705839d Gems that stay on the limb they were cut into
MIAPI draws a module's model in the pass its own origin names, and positions it
from the entry of the transform stack filed under that same name - a model with
no origin is drawn only for the inventory icon, from the entry a transform lands
in when it names none. Armory's gemstone names no origin, so it was never drawn
on worn armour by any route, and a slot transform aimed at the body reached
neither the limb nor the icon.

So the gems are ours now: one per limb, tagged so a left pauldron gem cannot go
in a knee and be drawn on the wrong arm, and an Apothic case beside each on the
same model and placement. The cases keep counting one Apotheosis socket apiece -
ApothicSockets already listed these module ids, so the Java is unchanged and two
armed cases still make two sockets on the chestplate.

Each carries two model entries, because one transform cannot aim both places: a
limb entry drawn under the part's animated pose, so it follows the walk cycle,
and an origin-less entry with its own offset for the icon. The limb placements
were solved rather than tried - the target taken in body space, Armory's limb
chain inverted, the result decomposed - and land on the pauldron face and the
knee to the pixel. The icon offsets sit on the pauldron boss and the knee band
of each of the four sprites.

The sockets themselves are drawn here too, under the limb origin, so there is a
setting for the stone to sit in. Their textures are unwrap templates for now and
want painting. The Apothic case wears the gemstone's model rather than its own
art, its materials unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:12:58 +02:00
Amir Alexander Abdelbaki 5e1b62fd9b Tools that draw the armour instead of guessing at it
Five of them, sharing one geometry layer. mcmodel.py reads Minecraft and MIAPI
models out of jars and implements both of the game's UV conventions separately -
entity cubes for vanilla armour, JSON model faces for every module - because
they disagree on four of six faces, which survives an eyeball on a symmetric
breastplate and then ruins a pauldron.

armour_editor.py places modules and writes the numbers back into the JSON they
came from. It replaces what preview_armour.py did and fixes what that got
wrong: sorting whole faces by average depth is not a depth buffer, and a gem
half-sunk into a plate was sorting arbitrarily - exactly the case the tool
existed to judge. VTK rasterises against a real z-buffer instead.

armour_gui.py draws new geometry onto a body part, with the origin settable,
since that is what decides which part a model is drawn under and therefore what
it moves with. It unwraps as it goes and writes a painting template beside the
model. The untextured view colours a hue per shape and a shade per face, and
the template uses the same key, so one is the legend for the other.

material_editor.py edits materials.py by rewriting one argument's source span
at a time, so ingots(...) stays a call, the comments stay put, and a two-number
change is a two-number diff. The generated JSON is downstream and would be
overwritten, so it is not what gets edited.

guiplatform.py prefers Wayland and falls back to X11. VTK's wheels have no
Wayland backend, but handing Qt the GL context works on both; where that fails
the process dies on an X BadAccess that nothing in-process can catch, so the
question is asked in a subprocess that renders the same features the tools use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:12:31 +02:00
Amir Alexander Abdelbaki 532c3a0da8 model adjustments 2026-08-18 17:50:53 +02:00
Amir Alexander Abdelbaki 87a78136ec The fang's element, the core's new corners, and gem slots for limbs
Sea serpent fang was a fang anywhere: one of them, socketed, worn or held,
and you breathed underwater. It is an armour material now, and worth
something different in hand.

Worn, it grants half a lung per module in each armour slot and the handler
still only reads 1 as breathing, so it takes two worn fang modules - two
sockets, two pieces built of it, or a socketed piece that is both. That is
the trade the space set already makes, and a fang weapon no longer makes it
at all.

Held, it does what a serpent does in its element. Mining needs nothing of
ours: vanilla models underwater digging as an attribute of its own,
minecraft:player.submerged_mining_speed, base 0.2 for the fifth-speed
penalty, and +0.6 a module puts a single fang module four times past a plain
tool and two of them past dry land. Damage and swing speed have no such
switch, so WaterCombat.java registers cmmodular:water_combat the way
FireImmunity and WaterBreathing register theirs. Damage is answered on the
blow. Attack speed cannot be - the game reads it off the attribute map
whenever it likes - so a transient modifier under one id goes on and comes
off as the bearer enters and leaves the water.

The arcana core's recipe puts a trophy from each corner of the game in the
corners of the grid - nether star, dragon's breath, heart of the sea and
Iron's Spells' ancient knowledge fragment - with two cinder and two arcane
essence on the edges and nothing in the middle.

Armory sockets a helmet, a chestplate, a backplate and a belt and stops
there, so an arm or a leg could never hold a gem. Four pieces here finish it:
the heavy pauldron and the heavy pants, left and right, inheriting Armory's
own whole and adding the same gem_armor_medium slot at the same price, 5% of
the piece's armour. Where the gem sits is the other half - out on the
shoulder in the pauldron, forward on the knee in the pants - and the numbers
come from Armory's own art, which lies a sprite proud of the side of an arm
at 2.15 and of the front of a body at 2.35.

Last, a gem case, for anyone running Apotheosis. Its gems are not modules and
never can be, since what makes one worth having is its own rarity and purity
data, but Apotheosis counts sockets in a plain data component and MIAPI's
component property sets any component a module likes. So the case is one line
of data: put it in an armour piece's gem slot instead of a gemstone and the
piece reports an Apotheosis socket, which the gem goes into at a smithing
table. Modular armour extends vanilla ArmorItem, so Apotheosis' LootCategory
already calls it a helmet or a chestplate, and gem bonuses arrive through
ItemAttributeModifierEvent, where MIAPI is not looking. The pack is gated on
Apotheosis and Armory both, which is why a pack can now name more than one
mod.

None of this is compiled or run - there is no JDK on the machine it was
written on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:46:59 +02:00
Amir Alexander Abdelbaki 3f3b4bdef6 further update 2026-08-15 01:40:59 +02:00
Amir Alexander Abdelbaki 5098b5617a Make the near-invisible pixels in the drawn art opaque
Eight pixels of the khopesh blade and five of the great socketed pommel
were saved with an alpha between 1 and 27 out of 255. They have colours
- white, greys, near-black - and were clearly drawn rather than erased,
but at under 11% opacity they read in game as holes in the edge of the
blade. A soft or part-opacity brush is the likely cause; nothing else in
the mod's art has an alpha that is neither 0 nor 255.

Their colours are untouched. Only the alpha moves, to 255.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:50:46 +02:00
Amir Alexander Abdelbaki 891ea12338 Name materials with the lang key MIAPI actually reads
Every material showed as miapi.material.cmmodular.metal.arcane_metal in
game rather than as Arcane Metal. The name was being written as a
`translation` field on the material, which MIAPI reads only when a
`fake_translation` sits beside it - the two are a pair that registers a
name for a key at runtime, and alone the first does nothing. MIAPI's own
materials carry no such field: a material is named by the lang key
miapi.material.<namespace>.<group>.<name>, which is its id with the
separators turned into dots, and MIAPI says so in the warning it logs
about it.

So the generator writes those keys into the mod's lang file, and the
dead field is gone from all 128 materials. The value is the bare name -
the space before the module's own name is MIAPI's to add, which is why
the old field carried a trailing space that no longer belongs.

Names are written for every material, not only the ones a jar was found
for, so generating against a partial set of jars cannot strip the rest
of the file. Hand-written keys - modules, attributes, the arcana core -
are matched by prefix and left alone.

Includes the redrawn great socketed pommel texture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:47:02 +02:00
Amir Alexander Abdelbaki 85eee5aeec File material extensions under their own name
An extension was written to the same path as the material it extends,
one pack over. A datapack file is identified by its path, so that never
added an extension to anything: it replaced the material with a stub,
whichever of the two packs loaded last. The stub then named itself as
its own parent, which MIAPI reads as a cycle and drops, and the material
was gone entirely for anyone with both mods installed:

  Unresolved extension cmmodular:metal/arcane_metal for target
  cmmodular:metal/arcane_metal (missing or cyclic dependency)

That is arcane metal and mithril, the two materials Create: Wizardry
adds forms of, and it explains why they worked before the extensions
existed. Ice and Fire's amethyst was never affected - it extends a
miapi: material, so its own cmmodular: path collided with nothing.

The generated name carries the pack that owns the extension, since the
pack is why the extension exists and there could be another from a
different mod later. Nothing else about the files changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:33:05 +02:00
Amir Alexander Abdelbaki 766d8afe2b khopesh texture 2026-08-14 15:19:24 +02:00
Amir Alexander Abdelbaki 1f0b4c5848 Stop build.sh cleaning away the jar it just built
Arguments were appended to the default task, so build.sh clean build ran
gradlew build clean build. Gradle folds that back into build and clean,
in that order, and the run ended by deleting the jar - with the script
then reporting the jar was missing, which was at least honest. The
README had claimed the invocation worked; nobody had run it.

A named task now replaces the default instead of being appended to it.
Flags are told apart from tasks by their leading dash, so --info still
gets a task to attach to rather than running gradlew with nothing to do.

The closing jar report is now conditional on a task that produces one
having run, since build.sh clean properly ends with no jar and should
not call that a failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:04:57 +02:00
Amir Alexander Abdelbaki 9239f03fe0 Call the arcane metal what it is
The material was named Arcane Alloy on the grounds that Create: Wizardry
ships a resource pack renaming the ingot to that. It does not, and there
is no arcane alloy in any of these mods to rename it to. Iron's Spells
calls the item an Arcane Ingot and the material plain Arcane, in the
tetra.material lang keys it already ships.

It is Arcane Metal here rather than Arcane, because a material is named
for the substance and not the shape it is traded in, and because plain
Arcane belongs to the essence, which is the one of the two with no other
name to fall back on. So the pair now reads Arcane and Arcane Metal.

The generated JSON is edited in place rather than regenerated: the only
field that moves is the translation, and no jar of Create: Wizardry is
around to sample against - regenerating would have gone and rewritten
palettes off whatever jars are to hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:02:35 +02:00
Amir Alexander Abdelbaki 5411797799 Add a build script that picks a JDK Gradle can run on
A plain ./gradlew build fails outright on a machine whose default java
is newer than 23, which is now an ordinary thing for a distro to ship.
Gradle 8.12 cannot run there, and what it says about it is that it
could not create task ':test' because type T is not present - nothing
about Java, and nothing to act on.

build.sh picks a JDK between 17 and 23 and builds with that. JAVA_HOME
wins if it already points at a usable one, otherwise the openjdk paths
are tried in turn; if there is nothing to pick it says so and names the
range, rather than leaving the Gradle error to be decoded. Arguments
pass through, so it is a wrapper around the wrapper and not a second
way to describe the build.

It reports the jar by reading mod_id and mod_version out of
gradle.properties instead of listing build/libs, which keeps old
versions sitting in that directory from being mistaken for the build
that just ran.

The toolchain is untouched by any of this - the mod still compiles
against Java 21, whatever Gradle itself is running on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 14:50:45 +02:00
Amir Alexander Abdelbaki cd8993ea25 Recolour the real heart of the sea for the arcana core
The texture was drawn to that silhouette rather than taken from it,
because there was no copy of the vanilla assets on hand. There is now:
the item texture is Minecraft's own heart of the sea with its hue
rotated from cyan into violet and nothing else touched, so the shading
is the vanilla item's.

That makes it the one piece of art here that is not drawn for this mod,
which the README now says next to the khopesh paragraph that takes the
opposite line. It also means the palette the material hands out is a
deep one with almost no highlight - the heart of the sea is a dark item,
and the sampler reports what a texture looks like rather than re-lighting
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:07:57 +02:00
Amir Alexander Abdelbaki 29f78fa721 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>
2026-08-14 12:02:19 +02:00
Amir Alexander Abdelbaki dcf3b7e9c3 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>
2026-08-14 11:39:02 +02:00
194 changed files with 7323 additions and 197 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ runs/
repo/
*.class
__pycache__/
# Python virtualenv for the tools in tools/
tools/.venv/

380
README.md
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 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
@ -9,8 +9,12 @@ 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.
Arsenal, a sealed helmet and life support pack for Armory that between them
let you breathe in the vacuum Create: Cosmonautics puts above the atmosphere,
socketed arms and legs for the armour pieces Armory leaves without a gem slot,
and a gem case that turns one of Armory's gem slots into an Apotheosis one.
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 +24,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,
@ -58,10 +62,65 @@ 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 armour:
`WaterBreathing.java` answers the same `LivingBreatheEvent`, but only when the
wearer's eyes are in water, so a fang is a fang rather than half a spacesuit.
Half a lung per module, the same trade the space set makes, so it takes two
worn fang modules and a fang weapon does nothing at all - see below.
**Fighting underwater.** The other half of what a fang is for.
`WaterCombat.java` registers a `cmmodular:water_combat` attribute that a fang
grants in the main hand, and while the bearer is in water it adds that fraction
to the damage they deal and to how fast they swing. Damage is answered on the
blow. Attack speed cannot be - the game reads it off the attribute map whenever
it wants to know - so a transient modifier under one id goes on and comes off as
the bearer enters and leaves the water. Mining needs none of this: vanilla
already models underwater digging as an attribute,
`minecraft:player.submerged_mining_speed`, and the material simply adds to it.
**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.
**Counting gem cases.** `ApothicSockets.java` makes each Apothic gem case worth
an Apotheosis socket, which data cannot do at all - every route through MIAPI's
component properties is either dead or crashes the crafting screen, see below.
It is the one place this mod reaches into another's internals, and it does it
reflectively so that neither Apotheosis nor Truly Modular becomes something to
build against.
**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
rather than generated, and gated on the Truly Modular addon whose slots they use.
Four 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 - and, for the gem case, on Apotheosis as well.
**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
@ -81,6 +140,137 @@ 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.
Either socket takes an Apothic gem case as readily as a gemstone - see below.
**Socketed arms and legs** (`packs/armory`) finish a set Armory started.
Armory sockets four pieces - helmet, chestplate, backplate and belt - and stops
there, so an arm or a leg is a piece of armour that can never hold a gem. These
four are the missing ones: the heavy pauldron and the heavy pants, left and
right, each with the same `gem_armor_medium` slot the rest of the set uses and
the same price, 5% of the piece's armour.
They inherit Armory's heavy pieces whole and change two things: the slot, and
where the gem sits in it.
Where the gem sits is the unfinished part of these four, and the honest summary
is that it is placed by trial rather than by arithmetic. MIAPI draws a worn piece
once per body part; which part a module is drawn under comes from its own model's
`origin`, and where it sits comes from the matching entry of its slot's transform
stack. Armory's gemstone declares no origin, so a gem is drawn once, in the
`body` pass, from the `body` entry - which means a gem slot is placed in body
space no matter which limb it is cut into, and is pinned to the torso rather than
to the limb it belongs to.
The numbers are body-space pixels from the neck: +x to the wearer's left, +y
down, -z forward. A pauldron gem is turned a quarter turn about y to face out of
the shoulder; a knee gem faces forward already. Right-hand pieces are the same
numbers with x and the turn negated.
They are not right yet, and two attempts to make them right are worth recording
so that they are not repeated. Naming the limb as the gem slot's `origin` - so
the offset lands in the entry the limb pass reads and the gem follows the arm -
composes correctly on paper and in game puts the gems several blocks away from
the wearer, floating. And `tools/preview_armour.py`, which applies the same chain
MIAPI does and draws the result from any angle, agrees with the paper rather than
with the game: given the configuration that in practice left gems at the neck, it
puts them on the pauldron. The tool is worth keeping for reading Armory's own
geometry - where the plate bands are, which face is which, how far the chain
scales a limb - and it should not be trusted to predict a gem's final position
until it can reproduce a symptom somebody has actually seen.
One thing does not survive any of this and is not fixable from here. A slot
transform that names an origin has nothing left for the inventory icon, which
reads a different entry of the same stack, so the gem sits at the centre of the
icon - the collar of a chestplate, which is where Armory leaves its own, and the
crotch of a pair of pants, which is where this one leaves ours. The two
placements come from one transform and cannot both be aimed.
The pieces take any medium armour gemstone, which includes the Apothic gem case
below - so a pauldron can hold an Apotheosis gem as readily as a Truly Modular
one.
**The Apothic gem case** (`packs/apotheosis`) trades a Truly Modular gem slot
for an Apotheosis one. Apotheosis gems are not modules and never can be - what
makes one worth having is its own rarity and purity data, which a MIAPI
material cannot carry - so the two systems cannot share a slot. What they can
share is the item. Apotheosis stores how many gems a thing holds in a plain
data component, `apotheosis:sockets`, and MIAPI can set any component a module
likes: put a case in an armour piece's `gem_armor_medium` slot instead of a
gemstone, and the piece reports an Apotheosis socket. The gem itself goes in at
a smithing table, through Apotheosis' own socketing recipe, not at the workbench.
A weapon takes one too, and there is a case per gem size to say so. A slot
accepts what its `allowed` list names: the socketed pommels above ask for
`gem_melee_medium` and `gem_melee_large`, and the baseline sockets - Arsenal's
own socket pommel, and the dual socket guard that carries two of them - ask for
`gem_melee_small`. So the case is allowed in the medium slots, a **great case**
answers for the large, and a **small case** for the small, which is the same
trade in a sword that it is in a pauldron. The small one names the armour size
key as well, because a small slot is a small slot wherever it was cut and a key
nothing asks for costs nothing.
Nothing else changes. Sockets are counted per case rather than per item, so a
sword with a case in its pommel reports one the same way a chestplate does and
a guard holding two small cases reports two; Apotheosis sockets gems into
weapons already. All three are filed under `packs/apotheosis` rather than with
the pommels, because a case without Apotheosis is an item that grants nothing,
while a case without Arsenal is one that simply never fits anywhere.
One socket a case, and none of it is in the module JSON, because every route
through data is closed. MIAPI's `components` property sets a component rather
than adding to it, so two cases both writing `1` leave the piece with one socket
between them. The property that could resolve a count instead of a constant,
`advanced_components`, is written but never registered in MIAPI 2.3.8 -
`PropertyRegistry` wires up `components` and `material_component_property` and
never mentions it - so that key resolves to nothing. And the one that does work
turned out to be worse than useless: `ComponentProperty.preview` dereferences the
workbench without a null check, MIAPI passes it null while hovering an option in
the replace list, and `shouldExecuteOnCraft` only calls it for a module that
carries the property - so a gem case with a `components` line crashed the
crafting screen, and only a gem case did.
So `ApothicSockets.java` answers it instead, and Apotheosis makes that easy by
asking the question out loud: `SocketHelper` reads the component and then fires
`GetItemSocketsEvent` so anything may revise the answer. This counts the cases on
the stack and adds one for each. Nothing writes the component now, which is
better than the data version managed: a socket cut by a Sigil of Socketing
survives and the cases stack on top, where the component used to overwrite it.
It reaches both mods reflectively rather than compiling against them. Neither is
a build dependency here and one socket is not worth making them one, so the seven
members it needs - three off MIAPI's module tree, three off the event, and the
event class itself - are resolved once at startup and cached. Missing classes
mean the mods are not installed, which is ordinary and silent; a missing method
means something was renamed, which is logged, because it is a thing to fix rather
than a thing to expect. Either way the count falls back to what the component
says on its own.
That leaves the two systems where they each work best. Apotheosis reads the
socket count through `SocketHelper`, which classifies the item with
`LootCategory` - and modular armour extends vanilla `ArmorItem`, so it is a
helmet or a chestplate to Apotheosis rather than nothing at all. Gem bonuses
arrive as attribute modifiers on the stack, through `ItemAttributeModifierEvent`,
which is where MIAPI is not looking; MIAPI writes its own numbers into the
`ATTRIBUTE_MODIFIERS` component, and the two add up rather than overwrite.
It is drawn here rather than borrowed. It used to wear Armory's medium gemstone
model, which was the least work and the wrong picture: a case is the setting,
not the stone, and one that looks like a gemstone is indistinguishable from the
gemstone it is an alternative to. So `items/gem_case.png` is a four-by-four
bezel around an empty two-by-two socket - a gemstone's own footprint, with the
mount drawn around it - lit from the top left like the pommel sockets, and on
the same greys the materials' palettes map, so it comes out in whatever it was
built from. The socket stays empty-looking whatever is in it, which is honest:
an Apotheosis gem lives in a component and MIAPI has nothing to render for it.
The bezel is twice a gemstone across, which would put a case at twice a
gemstone's size in a slot sized for one, so the module's own model transform
carries a scale of 0.5 and hands the slot back a gemstone's footprint. That is
the module's business rather than the slot's: a socket that holds either should
not have to be scaled differently depending on which went in.
One thing worth knowing: a gem sitting in a case stops counting if the case is
removed - it is not destroyed, it is just in a socket that no longer exists.
**The space set** (`packs/armory`) is Create: Cosmonautics' answer to vacuum,
rebuilt as modules. Cosmonautics suffocates you above the atmosphere by
@ -101,6 +291,37 @@ 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. One trophy from each corner of the game in the corners
of the grid - a nether star, a dragon's breath, a heart of the sea and Iron's
Spells' ancient knowledge fragment - with two cinder essence and two arcane
essence on the edges and nothing in the middle. All of it is 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 Minecraft's own heart of the sea with its hue rotated into
violet - the only art here that is not drawn for this mod, and Mojang's rather
than ours, which is worth knowing given what the khopesh section above says
about shipping other people's. Nothing but the hue is touched, so the shading
is the vanilla item's, including how dark its underside is: the palette the
material hands out is therefore a deep one with little highlight, which is what
the item actually looks like rather than what a gem usually does.
## Stats
Tinkers' Construct 3 is the reference, re-scaled onto MIAPI's vanilla-derived
@ -172,11 +393,26 @@ new piece is wanted rather than a repaint of an existing one.
## Building
```sh
./gradlew build
./build.sh
```
Needs nothing but NeoForge - Truly Modular is a runtime dependency only, since
this mod talks to it purely through datapack JSON.
The jar lands in `build/libs/`. Needs nothing but NeoForge - Truly Modular is a
runtime dependency only, since this mod talks to it purely through datapack
JSON.
`./gradlew build` does the same thing, and is fine if the java on your path is
one Gradle can run on. Gradle 8.12 cannot run on a JDK newer than 23, which a
distro's default java may well be by now, and the failure when it is says
`Could not create task ':test'` and `Type T not present` without ever mentioning
Java. `build.sh` exists to pick a JDK between 17 and 23 - honouring `JAVA_HOME`
if it already points at one - and to say what is wrong in as many words when
there is none to pick. Arguments are passed through, with any task named
replacing the default `build` rather than adding to it, so `./build.sh clean
build` cleans and then builds; a bare flag such as `./build.sh --info` still
gets the default task.
None of this touches what the mod is compiled against: the toolchain in
`build.gradle` is Java 21 either way, and Gradle provisions it if it is missing.
## Regenerating materials
@ -187,8 +423,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
@ -205,6 +443,58 @@ 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.
## Looking at the armour
```sh
python3 tools/ARMOUR_QUICKSTART.py # every armour model, one at a time
python3 tools/ARMOUR_GUI.py --jar <armory> # draw and unwrap socket geometry
python3 tools/ARMOUR_EDITOR.py --jar <armory> # pose a scene and nudge placements
```
The quickstart is the way in and needs no arguments: it reads every model in
`src/main/resources` - worn armour, icons, sword parts, the loose item models,
the files in the tree rather than a built jar - and lists them with the first
one showing. A jar passed with `--jar` adds its models to the list too, after
yours and marked with their namespace, which is the only way to see a socket
against the plate it is cut into: the geometry in this repo is the socket, and
`arm_left/heavy` is Armory's. Click a row to show or hide it and to point the camera at it;
`Reload from disk` picks up a model saved in another window.
The left panel is the selected model's MIAPI transform. Three arrows on the
model's origin move it along an axis and the rotate toggle swaps them for three
rings; the position and rotation boxes read what the drag did, in the units a
module writes, and take typed numbers back. `Copy as JSON` puts the `transform`
block on the clipboard, and `Write to source` puts it back in the module entry
it was read from - scale and origin untouched. It writes every model that has
moved rather than just the selected one, and marks a moved row with a `*` until
it has; a model no module names has nowhere to write to and is named in the
report instead. `Unwrap UVs` re-cuts the selected model's texture so every
face has a patch of its own, writes the atlas size beside the boxes, and paints
a template to match where there is no texture yet - never over one there is.
Where a model goes comes from the mod's own module data: MIAPI draws a model
under the body part its `origin` names, and those are declared in
`packs/*/data/*/miapi/modules/`. What is *not* in this tree is the offset
Armory's slot transforms carry, so without `--jar` a worn model is flipped onto
its part but not moved along it - right limb, roughly right place. Pass the
Armory jar for exact placement, and for its own plates to line new geometry up
against.
These need pyvista and pyvistaqt, which hardly any distribution packages, so
the tools keep a virtualenv at `tools/.venv` and re-run themselves inside it.
Build it once with
```sh
python3 -m venv --system-site-packages tools/.venv
tools/.venv/bin/pip install pyvista pyvistaqt
```
`--system-site-packages` is what keeps the distribution's Qt in charge; a
PySide6 from pip alongside the system Qt is a partial upgrade waiting to
happen. On a Wayland session the viewport runs through XWayland, because VTK's
Python wheels have no Wayland window backend - the tools say which display they
picked, and why, when they start.
## Notes
- Titanium's palette comes from Cosmonautics' ingot, which is violet. Air War's
@ -213,6 +503,51 @@ 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
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.
- What a fang is worth depends on where it sits, and all of it is filed under
`default` so a fang set into an armour socket counts as much as one shaped
into the piece. Worn, it grants 0.5 water breathing in each armour slot -
which is what Ice and Fire's own Tide Guardian armour does - and the handler
only reads 1 as breathing, so it takes two worn fang modules: two sockets,
two pieces built of it, or a socketed piece that is both. Held, it breathes
not at all and instead does what a serpent does in its element: `+0.6`
submerged mining speed on vanilla's base of `0.2`, so a single fang module
digs four times faster underwater than a plain tool and two of them beat
dry-land speed, and `+0.10` `cmmodular:water_combat`, so a weapon built of it
hits that much harder and swings that much faster while its bearer is in
water. Both stack per module; the combat half caps at `+0.5`. That half is
the bearer's rather than the weapon's, so a fang bow's arrows count as well,
as long as the fang is in hand and its bearer in water when they land.
- 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.
@ -239,25 +574,34 @@ folders, so the hand-written packs survive it.
- Cinder essence uses slot `any`, so one gem grants fire immunity wherever it
sits, worn or held. The attribute caps at 1 and the handler treats anything at
or above 1 as immune, so extra pieces neither add nor dilute.
- A material's name is a lang key, not a field on the material. MIAPI reads
`miapi.material.<namespace>.<group>.<name>` - its own id with the separators
turned into dots - so `cmmodular:metal/arcane_metal` is named by
`miapi.material.cmmodular.metal.arcane_metal` in `assets/cmmodular/lang/`,
which the generator writes. The value is the bare name: MIAPI puts in the
space before the module's own name. There is a `translation` field, but it is
only read alongside `fake_translation`, as a pair registering a name for a key
at runtime; on its own it names nothing, and the key shows raw in game.
- Material properties belong under the `default` key, not `armor` or `handheld`.
A gem socketed into armour is not tagged `armor`: gem modules declare which
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:
- Arcane metal 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
per module, so a weapon built entirely of arcane metal 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.
bottoms out. The material is called Arcane Metal because the item is an Arcane
Ingot and a material is named for the substance, not the shape it is traded
in; plain Arcane, which is what Iron's Spells calls the material itself, is
taken here by the essence. 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

76
build.sh Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Builds the mod jar into build/libs/.
#
# Gradle 8.12 cannot run on a JDK newer than 23, and a distro's default java is
# often newer than that, so this picks a JDK Gradle can run on rather than
# leaving you to read the "Type T not present" failure that comes of not doing
# so. The build itself still compiles against Java 21 either way - that is the
# toolchain in build.gradle, and Gradle provisions it if it is missing.
set -euo pipefail
cd "$(dirname "$0")"
# Major version of the java at $1/bin/java, or nothing if it will not run.
java_major() {
local out
out=$("$1/bin/java" -version 2>&1) || return 0
sed -n 's/^[^"]*"\([0-9]*\).*/\1/p' <<<"$out" | head -1
}
usable() {
local major
[ -x "$1/bin/java" ] || return 1
major=$(java_major "$1")
[ -n "$major" ] && [ "$major" -ge 17 ] && [ "$major" -le 23 ]
}
if [ -z "${JAVA_HOME:-}" ] || ! usable "$JAVA_HOME"; then
for candidate in /usr/lib/jvm/java-{21,17}-openjdk /usr/lib/jvm/java-{21,17}-openjdk-amd64; do
if usable "$candidate"; then
export JAVA_HOME="$candidate"
break
fi
done
fi
if [ -z "${JAVA_HOME:-}" ] || ! usable "$JAVA_HOME"; then
echo "build.sh: no JDK between 17 and 23 found - Gradle 8.12 cannot run on anything newer." >&2
echo "Install one (e.g. pacman -S jdk17-openjdk) or point JAVA_HOME at it." >&2
exit 1
fi
# Named tasks replace the default rather than being appended to it. Appending
# would turn `build.sh clean build` into `gradlew build clean build`, which
# Gradle folds back into build and clean, in that order - it cleans away the jar
# it just built. Bare flags still get the default task, so `build.sh --info`
# means an informative build rather than a no-op.
tasks=()
for arg in "$@"; do
[ "${arg#-}" = "$arg" ] && tasks+=("$arg")
done
[ ${#tasks[@]} -eq 0 ] && set -- build "$@"
echo "build.sh: using JDK $(java_major "$JAVA_HOME") at $JAVA_HOME"
./gradlew "$@"
# Name the jar this run produced. Old versions linger in build/libs/, so listing
# the whole directory would be ambiguous about which one is current. Only worth
# saying when a task that builds one actually ran: `build.sh clean` legitimately
# ends with no jar at all.
builds_jar=false
for arg in "$@"; do
case "$arg" in build | assemble | jar) builds_jar=true ;; esac
done
if [ "$builds_jar" = true ]; then
prop() { sed -n "s/^$1=//p" gradle.properties; }
jar="build/libs/$(prop mod_id)-$(prop mod_version).jar"
echo
if [ -f "$jar" ]; then
echo "Built: $jar"
else
echo "build.sh: expected $jar, but it is not there." >&2
exit 1
fi
fi

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.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,144 @@
package eu.abdelbaki.cmmodular;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Consumer;
import net.minecraft.world.item.ItemStack;
import net.neoforged.bus.api.Event;
import net.neoforged.neoforge.common.NeoForge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Makes each Apothic gem case worth one Apotheosis socket instead of the piece
* as a whole being worth one.
*
* <p>None of this is in the module JSON, and it cannot be. MIAPI's
* {@code components} property sets a component rather than adding to it, so two
* cases both writing 1 would leave the piece with one socket between them; the
* property that could resolve a count instead, {@code advanced_components}, is
* written but never registered in MIAPI 2.3.8, so that key is dead. And the one
* that does work is worse than useless here: {@code ComponentProperty.preview}
* dereferences the workbench without a null check, and MIAPI hands it null while
* hovering an option in the replace list, so merely carrying a {@code components}
* property crashes the crafting screen on a server.
*
* <p>Apotheosis asks the question itself. {@code SocketHelper.getSockets} reads
* the component and then fires {@code GetItemSocketsEvent} so anything can
* revise the answer, which is exactly what is wanted, so this counts the cases
* and revises it.
*
* <p>It does that without compiling against either mod. This mod deliberately
* has no dependencies to build on - Truly Modular and Apotheosis are runtime
* mods it talks to through data - and one socket is not worth giving that up.
* So both sides are reached reflectively and resolved once at startup. Failing
* costs nothing: the count reverts to what the component alone says, which is
* the behaviour without this class at all. Absence is silent, since a pack
* without those mods is the ordinary case, but a rename is logged, because
* that one is a thing to fix rather than a thing to expect.
*/
public final class ApothicSockets {
/** The one place this mod logs: a silent reflective failure is unfindable. */
private static final Logger LOGGER = LoggerFactory.getLogger(CMModular.MOD_ID);
private static final String EVENT = "dev.shadowsoffire.apotheosis.event.GetItemSocketsEvent";
private static final String MIAPI_MODULE = "smartin.miapi.modules.ItemModule";
/**
* Module ids that are a gem case: one per gem size, one per limb socket.
* A case is not listed twice for being allowed in a pommel as well as in
* armour - it is one module either way, and this counts modules, so a
* guard holding two small cases is worth two sockets by the same rule
* that makes a chestplate holding one worth one.
*/
private static final List<String> CASES = List.of(
"cmmodular:gem/apothic_case",
"cmmodular:gem/case_small",
"cmmodular:gem/case_great",
"cmmodular:gem/limb/case_arm_left",
"cmmodular:gem/limb/case_arm_right",
"cmmodular:gem/limb/case_leg_left",
"cmmodular:gem/limb/case_leg_right");
/** MIAPI's module tree, reached without naming its types at compile time. */
private static Method getModules;
private static Method getFlatList;
private static Method getModule;
private static Method moduleId;
/** Apotheosis' event, likewise. */
private static Method getStack;
private static Method getSockets;
private static Method setSockets;
private ApothicSockets() {
}
static void register() {
Class<?> itemModule;
Class<?> moduleInstance;
Class<?> event;
try {
itemModule = Class.forName(MIAPI_MODULE);
moduleInstance = Class.forName("smartin.miapi.modules.ModuleInstance");
event = Class.forName(EVENT);
} catch (ClassNotFoundException absent) {
// One of the two mods is not installed, which is the ordinary case
// for a pack without them and nothing to say anything about. The
// classes answer that better than ModList does, which is not
// reliably populated this early.
return;
}
try {
getModules = itemModule.getMethod("getModules", ItemStack.class);
getFlatList = moduleInstance.getMethod("getFlatList");
getModule = moduleInstance.getMethod("getModule");
moduleId = itemModule.getMethod("id");
getStack = event.getMethod("getStack");
getSockets = event.getMethod("getSockets");
setSockets = event.getMethod("setSockets", int.class);
@SuppressWarnings("unchecked")
Class<Event> type = (Class<Event>) event;
NeoForge.EVENT_BUS.addListener(type, (Consumer<Event>) ApothicSockets::onGetSockets);
} catch (ReflectiveOperationException | ClassCastException moved) {
// Both mods are here but something has been renamed, which is worth
// saying out loud: the cases still work, they just stop counting.
LOGGER.warn("Apothic gem cases will hold one socket a piece rather than one"
+ " each - Apotheosis or MIAPI has moved something: {}", moved.toString());
}
}
/**
* Answers with a socket a case, added to whatever the stack already had.
* Nothing else here writes that component now, so a socket cut by a Sigil of
* Socketing survives and the cases stack on top of it, which is a better
* trade than the component's own answer of overwriting it.
*/
private static void onGetSockets(Event event) {
try {
int cases = countCases((ItemStack) getStack.invoke(event));
if (cases > 0) {
setSockets.invoke(event, (int) getSockets.invoke(event) + cases);
}
} catch (ReflectiveOperationException | RuntimeException e) {
// Answering wrongly is worse than not answering: leave the count be.
}
}
private static int countCases(ItemStack stack) throws ReflectiveOperationException {
Object root = getModules.invoke(null, stack);
if (root == null) {
return 0;
}
int found = 0;
for (Object module : (List<?>) getFlatList.invoke(root)) {
Object id = moduleId.invoke(getModule.invoke(module));
if (id != null && CASES.contains(id.toString())) {
found++;
}
}
return found;
}
}

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

@ -33,10 +33,15 @@ import net.neoforged.neoforge.event.AddPackFindersEvent;
public class CMModular {
public static final String MOD_ID = "cmmodular";
/** A built-in datapack - materials or modules - and the mod it needs. */
private record MaterialPack(String id, String name, String requires) {
/** A built-in datapack - materials or modules - and the mods it needs. */
private record MaterialPack(String id, String name, String... requires) {
boolean present() {
return ModList.get().isLoaded(requires);
for (String mod : requires) {
if (!ModList.get().isLoaded(mod)) {
return false;
}
}
return true;
}
}
@ -68,11 +73,21 @@ public class CMModular {
// 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"));
new MaterialPack("armory", "Armory Modules", "tm_armory"),
// Two mods, because the case sits in one of Armory's gem slots and
// sets a component only Apotheosis knows what to do with. Without
// either half it is a module that fits nowhere and does nothing.
new MaterialPack("apotheosis", "Apothic Gem Cases", "apotheosis", "tm_armory"));
public CMModular(IEventBus modBus) {
FireImmunity.register(modBus);
VacuumSeal.register(modBus);
WaterBreathing.register(modBus);
WaterCombat.register(modBus);
SandPhasing.register(modBus);
EquipmentAttributes.register(modBus);
ArcanaCore.register(modBus);
ApothicSockets.register();
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

@ -0,0 +1,67 @@
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.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 armour lets its wearer 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>Half a lung per module, as with {@link VacuumSeal}: the fang grants 0.5 in
* each armour slot and nothing in hand, so it takes two worn pieces of it -
* two sockets, two pieces built of it, or a socketed piece that is both - to
* breathe, and a fang weapon breathes not at all. What a fang is worth in hand
* is {@link WaterCombat}'s.
*
* <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; each worn fang module is 0.5. */
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;
}
if (EquipmentAttributes.value(event.getEntity(), WATER_BREATHING) >= 1.0D) {
event.setCanBreathe(true);
}
}
}

View File

@ -0,0 +1,116 @@
package eu.abdelbaki.cmmodular;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
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.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import net.minecraft.world.entity.player.Player;
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.event.tick.PlayerTickEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
/**
* A sea serpent fang in hand is worth more in water than out of it: harder
* hits and a faster swing while the bearer is in it.
*
* <p>The mining half of the same idea needs nothing here - vanilla models
* underwater digging as an attribute of its own,
* {@code minecraft:player.submerged_mining_speed}, and the material simply
* adds to it. Damage and attack speed have no such switch, so this follows the
* pattern {@link FireImmunity} and {@link WaterBreathing} 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>Damage is answered on the blow, which needs no state. Attack speed cannot
* be - it is read off the attribute map whenever the game wants to know how
* fast the bearer swings - so a modifier goes on and comes off as the bearer
* enters and leaves the water. It is transient and keyed by one id, so a
* player who never touches a fang never has one and a player who does never
* collects two.
*/
public final class WaterCombat {
private static final DeferredRegister<Attribute> ATTRIBUTES =
DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID);
/** No more than half again, however much fang the weapon is built from. */
private static final double MAX = 0.5D;
/** The fraction added to damage dealt and to swing speed, in water only. */
public static final DeferredHolder<Attribute, Attribute> WATER_COMBAT =
ATTRIBUTES.register("water_combat", id -> new RangedAttribute(
"attribute.cmmodular.water_combat", 0.0D, 0.0D, MAX).setSyncable(true));
/** The one attack speed modifier this puts on, so it can be taken off. */
private static final ResourceLocation SWING_ID =
ResourceLocation.fromNamespaceAndPath(CMModular.MOD_ID, "water_combat");
private WaterCombat() {
}
static void register(IEventBus modBus) {
ATTRIBUTES.register(modBus);
modBus.addListener(WaterCombat::attachToEntities);
NeoForge.EVENT_BUS.addListener(WaterCombat::onIncomingDamage);
NeoForge.EVENT_BUS.addListener(WaterCombat::onPlayerTick);
}
/** 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_COMBAT));
}
private static void onIncomingDamage(LivingIncomingDamageEvent event) {
if (!(event.getSource().getEntity() instanceof LivingEntity attacker)
|| !attacker.isInWater()) {
return;
}
double bonus = boost(attacker);
if (bonus > 0.0D) {
event.setAmount((float) (event.getAmount() * (1.0D + bonus)));
}
}
/**
* Keeps the swing speed modifier in step with the water and the weapon.
*
* <p>Only players, because attack speed is only ever read for a player.
* The equipment is only consulted while the bearer is in water, so this
* costs nothing on dry land.
*/
private static void onPlayerTick(PlayerTickEvent.Post event) {
Player player = event.getEntity();
AttributeInstance speed = player.getAttribute(Attributes.ATTACK_SPEED);
if (speed == null) {
return;
}
double bonus = player.isInWater() ? boost(player) : 0.0D;
AttributeModifier applied = speed.getModifier(SWING_ID);
if (bonus <= 0.0D) {
if (applied != null) {
speed.removeModifier(SWING_ID);
}
return;
}
if (applied != null && applied.amount() == bonus) {
return;
}
// Weapon swapped for one with more fang in it, or the map was rebuilt
// out from under the modifier - either way, put the right one back.
speed.removeModifier(SWING_ID);
speed.addTransientModifier(new AttributeModifier(SWING_ID, bonus,
AttributeModifier.Operation.ADD_MULTIPLIED_BASE));
}
/** What the held modules are worth, clamped to what the attribute allows. */
private static double boost(LivingEntity entity) {
return Math.min(EquipmentAttributes.value(entity, WATER_COMBAT), MAX);
}
}

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

@ -1,13 +1,166 @@
{
"attribute.cmmodular.fire_immunity": "Fire Immunity",
"attribute.cmmodular.vacuum_seal": "Vacuum Seal",
"attribute.cmmodular.water_breathing": "Water Breathing",
"attribute.cmmodular.water_combat": "Underwater Fighting",
"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",
"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"
"miapi.module.cmmodular.armor.space.life_support_socket.name": "Life Support Pack with Socket",
"miapi.module.cmmodular.armor.socket.arm_left": "Left Heavy Socket Pauldron",
"miapi.module.cmmodular.armor.socket.arm_left.name": "%s Left Heavy Socket Pauldron",
"miapi.module.cmmodular.armor.socket.arm_left.description": "A Heavy Pauldron with a Gemstone set into it",
"miapi.module.cmmodular.armor.socket.arm_right": "Right Heavy Socket Pauldron",
"miapi.module.cmmodular.armor.socket.arm_right.name": "%s Right Heavy Socket Pauldron",
"miapi.module.cmmodular.armor.socket.arm_right.description": "A Heavy Pauldron with a Gemstone set into it",
"miapi.module.cmmodular.armor.socket.leg_left": "Heavy Socket Left Pants",
"miapi.module.cmmodular.armor.socket.leg_left.name": "%s Heavy Socket Left Pants",
"miapi.module.cmmodular.armor.socket.leg_left.description": "Heavy Pants with a Gemstone set into the Knee",
"miapi.module.cmmodular.armor.socket.leg_right": "Heavy Socket Right Pants",
"miapi.module.cmmodular.armor.socket.leg_right.name": "%s Heavy Socket Right Pants",
"miapi.module.cmmodular.armor.socket.leg_right.description": "Heavy Pants with a Gemstone set into the Knee",
"miapi.module.cmmodular.gem.apothic_case.name": "Apothic Gem Case",
"miapi.module.cmmodular.gem.case_small.name": "Small Apothic Gem Case",
"miapi.module.cmmodular.gem.case_great.name": "Great Apothic Gem Case",
"miapi.module.cmmodular.gem.limb.gem_arm_left.name": "%s Left Pauldron Gemstone",
"miapi.module.cmmodular.gem.limb.case_arm_left.name": "Left Pauldron Apothic Gem Case",
"miapi.module.cmmodular.gem.limb.gem_arm_right.name": "%s Right Pauldron Gemstone",
"miapi.module.cmmodular.gem.limb.case_arm_right.name": "Right Pauldron Apothic Gem Case",
"miapi.module.cmmodular.gem.limb.gem_leg_left.name": "%s Left Knee Gemstone",
"miapi.module.cmmodular.gem.limb.case_leg_left.name": "Left Knee Apothic Gem Case",
"miapi.module.cmmodular.gem.limb.gem_leg_right.name": "%s Right Knee Gemstone",
"miapi.module.cmmodular.gem.limb.case_leg_right.name": "Right Knee Apothic Gem Case",
"miapi.material.cmmodular.bone.dragonbone": "Dragonbone",
"miapi.material.cmmodular.bone.hippogryph_talon": "Talon",
"miapi.material.cmmodular.bone.sea_serpent_fang": "Sea Serpent Fang",
"miapi.material.cmmodular.bone.witherbone": "Witherbone",
"miapi.material.cmmodular.crystal.arcana_core": "Arcana Core",
"miapi.material.cmmodular.crystal.arcane_essence": "Arcane",
"miapi.material.cmmodular.crystal.avaricite_shard": "Avaricite",
"miapi.material.cmmodular.crystal.bismuth_crystal": "Bismuth",
"miapi.material.cmmodular.crystal.blazing_crystal": "Blazing",
"miapi.material.cmmodular.crystal.certus_quartz": "Certus Quartz",
"miapi.material.cmmodular.crystal.charged_certus_quartz": "Charged Certus",
"miapi.material.cmmodular.crystal.cinder_essence": "Cinder",
"miapi.material.cmmodular.crystal.citrine_shard": "Citrine",
"miapi.material.cmmodular.crystal.dimensional_shard": "Dimensional",
"miapi.material.cmmodular.crystal.divinite_shard": "Divinite",
"miapi.material.cmmodular.crystal.dread_shard": "Dread",
"miapi.material.cmmodular.crystal.ender_crystal": "Ender Crystal",
"miapi.material.cmmodular.crystal.enticing_crystal": "Enticing Crystal",
"miapi.material.cmmodular.crystal.floralite_shard": "Floralite",
"miapi.material.cmmodular.crystal.fluix": "Fluix",
"miapi.material.cmmodular.crystal.fluorite": "Fluorite",
"miapi.material.cmmodular.crystal.frostbite_crystal": "Frostbite",
"miapi.material.cmmodular.crystal.glow_shard": "Glowshard",
"miapi.material.cmmodular.crystal.hemalite_shard": "Hemalite",
"miapi.material.cmmodular.crystal.ice_shard": "Ice",
"miapi.material.cmmodular.crystal.moonstone_shard": "Moonstone",
"miapi.material.cmmodular.crystal.onyx_shard": "Onyx",
"miapi.material.cmmodular.crystal.paltaeria_gem": "Paltaeria",
"miapi.material.cmmodular.crystal.pixie_dust": "Pixie Dust",
"miapi.material.cmmodular.crystal.prescient_crystal": "Prescient Crystal",
"miapi.material.cmmodular.crystal.pulsating_crystal": "Pulsating Crystal",
"miapi.material.cmmodular.crystal.sapphire": "Sapphire",
"miapi.material.cmmodular.crystal.shimmerstone_gem": "Shimmerstone",
"miapi.material.cmmodular.crystal.silicon_carbide": "Silicon Carbide",
"miapi.material.cmmodular.crystal.source_gem": "Source",
"miapi.material.cmmodular.crystal.spirit_attuned_gem": "Spirit Attuned",
"miapi.material.cmmodular.crystal.stratine_gem": "Stratine",
"miapi.material.cmmodular.crystal.sulfur": "Sulfur",
"miapi.material.cmmodular.crystal.topaz_shard": "Topaz",
"miapi.material.cmmodular.crystal.vibrant_crystal": "Vibrant Crystal",
"miapi.material.cmmodular.crystal.voidstone_shard": "Voidstone",
"miapi.material.cmmodular.crystal.volcanite_shard": "Volcanite",
"miapi.material.cmmodular.crystal.voltite_shard": "Voltite",
"miapi.material.cmmodular.crystal.weather_crystal": "Weather Crystal",
"miapi.material.cmmodular.fabric.troll_leather_forest": "Forest Troll Leather",
"miapi.material.cmmodular.fabric.troll_leather_frost": "Frost Troll Leather",
"miapi.material.cmmodular.fabric.troll_leather_mountain": "Mountain Troll Leather",
"miapi.material.cmmodular.fletching.amphithere_feather": "Amphithere",
"miapi.material.cmmodular.fletching.stymphalian_feather": "Stymphalian",
"miapi.material.cmmodular.metal.aluminum": "Aluminium",
"miapi.material.cmmodular.metal.amalgam": "Amalgam",
"miapi.material.cmmodular.metal.andesite_alloy": "Andesite Alloy",
"miapi.material.cmmodular.metal.antimatter": "Antimatter",
"miapi.material.cmmodular.metal.aquarine_steel": "Aquarine Steel",
"miapi.material.cmmodular.metal.arcane_metal": "Arcane Metal",
"miapi.material.cmmodular.metal.atlantic_gold": "Atlantic Gold",
"miapi.material.cmmodular.metal.atomic_alloy": "Atomic Alloy",
"miapi.material.cmmodular.metal.aviation_alloy": "Aviation Alloy",
"miapi.material.cmmodular.metal.brass": "Brass",
"miapi.material.cmmodular.metal.bronze": "Bronze",
"miapi.material.cmmodular.metal.cast_iron": "Cast Iron",
"miapi.material.cmmodular.metal.cemented_carbide": "Cemented Carbide",
"miapi.material.cmmodular.metal.computronic_substrate": "Computronic",
"miapi.material.cmmodular.metal.conductive_alloy": "Conductive Alloy",
"miapi.material.cmmodular.metal.constantan": "Constantan",
"miapi.material.cmmodular.metal.dark_steel": "Dark Steel",
"miapi.material.cmmodular.metal.dragonsteel_fire": "Fire Dragonsteel",
"miapi.material.cmmodular.metal.dragonsteel_ice": "Ice Dragonsteel",
"miapi.material.cmmodular.metal.dragonsteel_lightning": "Lightning Dragonsteel",
"miapi.material.cmmodular.metal.electrum": "Electrum",
"miapi.material.cmmodular.metal.end_steel": "End Steel",
"miapi.material.cmmodular.metal.energetic_alloy": "Energetic Alloy",
"miapi.material.cmmodular.metal.ferdonzor": "Ferdonzor",
"miapi.material.cmmodular.metal.ghost_ingot": "Phantasmal",
"miapi.material.cmmodular.metal.hdpe": "HDPE",
"miapi.material.cmmodular.metal.hop_graphite": "Graphite",
"miapi.material.cmmodular.metal.iesnium": "Iesnium",
"miapi.material.cmmodular.metal.industrial_iron": "Industrial Iron",
"miapi.material.cmmodular.metal.infused_alloy": "Infused Alloy",
"miapi.material.cmmodular.metal.lead": "Lead",
"miapi.material.cmmodular.metal.microlattice": "Microlattice",
"miapi.material.cmmodular.metal.mithril": "Mithril",
"miapi.material.cmmodular.metal.nickel": "Nickel",
"miapi.material.cmmodular.metal.osmium": "Osmium",
"miapi.material.cmmodular.metal.pink_slime": "Pink Slime",
"miapi.material.cmmodular.metal.platinum": "Platinum",
"miapi.material.cmmodular.metal.plutonium": "Plutonium",
"miapi.material.cmmodular.metal.polonium": "Polonium",
"miapi.material.cmmodular.metal.pulsating_alloy": "Pulsating Alloy",
"miapi.material.cmmodular.metal.pyrium": "Pyrium",
"miapi.material.cmmodular.metal.redstone_alloy": "Redstone Alloy",
"miapi.material.cmmodular.metal.refined_glowstone": "Refined Glowstone",
"miapi.material.cmmodular.metal.refined_obsidian": "Refined Obsidian",
"miapi.material.cmmodular.metal.refined_radiance": "Refined Radiance",
"miapi.material.cmmodular.metal.reinforced_alloy": "Reinforced Alloy",
"miapi.material.cmmodular.metal.shadow_steel": "Shadow Steel",
"miapi.material.cmmodular.metal.silver": "Silver",
"miapi.material.cmmodular.metal.soularium": "Soularium",
"miapi.material.cmmodular.metal.steel": "Steel",
"miapi.material.cmmodular.metal.tin": "Tin",
"miapi.material.cmmodular.metal.titanium": "Titanium",
"miapi.material.cmmodular.metal.titanium_alloy": "Titanium Alloy",
"miapi.material.cmmodular.metal.tungsten": "Tungsten",
"miapi.material.cmmodular.metal.uranium": "Uranium",
"miapi.material.cmmodular.metal.vibrant_alloy": "Vibrant Alloy",
"miapi.material.cmmodular.metal.zinc": "Zinc",
"miapi.material.cmmodular.scute.deathworm_chitin_red": "Red Chitin",
"miapi.material.cmmodular.scute.deathworm_chitin_white": "White Chitin",
"miapi.material.cmmodular.scute.deathworm_chitin_yellow": "Tan Chitin",
"miapi.material.cmmodular.scute.dragonscales_amethyst": "Amethyst Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_black": "Black Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_blue": "Blue Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_bronze": "Bronze Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_copper": "Copper Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_electric": "Electric Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_gray": "Gray Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_green": "Green Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_red": "Red Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_sapphire": "Sapphire Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_silver": "Silver Dragonscale",
"miapi.material.cmmodular.scute.dragonscales_white": "White Dragonscale",
"miapi.material.cmmodular.scute.sea_serpent_scales_blue": "Blue Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_bronze": "Bronze Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_deepblue": "Deep Blue Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_green": "Green Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_purple": "Purple Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_red": "Red Serpent Scale",
"miapi.material.cmmodular.scute.sea_serpent_scales_teal": "Teal Serpent Scale"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,91 @@
{
"comment": "Gem socket drawn under left_arm, so it moves with the limb. MIAPI draws a model in the pass its own origin names, and Armory's gemstone names none, so the socket has to be ours for anything here to follow the limb at all.",
"texture_size": [
16,
8
],
"textures": {
"0": "cmmodular:equipment/arm_left_socket",
"particle": "cmmodular:equipment/arm_left_socket"
},
"elements": [
{
"name": "pauldron socket",
"from": [
-2.5,
-0.25,
-1.5
],
"to": [
-2.0,
1.75,
1.5
],
"rotation": {
"angle": -22.5,
"axis": "z",
"origin": [
0,
0,
0
]
},
"faces": {
"up": {
"texture": "#0",
"uv": [
3.0,
0.0,
4.0,
3.0
]
},
"down": {
"texture": "#0",
"uv": [
4.0,
0.0,
5.0,
3.0
]
},
"west": {
"texture": "#0",
"uv": [
0.0,
3.0,
3.0,
5.0
]
},
"north": {
"texture": "#0",
"uv": [
3.0,
3.0,
4.0,
5.0
]
},
"east": {
"texture": "#0",
"uv": [
4.0,
3.0,
7.0,
5.0
]
},
"south": {
"texture": "#0",
"uv": [
7.0,
3.0,
8.0,
5.0
]
}
}
}
]
}

View File

@ -0,0 +1,91 @@
{
"comment": "Gem socket drawn under right_arm, so it moves with the limb. MIAPI draws a model in the pass its own origin names, and Armory's gemstone names none, so the socket has to be ours for anything here to follow the limb at all.",
"texture_size": [
16,
8
],
"textures": {
"0": "cmmodular:equipment/arm_right_socket",
"particle": "cmmodular:equipment/arm_right_socket"
},
"elements": [
{
"name": "pauldron socket",
"from": [
2.0,
-0.25,
-1.5
],
"to": [
2.5,
1.75,
1.5
],
"rotation": {
"angle": 22.5,
"axis": "z",
"origin": [
0,
0,
0
]
},
"faces": {
"up": {
"texture": "#0",
"uv": [
3.0,
0.0,
4.0,
3.0
]
},
"down": {
"texture": "#0",
"uv": [
4.0,
0.0,
5.0,
3.0
]
},
"west": {
"texture": "#0",
"uv": [
0.0,
3.0,
3.0,
5.0
]
},
"north": {
"texture": "#0",
"uv": [
3.0,
3.0,
4.0,
5.0
]
},
"east": {
"texture": "#0",
"uv": [
4.0,
3.0,
7.0,
5.0
]
},
"south": {
"texture": "#0",
"uv": [
7.0,
3.0,
8.0,
5.0
]
}
}
}
]
}

View File

@ -0,0 +1,82 @@
{
"comment": "Gem socket drawn under left_leg, so it moves with the limb. MIAPI draws a model in the pass its own origin names, and Armory's gemstone names none, so the socket has to be ours for anything here to follow the limb at all.",
"texture_size": [
8,
4
],
"textures": {
"0": "cmmodular:equipment/leg_left_socket",
"particle": "cmmodular:equipment/leg_left_socket"
},
"elements": [
{
"name": "knee socket",
"from": [
-1.5,
-2.95,
-2.7
],
"to": [
1.5,
-0.95,
-2.2
],
"faces": {
"up": {
"texture": "#0",
"uv": [
1.0,
0.0,
4.0,
1.0
]
},
"down": {
"texture": "#0",
"uv": [
4.0,
0.0,
7.0,
1.0
]
},
"west": {
"texture": "#0",
"uv": [
0.0,
1.0,
1.0,
3.0
]
},
"north": {
"texture": "#0",
"uv": [
1.0,
1.0,
4.0,
3.0
]
},
"east": {
"texture": "#0",
"uv": [
4.0,
1.0,
5.0,
3.0
]
},
"south": {
"texture": "#0",
"uv": [
5.0,
1.0,
8.0,
3.0
]
}
}
}
]
}

View File

@ -0,0 +1,82 @@
{
"comment": "Gem socket drawn under right_leg, so it moves with the limb. MIAPI draws a model in the pass its own origin names, and Armory's gemstone names none, so the socket has to be ours for anything here to follow the limb at all.",
"texture_size": [
8,
4
],
"textures": {
"0": "cmmodular:equipment/leg_right_socket",
"particle": "cmmodular:equipment/leg_right_socket"
},
"elements": [
{
"name": "knee socket",
"from": [
-1.5,
-2.95,
-2.7
],
"to": [
1.5,
-0.95,
-2.2
],
"faces": {
"up": {
"texture": "#0",
"uv": [
1.0,
0.0,
4.0,
1.0
]
},
"down": {
"texture": "#0",
"uv": [
4.0,
0.0,
7.0,
1.0
]
},
"west": {
"texture": "#0",
"uv": [
0.0,
1.0,
1.0,
3.0
]
},
"north": {
"texture": "#0",
"uv": [
1.0,
1.0,
4.0,
3.0
]
},
"east": {
"texture": "#0",
"uv": [
4.0,
1.0,
5.0,
3.0
]
},
"south": {
"texture": "#0",
"uv": [
5.0,
1.0,
8.0,
3.0
]
}
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 163 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

@ -1,5 +1,4 @@
{
"translation": "Certus Quartz ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Charged Certus ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Fluix ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Aviation Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Sulfur ",
"groups": [
"crystal",
"gem_melee",

View File

@ -1,5 +1,4 @@
{
"translation": "Amalgam ",
"groups": [
"metal"
],

View File

@ -0,0 +1,41 @@
{
"display_name": "miapi.module.cmmodular.gem.apothic_case.name",
"model": {
"path": "miapi:models/item/armor/gems/medium/[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}
}
},
"allowed_in_slots": [
"gem_armor_medium",
"gem_melee_medium"
],
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic",
"gem_melee",
"gem_melee_medium",
"gem_melee_generic",
"gem_melee_medium_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,37 @@
{
"comment": "The great socket takes a large gem and nothing else, so the case that goes in it has to be a large one too - `gem_melee_large` is the key the great pommel asks for, and a medium case does not answer to it.",
"display_name": "miapi.module.cmmodular.gem.case_great.name",
"model": {
"path": "miapi:models/item/armor/gems/large/[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}
}
},
"allowed_in_slots": [
"gem_melee_large"
],
"tag": [
"gem_melee",
"gem_melee_large",
"gem_melee_generic",
"gem_melee_large_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,42 @@
{
"comment": "The small case, for the slots a small gemstone goes in - Arsenal's own socket pommel and the dual socket guard, which is two of them. Both size keys are named because a small slot is a small slot whether it was cut into a weapon or into armour, and a key nothing asks for costs nothing.",
"display_name": "miapi.module.cmmodular.gem.case_small.name",
"model": {
"path": "miapi:models/item/armor/gems/small/[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}
}
},
"allowed_in_slots": [
"gem_melee_small",
"gem_armor_small"
],
"tag": [
"gem_melee",
"gem_melee_small",
"gem_melee_generic",
"gem_melee_small_generic",
"gem_armor",
"gem_armor_small",
"gem_armor_generic",
"gem_armor_small_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,47 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.case_arm_left.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "left_arm"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 4.5, "y": 3.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_arm_left"
],
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,47 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.case_arm_right.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "right_arm"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -4.5, "y": 3.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_arm_right"
],
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,47 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.case_leg_left.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "left_leg"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 2.5, "y": -4.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_leg_left"
],
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,47 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.case_leg_right.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "right_leg"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -2.5, "y": -4.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_leg_right"
],
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"material_property": [
"default"
],
"priority": 0,
"repair_priority": 0,
"rarity": "rare",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass",
"bone",
"stone"
],
"cost": 2
}
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Apothic Gem Cases for Truly Modular",
"pack_format": 48
}
}

View File

@ -0,0 +1,61 @@
{
"parent": "tm_armory:armor/heavy/arm_left",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "chest"
}
],
"model": [
{
"path": "cmmodular:models/item/armor/model/arm_left/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": "left_arm"
}
}
]
},
"replace": {
"slots": {
"banner-arm-left": {
"allowed": [
"cosmetic_2x4",
"banner_2x4"
],
"slotType": "cosmetic",
"translationKey": "miapi.slot.arm_patch.name",
"transform": {
"rotation": {"x": 0, "y": 90, "z": 0},
"translation": {"x": -2.15, "y": -1, "z": 0},
"scale": {"x": 1.1, "y": 1, "z": 1},
"origin": "left_arm"
}
},
"gem": {
"allowed": [
"gem_armor_medium_arm_left"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 90.0, "y": 55.7523, "z": -90.0},
"translation": {"x": -1.7667, "y": 1.5983, "z": 0.0},
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
"origin": "left_arm"
}
}
},
"display_name": "miapi.module.cmmodular.armor.socket.arm_left.name"
}
}
}

View File

@ -0,0 +1,61 @@
{
"parent": "tm_armory:armor/heavy/arm_right",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "chest"
}
],
"model": [
{
"path": "cmmodular:models/item/armor/model/arm_right/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": "right_arm"
}
}
]
},
"replace": {
"slots": {
"banner-arm-right": {
"allowed": [
"cosmetic_2x4",
"banner_2x4"
],
"slotType": "cosmetic",
"translationKey": "miapi.slot.arm_patch.name",
"transform": {
"rotation": {"x": 0, "y": -90, "z": 0},
"translation": {"x": 2.15, "y": -1, "z": 0},
"scale": {"x": 1.1, "y": 1, "z": 1},
"origin": "right_arm"
}
},
"gem": {
"allowed": [
"gem_armor_medium_arm_right"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 90.0, "y": -55.7523, "z": 90.0},
"translation": {"x": 1.7667, "y": 1.5983, "z": 0.0},
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
"origin": "right_arm"
}
}
},
"display_name": "miapi.module.cmmodular.armor.socket.arm_right.name"
}
}
}

View File

@ -0,0 +1,47 @@
{
"parent": "tm_armory:armor/heavy/leg_left",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "legs"
}
],
"model": [
{
"path": "cmmodular:models/item/armor/model/leg_left/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": "left_leg"
}
}
]
},
"replace": {
"slots": {
"gem": {
"allowed": [
"gem_armor_medium_leg_left"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
"origin": "left_leg"
}
}
},
"display_name": "miapi.module.cmmodular.armor.socket.leg_left.name"
}
}
}

View File

@ -0,0 +1,47 @@
{
"parent": "tm_armory:armor/heavy/leg_right",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "legs"
}
],
"model": [
{
"path": "cmmodular:models/item/armor/model/leg_right/socket/[material.texture].json",
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": -2, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "right_leg"
}
}
]
},
"replace": {
"slots": {
"gem": {
"allowed": [
"gem_armor_medium_leg_right"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
"origin": "right_leg"
}
}
},
"display_name": "miapi.module.cmmodular.armor.socket.leg_right.name"
}
}
}

View File

@ -21,7 +21,7 @@
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 4.1387, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "head"
}

View File

@ -21,7 +21,7 @@
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": -2.1901, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "body"
}

View File

@ -0,0 +1,48 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.gem_arm_left.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "left_arm"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 4.5, "y": 3.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_arm_left"
],
"material_property": [
"default",
"gem_armor_handheld",
"gem_armor_generic"
],
"module_stats": {
"gem_power": 1
},
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"priority": 0,
"repair_priority": 0,
"rarity": "uncommon",
"allowed_material": {
"allowedMaterials": [
"gem_armor"
],
"cost": 2
}
}

View File

@ -0,0 +1,48 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.gem_arm_right.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "right_arm"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -4.5, "y": 3.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_arm_right"
],
"material_property": [
"default",
"gem_armor_handheld",
"gem_armor_generic"
],
"module_stats": {
"gem_power": 1
},
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"priority": 0,
"repair_priority": 0,
"rarity": "uncommon",
"allowed_material": {
"allowedMaterials": [
"gem_armor"
],
"cost": 2
}
}

View File

@ -0,0 +1,48 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.gem_leg_left.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "left_leg"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 2.5, "y": -4.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_leg_left"
],
"material_property": [
"default",
"gem_armor_handheld",
"gem_armor_generic"
],
"module_stats": {
"gem_power": 1
},
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"priority": 0,
"repair_priority": 0,
"rarity": "uncommon",
"allowed_material": {
"allowedMaterials": [
"gem_armor"
],
"cost": 2
}
}

View File

@ -0,0 +1,48 @@
{
"display_name": "miapi.module.cmmodular.gem.limb.gem_leg_right.name",
"model": [
{
"path": "miapi:models/item/armor/gems/medium/[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": "right_leg"
}
},
{
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -2.5, "y": -4.5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"allowed_in_slots": [
"gem_armor_medium_leg_right"
],
"material_property": [
"default",
"gem_armor_handheld",
"gem_armor_generic"
],
"module_stats": {
"gem_power": 1
},
"tag": [
"gem_armor",
"gem_armor_medium",
"gem_armor_generic",
"gem_armor_medium_generic"
],
"priority": 0,
"repair_priority": 0,
"rarity": "uncommon",
"allowed_material": {
"allowedMaterials": [
"gem_armor"
],
"cost": 2
}
}

View File

@ -1,5 +1,4 @@
{
"translation": "Source ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Titanium ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Titanium Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Andesite Alloy ",
"groups": [
"metal",
"stone"

View File

@ -1,5 +1,4 @@
{
"translation": "Brass ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Refined Radiance ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Shadow Steel ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Zinc ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Industrial Iron ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Platinum ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Avaricite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Divinite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Floralite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Hemalite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Ice ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Voidstone ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Volcanite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Voltite ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Ender Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Enticing Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Prescient Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Pulsating Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Vibrant Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Weather Crystal ",
"groups": [
"crystal",
"gem",

View File

@ -1,5 +1,4 @@
{
"translation": "Conductive Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Dark Steel ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "End Steel ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Energetic Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Pulsating Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Redstone Alloy ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Soularium ",
"groups": [
"metal"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Vibrant Alloy ",
"groups": [
"metal"
],

View File

@ -1,8 +1,10 @@
{
"translation": "Dragonbone ",
"groups": [
"bone"
],
"hidden_groups": [
"gem_armor"
],
"icon": {
"type": "item",
"item": "iceandfire:dragonbone"
@ -37,6 +39,11 @@
"properties": {
"head": {
"fracturing": "10"
},
"handheld": {
"fake_enchants": {
"minecraft:fire_aspect": 1
}
}
},
"items": [

View File

@ -1,5 +1,4 @@
{
"translation": "Talon ",
"groups": [
"bone"
],

View File

@ -0,0 +1,93 @@
{
"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": "0.5",
"operation": "+",
"slot": "head"
},
{
"attribute": "cmmodular:water_breathing",
"value": "0.5",
"operation": "+",
"slot": "chest"
},
{
"attribute": "cmmodular:water_breathing",
"value": "0.5",
"operation": "+",
"slot": "legs"
},
{
"attribute": "cmmodular:water_breathing",
"value": "0.5",
"operation": "+",
"slot": "feet"
},
{
"attribute": "minecraft:player.submerged_mining_speed",
"value": "0.6",
"operation": "+",
"slot": "mainhand"
},
{
"attribute": "cmmodular:water_combat",
"value": "0.1",
"operation": "+",
"slot": "mainhand"
}
]
},
"boot": {
"fake_enchants": {
"minecraft:depth_strider": 1
}
}
},
"items": [
{
"item": "iceandfire:sea_serpent_fang",
"value": 1.0
}
]
}

View File

@ -1,5 +1,4 @@
{
"translation": "Witherbone ",
"groups": [
"bone"
],

View File

@ -1,5 +1,4 @@
{
"translation": "Dread ",
"groups": [
"crystal",
"metal",

View File

@ -1,5 +1,4 @@
{
"translation": "Pixie Dust ",
"groups": [
"crystal",
"gem_melee",

Some files were not shown because too many files have changed in this diff Show More