-
-
Notifications
You must be signed in to change notification settings - Fork 1
Custom Content with KubeJS (1.20.1)
This guide describes how to use KubeJS to add various mod content in the 1.20.1 branch. There are certain differences between the 1.21.1 and 1.20.1 branches of the mod that are unavoidable due to internal structure changes. To see a version of this guide that is tailored for 1.21.1, see Custom Content with KubeJS.
KubeJS scripts made on a specific Integration Version should be compatible with those on the same major version. Changes to the minor component are backwards-compatible changes. Changes to this number are marked in the changelog for a given version if they happen (which should be rare!)
| Reactive Version Introduced | KubeJS Integration Version |
|---|---|
| 9a | 1.0 |
| 9d | 2.0 |
| 9k | 2.1 |
| 9.13 | 2.2 |
| 9.16 | 2.3 |
| 9.17 | 3.0 |
| 9.18 | 3.1 |
| 10.0 | 3.2 |
Custom Powers are added to reactive:power_registry at startup using StartupEvents.registry, like so:
StartupEvents.registry('reactive:power_registry', event => {
event.create('custom_power')
.color(0xFF00FF)
.setMagicWater()
.setName(Component.string('Custom'))
})The resulting power will be located at kubejs:custom_power. In this scenario, the power is bright purple in color and uses the "Magic" water render.
There are a few other possible method calls:
-
.bottle(Item)chooses an item to be this power's "Bottle". This allows the item to be made by clicking the Crucible with a Quartz Bottle if there's enough of this Power, and causes the item to release this Power and revert to a Quartz Bottle if put inside. -
.setNormalWater()causes the power to use the normal water texture in the Crucible. -
.setNoiseWater()causes the power to use a noisy water texture in the Crucible. -
.setFastWater()causes the power to use a more quickly moving water texture in the Crucible. -
.setSlowWater()causes the power to use a less quickly moving water texture in the Crucible. -
.setCustomWater(Block)causes the power to use any given block as its water texture. If the block is not animated, this will look bad! -
.setInvisible()causes the power to not change the appearance of water in the Crucible at all. The color is still used for Litmus Paper, but the water texture will not appear anywhere. (added in 2.1) -
.setName(Component)chooses a custom name for the Power. (added in 2.2) -
.setIcon(Item)sets the item that will represent this Power in JEI. (disabled in 2.3)
Once a Power is made, it may be used as a valid entry for recipes (which can of course be made using KubeJS or a data pack). You can also add a language entry for it similarly to KubeJS custom items or blocks.
Sources for the Power will automatically be searched for in the item tag reactive:(power_name)_sources; reactive:custom_power_sources for the above. Remember to put especially strong items into reactive:high_potency and consider adding Dissolve recipes if they should leave some byproduct.
The .bottle() method for the Power builder accepts any item, but only items that are registered as Power Bottles can be inserted into a Crucible with shift-right-click.
To create your own Power Bottle item, use the reactive:power_bottle item type (added in 2.2):
StartupEvents.registry('item', event => {
event.create('custom_bottle', 'reactive:power_bottle')
})You can feel free to add any item properties freely (as long as you don't override the use method), and the item should work as a bottle.
Some of the effects in the mod are represented as 'Special Cases' that occur when you do certain actions involving the Crucible. You can implement two kinds of these in KubeJS by handling different events in your server script file.
When you empty the Crucible, sometimes an effect occurs depending on the Powers inside the Crucible. This also fires the ReactiveEvents.emptyCrucible event, so you can add new effects!
ReactiveEvents.emptyCrucible(event => {
if(event.hasPower("kubejs:custom_power")){
if(event.getPowerLevel("kubejs:custom_power") > WorldSpecificValue.get("test_empty_threshold", 400, 600)){
console.log("Do something dramatic!")
}else{
console.log("Do something subtle!")
}
}
})The above handler checks to see if the Custom Power was present when the Crucible was emptied, and if so, tests the amount of that power against a world-specific threshold.
Here's a summary of the methods available through the event handler:
-
hasPower(ResourceLocation)checks if the given Power is in the Crucible at all (there is more than 0 of it) -
getPowerLevel(ResourceLocation)provides an integer from 0 to 1600, which is how much of the given Power is inside the Crucible -
getLevel()returns the Level that the reaction is being performed in -
getBlockPos()returns the BlockPos of the crucible performing the reaction -
getCrucible()returns the CrucibleBlockEntity itself. If you want to use this, you should check the source code
As an aside, the WorldSpecificValue class generates a random value between two numbers that is the same every time it is called in the same world, and different in other worlds. Here, it randomizes the threshold beyond which the effect changes.
When an item is dissolved in the Crucible, it fires a ReactiveEvents.dissolveItem event in KubeJS. You can handle it like this:
ReactiveEvents.dissolveItem(event => {
if(event.getItem().is("kubejs:simple_item")){
console.log("We're dissolving a custom item. Do something!")
}
})The event object all the same methods as the previous one, and these two as well:
-
.getItem()returns the item being dissolved. Since this event fires after ever dissolution, you need to test that this is the item you want. -
.getItemEntity()returns the item entity that is being dissolved. If you want to prevent it from being processed further, you could kill this entity.
To add a reaction, you will need to set up a few different event handlers and an advancement file.
Prior to KIV 3.0
First, you'll need to, in startup, set up a criteria trigger for the reaction. All reactions cause advancement criteria to complete when running, so this is mandatory.
StartupEvents.init(event => {
ReactionMan.CRITERIA_BUILDER.add("example_reaction")
})This automatically creates advancement criteria reactive:reaction/(alias)_criterion and reactive:reaction/(alias)_perfect_criterion.
Reactions are made in the server scripts file, and use the ReactiveEvents event group. Take the following example:
ReactiveEvents.constructReactions(event => {
event.builder("example_reaction", Component.literal("Example Reaction"),"reactive:light", "kubejs:custom_power").needsGoldSymbol().setCost(2).build()
})This defines a new reaction called example_reaction. The in-game name of the reaction will be "Example Reaction". This reaction requires Light and our Custom Power from before to occur, and additionally requires the presence of a Gold Symbol as its stimulus. Every server-side reaction tick, it consumes 2 units of power.
This reaction is relatively simple, but the builder has a few more methods you can use:
-
.needsGoldSymbol()makes the reaction require a Gold Symbol as its stimulus -
.needsElectric()makes the reaction require Electric Charge (for example from a Volt Cell) as its stimulus -
.needsNoElectric()makes the reaction require a lack of Electric Charge as its stimulus -
.needsEndCrystal()makes the reaction require a nearby End Crystal as its stimulus -
.needsNoEndCrystal()makes the reaction require there not to be a nearby End Crystal as its stimulus -
.setCost(int)adds a per-reaction-tick cost to the reaction. One tick occurs every half second or so (config dependant). -
.setYield(ResourceLocation, int)adds a yield to the reaction; each tick, the specified amount of the specified Power is added. Use this to make Synthesis or Conversion reactions.
To make the reaction do anything, you'll need to handle another event. ReactiveEvents.runReaction is a KubeJS event that will fire every time that a custom reaction performs its server tick, which should happen a few times a second. Since the same event is fired for all custom reactions, you'll need to check for the alias before you implement it:
ReactiveEvents.runReaction(event => {
if(event.getAlias() == "example_reaction"){
console.log("Do something!")
}
})The event includes these methods:
-
getAlias()returns the reaction's unique alias as a string -
getLevel()returns the Level that the reaction is being performed in. -
getBlockPos()returns the BlockPos of the crucible performing the reaction. -
getCrucible()returns the CrucibleBlockEntity itself. If you want to use this, you should check the source code -
expendPower(int)spends the given amount of energy from the Crucible. This allows you to make reactions that do not run forever -
hasPower(ResourceLocation)checks if the given Power is in the Crucible at all (there is more than 0 of it) -
getPowerLevel(ResourceLocation)provides an integer from 0 to 1600, which is how much of the given Power is inside the Crucible
Reactions can also run on the client side -- in fact, they do this every frame. Custom reactions use this time to send out another KubeJS event, which you can handle to add visual effects to your reaction. These handlers must be in a client-side script.
ReactiveEvents.renderReaction(event => {
if(event.getAlias() == "example_reaction"){
ParticleScribe.drawParticleRing(event.getLevel(), "minecraft:electric_spark", event.getBlockPos(), 2, 3, 10)
}
})This event has all the same fields as the runReaction event does. Also, as you can see, you have access to my ParticleScribe class if you want to use it. Check the source code in reactive.client.particle.
You can also add a custom check to determine if a reaction should occur. As of 3.1, this check occurs exclusively on the server side; before 3.1, it was necessary to duplicate all checks to the client side.
Here's an example of a custom check:
ReactiveEvents.checkReaction(event => {
if(event.getAlias() == "test_reaction"){
if(event.getLevel().isRaining()){
event.cancel()
}
}
})This check uses the getLevel() method to determine if it is raining. If so, the event is cancelled, which prevents the reaction from running or rendering. Note that this check occurs after the system checks for the reaction's power balance and stimulus, so if you do not cancel the event, the reaction is guaranteed to occur.
This event has all the same fields as the runReaction event does.
As it stands, your reaction will appear as "Unknown Reaction" when measured by Litmus Paper. That is because Litmus Paper checks if the player has achieved a specific advancement for each reaction before presenting its name. Since yours doesn't yet have an associated advancement, it will never appear. Let's fix that!
The advancement should be located within data/reactive/advancement/reactions, and its name must match the reaction alias. For example, the prior reaction will look for the advancement reactive:reactions/example_reaction.
The advancement can be in any valid format, but something like this is sufficient:
KIV 3.0+
{
"criteria": {
"criterion": {
"conditions": {
"reaction_alias": "example_reaction"
},
"trigger": "reactive:reaction"
}
},
"requirements": [
[
"criterion"
]
]
}Here, the alias is again example_reaction. Make sure that this matches the alias used when you registered the reaction!
KIV 2.x
{
"criteria": {
"criterion": {
"trigger": "reactive:reaction/example_reaction_criterion"
}
},
"requirements": [
[
"criterion"
]
]
}Here, the alias is again example_reaction. Make sure that the proper auto-generated reaction criterion is being used, or the advancement will not unlock when you observe the reaction.
If you want to add a Journal of Alchemy entry for your reaction, we'll need another advancement, this one for "perfectly" performing the reaction -- that is, performing the reaction without any extra Powers. This prevents players from learning the formula of reactions they didn't really discover. It follows a format like this:
KIV 3.0+
{
"criteria": {
"criterion": {
"conditions": {
"reaction_alias": "example_reaction"
},
"trigger": "reactive:perfect_reaction"
}
},
"requirements": [
[
"criterion"
]
]
}KIV 2.x
```json { "criteria": { "criterion": { "trigger": "reactive:reaction/example_reaction_perfect_criterion" } }, "requirements": [ [ "criterion" ] ], "sends_telemetry_event": true } ```You can add entries to the Journal of Alchemy by placing page JSON files in the proper resource pack path, assets/reactive/patchouli_books/journal/en_us/entries. All built-in reactions use an entry similar to this one:
{
"name": "Luminous Ring",
"icon": "minecraft:paper",
"category": "reactive:reactions",
"advancement": "reactive:reactions/sunlight",
"pages": [
{
"type": "patchouli:text",
"text": "$(bold)Visual:$(br)$()A ring of light appears with a 12 block radius around the Crucible.$(p)$(bold)Effect:$(br)$()Undead within the ring catch on fire as if burning in daylight."
},
{
"type": "reactive:reaction",
"reaction": "sunlight"
}
]
}The important bits here are the advancement field, which prevents this entry from unlocking if the player hasn't ever seen the reaction, and the reactive:reaction page template, which shows the formula if the player has the "perfect" advancement for the reaction.
Version 10+ only
Built-in Materials (like the Example Material entries I use as placeholders in JEI) are Materials that exist in every world. KubeJS integration allows you to add your own Materials, for use in builds or even as outputs from the Material Crafting system.
Materials are defined in the server, using ReactiveEvents.defineMaterials, like so:
ReactiveEvents.defineMaterials(event => {
event.builder('hyperlynx:lemon')
.defaultName('Lemon Cube')
.model('smooth')
.color(0xFFFF00)
.light(10)
.formulaBase('reactive:salt_block')
.addFormulaCost("reactive:light", 800)
.addFormulaCost("reactive:blaze", 200)
.build()
})The event uses a builder system similar to the Power and Reaction builders. You may choose any resource location to put within event.builder(), but please do not use the auto namespace, as that is reserved for automatically created Materials.
Between builder(ResourceLocation id) and build(), you can call a number of methods to define the properties of the Material:
-
defaultName(String name)sets the name the Material will have before anyone discovers it. Omitting this method will allow Players to name the material when they first encounter it. -
model(String model_name)set the model: the texture and sound of the Material. There are nine valid model names:-
"salt"is the default, and looks like Apprentice Salt Block -
"cracked"resembles Motion Salt Blocks -
"gel"has an animated fluid-like texture and can be walked through -
"streaked"has a busy rough texture -
"smooth"has a soft texture -
"circles"has curves that tile into long curved strips with adjacent blocks -
"squares"resembles Adept Salt Blocks -
"static"has an animated TV static texture -
"wool"resembles wool
-
-
breakStrength(float strength)sets how hard the block is to break in Survival -
blastResistance(float amount)sets the blast resistance of the block using the vanilla scale -
color(int hex)sets the tint of the item and block to the 8-bit RGB color provided. Alpha is not supported -
enchantPower(float power)sets how many levels of enchantment power this block contributes when placed near an Enchantment Table -
fireSource()makes this block sustain fires forever like Netherrack -
flammability(int rating)sets the flammability of the block using Minecraft's internal scale from 0 (not flammable) to 300 (maximally flammable) -
friction(float friction)sets the block's "friction" value. This value should usually be between 1 and 0.8 -
intangible()makes the block have no collision -
light(int light)sets the light emission from 0 to 15 -
magmaStep()makes this block dangerous to walk on like Magma Blocks -
redstone(int signal)sets the redstone signal output of the block -
redstoneMelting()causes this block to, when exposed to redstone, transform into the "gel" model and be able to be moved through -
semitangible()makes the block able to be fallen through while holding shift -
selfDefense(float damage)makes the block attack players that try to break it, doing the specified amount of damage each time -
warping()makes the block teleport occasionally to an adjacent empty space
There are also two methods for creating a "formula" that must be followed to craft the material. If no formula is defined, the material will not be craft-able, and must be obtained through other means, like /reactive material give.
-
formulaBase(ResourceLocation item_id)sets the base item for this material. Only the four base items defined by the mod are allowed, since this also determines the yield of the crafting process:- "reactive:salt_block"
- "reactive:adept_salt_block"
- "reactive:creation_salt_block"
- "minecraft:wool"
-
addFormulaCost(String power_id, int cost)sets the material to cost the given amount of Power. Calling this method multiple times keeps adding new required amounts, and the given power_id may be a built-in Power or one you've defined using KubeJS or Json Things
If you find areas that you were hoping would have more info, please open an issue!