Compare commits

..

22 Commits

Author SHA1 Message Date
WinDanesz 255d5add29 feat: Add support for optional NBT tags in currency items for wizard trades in the configs. example values::
minecraft:potion{Potion:"minecraft:strength"} 4
minecraft:enchanted_book{StoredEnchantments:[{id:16s,lvl:1s}]} 5
2026-05-12 01:35:58 +02:00
WinDanesz 032f1aa1a3 fix: Fixed potion effect desyncs in the codebase (lingering clientside-only potion effects with 0 duration) 2026-05-11 16:35:12 +00:00
WinDanesz ba71f2ae4a feat: Prevent crystal flowers from growing on client-side when bonemeal is used on grass. Fixes #907 2026-05-11 02:23:26 +02:00
WinDanesz 84a375c1be chore: update version to 4.3.18 2026-05-11 02:21:00 +02:00
WinDanesz 9a58332bf6 feat: Sneak clicking items place them in bookshelves. Sneak clicking a bookshelf with an empty hand removes items from the bookshelf 2026-05-11 02:00:24 +02:00
WinDanesz 62efc4cb02 feat: Added Ukrainian translation by Lotos 2026-05-10 23:07:08 +02:00
WinDanesz d6f9352940 feat: Add support for external mod predicates in ally designation system, allowing mods to define their own criteria for alliance status 2026-05-09 21:56:38 +02:00
WinDanesz 643ab1a1be feat: Improve owner ally check in AllyDesignationSystem to handle null owners and recursion 2026-05-09 20:40:03 +02:00
WinDanesz f499fcaeff chore: update version to 4.3.17 2026-05-05 23:59:56 +02:00
WinDanesz 80886c11ce Add Gradle build workflow to GitHub Actions 2026-05-05 17:35:55 +02:00
Xvareon 39f8d0ee7e fix: Fixed NPE crash for isAllied() function, causing world deadlocks (#906) 2026-05-05 11:30:20 +02:00
WinDanesz bbe4634535 Some bomb fixes, colored spell book overlay are now separate files 2026-05-05 02:43:22 +02:00
WinDanesz e7b8f7681c Fix ImbuementActivateEvent, fixes #901 2026-04-18 23:45:32 +02:00
ipdnaeip 64168d103d AllyDesignationSystem update
Added isOnSameTeam check to  isValidTarget and isAllied
2026-04-18 23:38:00 +02:00
ipdnaeip 600a321f49 Fixed spelling error in updateImbuedItem, Fixed improper evaluation for command spells causing them to not trigger spell.onFinish 2026-04-18 23:37:38 +02:00
WinDanesz e753f4df32 Fix: Ensure spell properties are initialized before checking if the spell is enabled 2026-04-18 23:34:56 +02:00
WinDanesz cbe7353122 Update version to 4.3.16 2025-12-31 00:37:10 +01:00
WinDanesz de5682a960 Add colorized bookmarks based on book element, requires spells to be discovered. It can be turned off in the Client settings. Another tweak setting can make it depend on the Archivist's Eyeglass artefact 2025-12-30 18:35:35 +01:00
Lumberjacksparrow b451399323 Fix for inventory key not exiting the Arcane Workbench GUI #897 (Lumberjacksparrow) 2025-12-29 22:17:18 +01:00
ipdnaeip e547aa32ca Fix: Modifiers persist in continuous spells cast by scrolls (#895) (ipdnaeip)
* ItemScroll Modifier Fix

Modifiers persist in continuous spells cast by scrolls.

Added check for tick 0 as to not reapply SpellModifier changes

* Fix comment formatting in ItemScroll.java

---------

Co-authored-by: WinDanesz <31292708+WinDanesz@users.noreply.github.com>
2025-12-29 22:16:24 +01:00
WinDanesz 3fe9d762da Added support for item metadata for "currencyItems" settings (List of registry names of items which wizard trades can use as currency) 2025-12-29 22:11:21 +01:00
WinDanesz 20b53596e2 fix: Forfeit sounds are properly registered for add-on forfeits 2025-10-22 23:50:04 +02:00
79 changed files with 3661 additions and 259 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"homepage": "https://www.curseforge.com/minecraft/mc-mods/electroblobs-wizardry",
"promos": {
"1.12.2-latest": "4.3.15",
"1.12.2-recommended": "4.3.15"
"1.12.2-latest": "4.3.18",
"1.12.2-recommended": "4.3.18"
}
}
+50
View File
@@ -0,0 +1,50 @@
name: Gradle Build
on:
push:
branches: [ "**" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
- name: Load Cache
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/1.12.2' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build --max-workers=1 --no-daemon
- name: Upload build artifacts
if: success()
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: build/libs/*.jar
retention-days: 30
+1 -1
View File
@@ -38,7 +38,7 @@ apply plugin: org.ajoberstar.grgit.gradle.GrgitPlugin
apply plugin: 'wtf.gofancy.fancygradle'
version = "4.3.15"
version = "4.3.18"
group= "electroblob.wizardry"// http://maven.apache.org/guides/mini/guide-naming-conventions.html
archivesBaseName = "ElectroblobsWizardry"
@@ -43,6 +43,10 @@ public class CommonProxy {
public void registerRenderers(){}
public void registerItemColorHandlers(){}
public void registerModelProperties(){}
public void initialiseLayers(){}
public void initialiseAnimations(){}
@@ -175,6 +179,8 @@ public class CommonProxy {
public void handleConquerShrinePacket(PacketConquerShrine.Message message){}
public void handleBombExplosionPacket(PacketBombExplosion.Message message){}
// SECTION Misc
// ===============================================================================================================
@@ -13,6 +13,9 @@ import net.minecraft.entity.EntityList;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTException;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.config.ConfigCategory;
import net.minecraftforge.common.config.Configuration;
@@ -251,6 +254,8 @@ public final class Settings {
public Pair<ResourceLocation, Short>[] bowItemWhitelist = parseItemMetaStrings();
/** <b>[Server-only]</b> Map of items to values which wizard trades may use as currency. */
public Map<Pair<ResourceLocation, Short>, Integer> currencyItems = new HashMap<>();
/** <b>[Server-only]</b> Map of optional NBT tags for currency items, keyed the same as {@link #currencyItems}. */
public Map<Pair<ResourceLocation, Short>, NBTTagCompound> currencyItemNbt = new HashMap<>();
/** <b>[Server-only]</b> Global damage scaling factor for all player magic damage. */
public double playerDamageScale = 1.0;
/** <b>[Server-only]</b> Global damage scaling factor for all npc magic damage. */
@@ -355,6 +360,8 @@ public final class Settings {
* velocity-based one.
*/
public boolean replaceVanillaFallDamage = true;
/** <b>[Synchronised]</b> Whether spell book colors are only shown when the player has the Archivist's Eyeglass equipped. */
public boolean spellBookColorsRequireArchivistsEyeglass = false;
/** <b>[Synchronised]</b> Chance of 'misreading' an undiscovered spell and triggering a forfeit instead. */
public double forfeitChance = 0.2;
/** <b>[Synchronised]</b> Progression requirements for upgrading a wand to each tier. */
@@ -461,6 +468,8 @@ public final class Settings {
public static final String DEFAULT_HUD_SKIN_KEY = "default"; // Defined here so it's not in a client-only class.
/** <b>[Client-only]</b> The string identifier of the skin used for the spell HUD. */
public String spellHUDSkin = DEFAULT_HUD_SKIN_KEY;
/** <b>[Client-only]</b> Whether to show elemental colors on spell books for discovered spells. */
public boolean spellBookColors = true;
/** Set of constants for each of the eight positions that the spell HUD can be in. */
public enum GuiPosition {
@@ -1103,6 +1112,13 @@ public final class Settings {
replaceVanillaFallDamage = property.getBoolean();
propOrder.add(property.getName());
property = config.get(TWEAKS_CATEGORY, "spellBookColorsRequireArchivistsEyeglass", false,
"If true, spell book colors are only shown when the player has the charm of spell discovery equipped.");
property.setLanguageKey("config." + Wizardry.MODID + ".spell_book_colors_require_charm");
Wizardry.proxy.setToNamedBooleanEntry(property);
spellBookColorsRequireArchivistsEyeglass = property.getBoolean();
propOrder.add(property.getName());
property = config.get(TWEAKS_CATEGORY, "blindnessTweak", true,
"Whether to tweak the blindness effect to reduce follow distance when used on non-players. This automatically disables itself in favour of Potion Core's implementation if installed.");
property.setLanguageKey("config." + Wizardry.MODID + ".blindness_tweak");
@@ -1213,22 +1229,43 @@ public final class Settings {
bookshelfSearchRadius = property.getInt();
propOrder.add(property.getName());
property = config.get(TWEAKS_CATEGORY, "currencyItems", new String[]{"gold_ingot 3", "emerald 6"}, "List of registry names of items which wizard trades can use as currency (in the first slot; the second slot is unaffected). Each entry in this list should consist of an item registry name, followed by a single space, then an integer which defines the 'value' of the item. Higher values mean fewer of that currency item are required for a given trade.",
Pattern.compile("[A-Za-z0-9:_]+ [0-9]+"));
property = config.get(TWEAKS_CATEGORY, "currencyItems", new String[]{"gold_ingot 3", "emerald 6"}, "List of registry names of items which wizard trades can use as currency (in the first slot; the second slot is unaffected). Each entry in this list should consist of an item registry name, followed by a single space, then an integer which defines the 'value' of the item. Higher values mean fewer of that currency item are required for a given trade. To specify metadata, use the format 'modid:item:meta value'. For example, 'minecraft:wool:1 5'. If no metadata is given, any metadata will be accepted. NBT tags may optionally be specified in SNBT format immediately after the item/metadata, e.g. 'minecraft:potion{Potion:\"minecraft:strength\"} 3'.",
Pattern.compile(".+ [0-9]+"));
property.setLanguageKey("config." + Wizardry.MODID + ".currency_items");
propOrder.add(property.getName());
currencyItems = new HashMap<>();
currencyItemNbt = new HashMap<>();
for(String string : property.getStringList()){
string = string.toLowerCase(Locale.ROOT).trim();
String[] args = string.split(" ");
if(args.length != 2){
string = string.trim();
// Split on the last space to separate the item spec from the integer value; this correctly
// handles NBT strings that contain spaces inside quoted values.
int lastSpace = string.lastIndexOf(' ');
if(lastSpace < 0){
Wizardry.logger.warn("Invalid entry in currency items: {}", string);
continue; // Ignore invalid entries, the pattern above should ensure this never happens
continue;
}
String itemSpec = string.substring(0, lastSpace).toLowerCase(Locale.ROOT);
String valueStr = string.substring(lastSpace + 1);
// Check for an optional NBT tag (do NOT lowercase the raw NBT string - it is case-sensitive)
int nbtStart = itemSpec.indexOf('{');
NBTTagCompound nbt = null;
if(nbtStart >= 0){
// Re-extract the NBT portion from the original (non-lowercased) string to preserve case
String rawNbt = string.substring(nbtStart, lastSpace);
itemSpec = itemSpec.substring(0, nbtStart);
try{
nbt = JsonToNBT.getTagFromJson(rawNbt);
}catch(NBTException e){
Wizardry.logger.warn("Invalid NBT in currency items '{}': {}", string, e.getMessage());
continue;
}
}
try{
currencyItems.put(parseItemMetaString(args[0]), Integer.parseInt(args[1]));
Pair<ResourceLocation, Short> key = parseItemMetaString(itemSpec);
currencyItems.put(key, Integer.parseInt(valueStr));
if(nbt != null) currencyItemNbt.put(key, nbt);
}catch(NumberFormatException e){
Wizardry.logger.warn("Invalid integer in currency items: {}", args[1]);
Wizardry.logger.warn("Invalid integer in currency items: {}", valueStr);
}
}
@@ -1487,6 +1524,14 @@ public final class Settings {
Wizardry.proxy.setToNamedBooleanEntry(property);
showChargeMeter = property.getBoolean();
propOrder.add(property.getName());
property = config.get(CLIENT_CATEGORY, "spellBookColors", true, "Whether to show elemental colors on spell books for discovered spells.");
property.setLanguageKey("config." + Wizardry.MODID + ".spell_book_colors");
property.setRequiresWorldRestart(false);
Wizardry.proxy.setToNamedBooleanEntry(property);
spellBookColors = property.getBoolean();
propOrder.add(property.getName());
property = config.get(CLIENT_CATEGORY, "loadHandbook", true, "Whether to initialise the in-game handbook. Setting this to false will brick the in-game handbook, but it might help if you have startup crashes.");
property.setLanguageKey("config." + Wizardry.MODID + ".load_handbook");
property.setRequiresWorldRestart(false);
@@ -1817,16 +1862,46 @@ public final class Settings {
string = string.toLowerCase(Locale.ROOT).trim();
String[] itemArgs = string.split(":");
String item;
short meta;
try {
meta = Short.parseShort(itemArgs[itemArgs.length-1]);
item = String.join(":", Arrays.copyOfRange(itemArgs, 0, itemArgs.length-1));
}catch(NumberFormatException e){ // If no metadata is specified
meta = OreDictionary.WILDCARD_VALUE;
int lastColon = string.lastIndexOf(':');
if (lastColon > -1 && lastColon < string.length() - 1) { // if there is a colon and it's not the last char
String metaString = string.substring(lastColon + 1);
try {
short parsedMeta = Short.parseShort(metaString);
String itemString = string.substring(0, lastColon);
// If itemString has no colon, it could be 'item:meta' or 'namespace:numeric_path'.
// e.g., 'wool:1' vs 'mod:123'. We want to support 'wool:1' as item 'wool' with meta 1.
// We can't reliably distinguish the two cases without knowing all namespaces, so we have to use
// a heuristic. The original code favoured the 'item:meta' interpretation, let's stick with that.
// The issue was with 'namespace:numeric_path' which this logic now handles.
if (itemString.indexOf(':') == -1) {
item = itemString;
meta = parsedMeta;
} else {
// Disambiguate things like 'mod:123:4' (item 'mod:123' with meta 4) vs 'mod:item:123' (item 'mod:item' with meta 123)
// If the bit before the last colon is a valid resource location, it's probably item:meta
try {
new ResourceLocation(itemString);
item = itemString;
meta = parsedMeta;
} catch (Exception e) {
item = string;
meta = OreDictionary.WILDCARD_VALUE;
}
}
} catch (NumberFormatException e) {
// The part after colon is not a number, so it's part of the name
item = string;
meta = OreDictionary.WILDCARD_VALUE;
}
} else {
item = string;
meta = OreDictionary.WILDCARD_VALUE;
}
return Pair.of(new ResourceLocation(item), meta);
@@ -82,7 +82,7 @@ public class Wizardry {
* 1.x.x represents Minecraft 1.7.x versions, 2.x.x represents Minecraft 1.10.x versions, 3.x.x represents Minecraft
* 1.11.x versions, and so on.
*/
public static final String VERSION = "4.3.15";
public static final String VERSION = "4.3.18";
/**
* Json file used by Forge's built-in <a href="https://mcforge.readthedocs.io/en/1.12.x/gettingstarted/autoupdate/">update checker</a>.
@@ -198,6 +198,8 @@ public class Wizardry {
// Client-side stuff (via proxies)
proxy.initGuiBits();
proxy.registerParticles();
proxy.registerItemColorHandlers();
proxy.registerModelProperties();
proxy.registerSoundEventListener();
}
@@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList;
import electroblob.wizardry.Settings;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.WizardryGuiHandler;
import electroblob.wizardry.inventory.ContainerBookshelf;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.registry.WizardryTabs;
import electroblob.wizardry.tileentity.TileEntityBookshelf;
@@ -22,6 +23,7 @@ import net.minecraft.init.Items;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.InventoryHelper;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.*;
import net.minecraft.util.math.BlockPos;
@@ -162,7 +164,37 @@ public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvid
TileEntity tileEntity = world.getTileEntity(pos);
if(tileEntity == null || player.isSneaking()){
if(tileEntity == null){
return false;
}
if(player.isSneaking()){
ItemStack heldItem = player.getHeldItem(hand);
if(heldItem.isEmpty() && tileEntity instanceof TileEntityBookshelf){
if(!world.isRemote){
TileEntityBookshelf bookshelf = (TileEntityBookshelf)tileEntity;
for(int i = 0; i < bookshelf.getSizeInventory(); i++){
ItemStack stack = bookshelf.getStackInSlot(i);
if(!stack.isEmpty()){
player.addItemStackToInventory(stack.copy());
bookshelf.setInventorySlotContents(i, ItemStack.EMPTY);
break;
}
}
}
return true;
}else if(!heldItem.isEmpty() && ContainerBookshelf.isBook(heldItem) && tileEntity instanceof TileEntityBookshelf){
if(!world.isRemote){
TileEntityBookshelf bookshelf = (TileEntityBookshelf)tileEntity;
for(int i = 0; i < bookshelf.getSizeInventory(); i++){
if(bookshelf.getStackInSlot(i).isEmpty()){
bookshelf.setInventorySlotContents(i, heldItem.splitStack(1));
break;
}
}
}
return true;
}
return false;
}
@@ -57,7 +57,7 @@ public class BlockCrystalFlower extends BlockBush {
@SubscribeEvent
public static void onBonemealEvent(BonemealEvent event){
// Grows crystal flowers when bonemeal is used on grass
if(Wizardry.settings.bonemealGrowsCrystalFlowers && event.getBlock().getBlock() == Blocks.GRASS){
if(!event.getWorld().isRemote && Wizardry.settings.bonemealGrowsCrystalFlowers && event.getBlock().getBlock() == Blocks.GRASS){
BlockPos pos = event.getPos().add(event.getWorld().rand.nextInt(8) - event.getWorld().rand.nextInt(8),
event.getWorld().rand.nextInt(4) - event.getWorld().rand.nextInt(4),
@@ -1,7 +1,6 @@
package electroblob.wizardry.client;
import electroblob.wizardry.CommonProxy;
import electroblob.wizardry.Settings;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.client.animation.ActionAnimation;
@@ -19,6 +18,8 @@ import electroblob.wizardry.client.model.ModelRobeArmour;
import electroblob.wizardry.client.model.ModelSageArmour;
import electroblob.wizardry.client.model.ModelWizardArmour;
import electroblob.wizardry.client.particle.*;
import electroblob.wizardry.constants.Element;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.client.particle.ParticleWizardry.IWizardryParticleFactory;
import electroblob.wizardry.client.renderer.RenderSpectralGolem;
import electroblob.wizardry.client.renderer.entity.*;
@@ -38,6 +39,7 @@ import electroblob.wizardry.entity.projectile.*;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
import electroblob.wizardry.integration.antiqueatlas.WizardryAntiqueAtlasIntegration;
import electroblob.wizardry.item.ItemArtefact;
import electroblob.wizardry.item.ItemScroll;
import electroblob.wizardry.item.ItemSpellBook;
import electroblob.wizardry.item.ItemWand;
@@ -715,6 +717,65 @@ public class ClientProxy extends CommonProxy {
}else Wizardry.logger.warn("Received a PacketConquerShrine, but there was no shrine core at the position sent");
}
@Override
public void handleBombExplosionPacket(PacketBombExplosion.Message message){
net.minecraft.world.World world = Minecraft.getMinecraft().world;
double x = message.x, y = message.y, z = message.z;
float blastMultiplier = message.blastMultiplier;
switch(message.bombType){
case PacketBombExplosion.FIREBOMB:
ParticleBuilder.create(Type.FLASH).pos(x, y, z).scale(5 * blastMultiplier).clr(1, 0.6f, 0).spawn(world);
for(int i = 0; i < 60 * blastMultiplier; i++){
ParticleBuilder.create(Type.MAGIC_FIRE, world.rand, x, y, z, 2 * blastMultiplier, false)
.time(10 + world.rand.nextInt(4)).scale(2 + world.rand.nextFloat()).spawn(world);
ParticleBuilder.create(Type.DARK_MAGIC, world.rand, x, y, z, 2 * blastMultiplier, false)
.clr(1.0f, 0.2f + world.rand.nextFloat() * 0.4f, 0.0f).spawn(world);
}
world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, x, y, z, 0, 0, 0);
break;
case PacketBombExplosion.POISON_BOMB:
ParticleBuilder.create(Type.FLASH).pos(x, y, z).scale(5 * blastMultiplier)
.clr(0.2f + world.rand.nextFloat() * 0.3f, 0.6f, 0.0f).spawn(world);
for(int i = 0; i < 60 * blastMultiplier; i++){
ParticleBuilder.create(Type.SPARKLE, world.rand, x, y, z, 2 * blastMultiplier, false).time(35)
.scale(2).clr(0.2f + world.rand.nextFloat() * 0.3f, 0.6f, 0.0f).spawn(world);
ParticleBuilder.create(Type.DARK_MAGIC, world.rand, x, y, z, 2 * blastMultiplier, false)
.clr(0.2f + world.rand.nextFloat() * 0.2f, 0.8f, 0.0f).spawn(world);
}
world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, x, y, z, 0, 0, 0);
break;
case PacketBombExplosion.SMOKE_BOMB:
ParticleBuilder.create(Type.FLASH).pos(x, y, z).scale(5 * blastMultiplier).clr(0, 0, 0).spawn(world);
world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, x, y, z, 0, 0, 0);
for(int i = 0; i < 60 * blastMultiplier; i++){
float brightness = world.rand.nextFloat() * 0.1f + 0.1f;
ParticleBuilder.create(Type.CLOUD, world.rand, x, y, z, 2 * blastMultiplier, false)
.clr(brightness, brightness, brightness).time(80 + world.rand.nextInt(12)).shaded(true).spawn(world);
brightness = world.rand.nextFloat() * 0.3f;
ParticleBuilder.create(Type.DARK_MAGIC, world.rand, x, y, z, 2 * blastMultiplier, false)
.clr(brightness, brightness, brightness).spawn(world);
}
break;
case PacketBombExplosion.SPARK_BOMB:
ParticleBuilder.spawnShockParticles(world, x, y, z);
for(int id : message.secondaryTargetIDs){
net.minecraft.entity.Entity target = world.getEntityByID(id);
if(target instanceof net.minecraft.entity.EntityLivingBase){
ParticleBuilder.create(Type.LIGHTNING).pos(x, y, z).target(target).spawn(world);
ParticleBuilder.spawnShockParticles(world, target.posX,
target.posY + target.height / 2, target.posZ);
}
}
break;
}
}
// Rendering
// ===============================================================================================================
@@ -877,4 +938,51 @@ public class ClientProxy extends CommonProxy {
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityImbuementAltar.class, new RenderImbuementAltar());
}
public void registerItemColorHandlers() {
// Spell book overlay textures are now per-element, so no runtime tinting is needed.
}
public void registerModelProperties() {
Item spellBook = WizardryItems.spell_book;
spellBook.addPropertyOverride(new ResourceLocation(Wizardry.MODID, "discovered"), (stack, world, entity) -> {
if (!Wizardry.settings.spellBookColors) return 0.0f;
boolean discovered = shouldDisplayDiscovered(Spell.byMetadata(stack.getMetadata()), stack);
if(discovered && Wizardry.settings.spellBookColorsRequireArchivistsEyeglass){
// Entity can be null so we have to check
if(entity instanceof EntityPlayer){
return ItemArtefact.isArtefactActive((EntityPlayer)entity, WizardryItems.charm_spell_discovery) ? 1.0f : 0.0f;
}
// If there's no entity, there's no charm, so no colour
return 0.0f;
}
return discovered ? 1.0f : 0.0f;
});
// Returns ordinal+1 for each element (1=MAGIC through 8=HEALING), or 0 if not yet discovered.
// Drives per-element bookmark texture selection via model overrides in spell_book.json.
spellBook.addPropertyOverride(new ResourceLocation(Wizardry.MODID, "element"), (stack, world, entity) -> {
if (!Wizardry.settings.spellBookColors) return 0.0f;
Spell spell = Spell.byMetadata(stack.getMetadata());
boolean discovered = shouldDisplayDiscovered(spell, stack);
if(!discovered) return 0.0f;
if(Wizardry.settings.spellBookColorsRequireArchivistsEyeglass){
if(entity instanceof EntityPlayer){
if(!ItemArtefact.isArtefactActive((EntityPlayer)entity, WizardryItems.charm_spell_discovery)) return 0.0f;
} else {
return 0.0f;
}
}
Element element = spell.getElement();
return element == null ? 0.0f : (float)(element.ordinal() + 1);
});
}
}
@@ -1,5 +1,6 @@
package electroblob.wizardry.client;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.renderer.overlay.RenderBlinkEffect;
import electroblob.wizardry.data.DispenserCastingData;
import electroblob.wizardry.data.SpellEmitterData;
@@ -13,7 +14,6 @@ import electroblob.wizardry.spell.SixthSense;
import electroblob.wizardry.spell.SlowTime;
import electroblob.wizardry.spell.Transience;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.renderer.BufferBuilder;
@@ -31,6 +31,9 @@ import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.text.TextFormatting;
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
@@ -196,6 +199,15 @@ public final class WizardryClientEventHandler {
}
}
@SubscribeEvent
public static void onItemTooltip(ItemTooltipEvent event) {
if (event.getItemStack().getItem() == WizardryItems.charm_spell_discovery) {
if (Wizardry.settings.spellBookColorsRequireArchivistsEyeglass) {
event.getToolTip().add(TextFormatting.GRAY + I18n.format("item.ebwizardry:charm_spell_discovery.desc.color"));
}
}
}
/**
* Renders an overlay across the entire screen.
* @param resolution The screen resolution
@@ -515,12 +515,13 @@ public class GuiArcaneWorkbench extends GuiContainer {
@Override
protected void keyTyped(char typedChar, int keyCode) throws IOException {
if(this.searchNeedsClearing){
this.searchNeedsClearing = false;
if(this.searchNeedsClearing){
this.searchNeedsClearing = false;
this.searchField.setText("");
}
if(this.searchField.textboxKeyTyped(typedChar, keyCode)){
// Allow exiting the GUI by pressing the inventory button. Does not work if bookshelves are present by default
if(this.searchField.getVisible() && this.searchField.textboxKeyTyped(typedChar, keyCode)){
arcaneWorkbenchContainer.setSearchText(searchField.getText().toLowerCase(Locale.ROOT));
}else{
super.keyTyped(typedChar, keyCode);
@@ -1062,4 +1063,4 @@ public class GuiArcaneWorkbench extends GuiContainer {
}
}
}
}
@@ -7,7 +7,6 @@ import electroblob.wizardry.enchantment.Imbuement;
import electroblob.wizardry.entity.living.ISummonedCreature;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
import electroblob.wizardry.item.ItemWand;
import electroblob.wizardry.packet.PacketCastContinuousSpell;
import electroblob.wizardry.packet.PacketPlayerSync;
import electroblob.wizardry.packet.WizardryPacketHandler;
@@ -356,20 +355,20 @@ public class WizardData implements INBTSerializable<NBTTagCompound> {
// For each item in the player's inventory
for(ItemStack stack : player.inventory.mainInventory){
updateImbutedItem(stack, activeImbuements);
updateImbuedItem(stack, activeImbuements);
}
for(ItemStack stack : player.inventory.armorInventory){
updateImbutedItem(stack, activeImbuements);
updateImbuedItem(stack, activeImbuements);
}
for(ItemStack stack : player.inventory.offHandInventory){
updateImbutedItem(stack, activeImbuements);
updateImbuedItem(stack, activeImbuements);
}
// Removes all imbuements from the map that are no longer active
this.imbuementDurations.keySet().retainAll(activeImbuements);
}
private void updateImbutedItem(ItemStack stack, Set<Imbuement> activeImbuements){
private void updateImbuedItem(ItemStack stack, Set<Imbuement> activeImbuements){
if(stack.isItemEnchanted()){
@@ -466,7 +465,7 @@ public class WizardData implements INBTSerializable<NBTTagCompound> {
if(this.castCommandSpell != null && this.castCommandSpell.isContinuous){
if(castCommandTick >= castCommandDuration){
if(castCommandTick > castCommandDuration){
this.stopCastingContinuousSpell();
return;
}
@@ -617,7 +617,10 @@ public class EntityWizard extends EntityCreature implements INpc, IMerchant, ISp
// ((tier.ordinal() + 1) * 16 + rand.nextInt(6)) gives a 'value' for the item being bought
// This is then divided by the value of the currency item to give a price
// The absolute maximum stack size that can result from this calculation (with value = 1) is 64.
return new ItemStack(item, MathHelper.clamp((8 + tier.ordinal() * 16 + rand.nextInt(9)) / value, 1, 64), meta);
ItemStack result = new ItemStack(item, MathHelper.clamp((8 + tier.ordinal() * 16 + rand.nextInt(9)) / value, 1, 64), meta);
NBTTagCompound nbt = Wizardry.settings.currencyItemNbt.get(itemName);
if(nbt != null) result.setTagCompound(nbt.copy());
return result;
}
private ItemStack getRandomItemOfTier(Tier tier){
@@ -1,18 +1,18 @@
package electroblob.wizardry.entity.projectile;
import electroblob.wizardry.packet.PacketBombExplosion;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.EntityUtils;
import electroblob.wizardry.util.MagicDamage;
import electroblob.wizardry.util.MagicDamage.DamageType;
import electroblob.wizardry.util.ParticleBuilder;
import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.util.List;
@@ -29,41 +29,28 @@ public class EntityFirebomb extends EntityBomb {
@Override
protected void onImpact(RayTraceResult rayTrace){
Entity entityHit = rayTrace.entityHit;
if(entityHit != null){
// This is if the firebomb gets a direct hit
float damage = Spells.firebomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.FIRE).setProjectile(),
damage);
if(!MagicDamage.isEntityImmune(DamageType.FIRE, entityHit))
entityHit.setFire(Spells.firebomb.getProperty(Spell.BURN_DURATION).intValue());
}
// Particle effect
if(world.isRemote){
ParticleBuilder.create(Type.FLASH).pos(this.getPositionVector()).scale(5 * blastMultiplier).clr(1, 0.6f, 0)
.spawn(world);
for(int i = 0; i < 60 * blastMultiplier; i++){
ParticleBuilder.create(Type.MAGIC_FIRE, rand, posX, posY, posZ, 2*blastMultiplier, false)
.time(10 + rand.nextInt(4)).scale(2 + rand.nextFloat()).spawn(world);
ParticleBuilder.create(Type.DARK_MAGIC, rand, posX, posY, posZ, 2*blastMultiplier, false)
.clr(1.0f, 0.2f + rand.nextFloat() * 0.4f, 0.0f).spawn(world);
}
this.world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, this.posX, this.posY, this.posZ, 0, 0, 0);
}
if(!this.world.isRemote){
Entity entityHit = rayTrace.entityHit;
if(entityHit != null){
// This is if the firebomb gets a direct hit
float damage = Spells.firebomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.FIRE).setProjectile(),
damage);
if(!MagicDamage.isEntityImmune(DamageType.FIRE, entityHit))
entityHit.setFire(Spells.firebomb.getProperty(Spell.BURN_DURATION).intValue());
}
// Notify clients to play the explosion effect
WizardryPacketHandler.net.sendToAllAround(
new PacketBombExplosion.Message(PacketBombExplosion.FIREBOMB, posX, posY, posZ, blastMultiplier),
new NetworkRegistry.TargetPoint(world.provider.getDimension(), posX, posY, posZ, 64));
this.playSound(WizardrySounds.ENTITY_FIREBOMB_SMASH, 1.5F, rand.nextFloat() * 0.4F + 0.6F);
this.playSound(WizardrySounds.ENTITY_FIREBOMB_FIRE, 1, 1);
@@ -82,9 +69,9 @@ public class EntityFirebomb extends EntityBomb {
target.setFire(Spells.firebomb.getProperty(Spell.BURN_DURATION).intValue());
}
}
this.setDead();
}
this.setDead();
}
}
@@ -1,20 +1,20 @@
package electroblob.wizardry.entity.projectile;
import electroblob.wizardry.packet.PacketBombExplosion;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.EntityUtils;
import electroblob.wizardry.util.MagicDamage;
import electroblob.wizardry.util.MagicDamage.DamageType;
import electroblob.wizardry.util.ParticleBuilder;
import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.init.MobEffects;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.util.List;
@@ -31,44 +31,30 @@ public class EntityPoisonBomb extends EntityBomb {
@Override
protected void onImpact(RayTraceResult rayTrace){
Entity entityHit = rayTrace.entityHit;
if(entityHit != null){
// This is if the poison bomb gets a direct hit
float damage = Spells.poison_bomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.POISON).setProjectile(),
damage);
if(entityHit instanceof EntityLivingBase && !MagicDamage.isEntityImmune(DamageType.POISON, entityHit))
((EntityLivingBase)entityHit).addPotionEffect(new PotionEffect(MobEffects.POISON,
Spells.poison_bomb.getProperty(Spell.DIRECT_EFFECT_DURATION).intValue(),
Spells.poison_bomb.getProperty(Spell.DIRECT_EFFECT_STRENGTH).intValue()));
}
// Particle effect
if(world.isRemote){
ParticleBuilder.create(Type.FLASH).pos(this.getPositionVector()).scale(5 * blastMultiplier)
.clr(0.2f + rand.nextFloat() * 0.3f, 0.6f, 0.0f).spawn(world);
for(int i = 0; i < 60 * blastMultiplier; i++){
ParticleBuilder.create(Type.SPARKLE, rand, posX, posY, posZ, 2*blastMultiplier, false).time(35)
.scale(2).clr(0.2f + rand.nextFloat() * 0.3f, 0.6f, 0.0f).spawn(world);
ParticleBuilder.create(Type.DARK_MAGIC, rand, posX, posY, posZ, 2*blastMultiplier, false)
.clr(0.2f + rand.nextFloat() * 0.2f, 0.8f, 0.0f).spawn(world);
}
// Spawning this after the other particles fixes the rendering colour bug. It's a bit of a cheat, but it
// works pretty well.
this.world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, this.posX, this.posY, this.posZ, 0, 0, 0);
}
if(!this.world.isRemote){
Entity entityHit = rayTrace.entityHit;
if(entityHit != null){
// This is if the poison bomb gets a direct hit
float damage = Spells.poison_bomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.POISON).setProjectile(),
damage);
if(entityHit instanceof EntityLivingBase && !MagicDamage.isEntityImmune(DamageType.POISON, entityHit))
((EntityLivingBase)entityHit).addPotionEffect(new PotionEffect(MobEffects.POISON,
Spells.poison_bomb.getProperty(Spell.DIRECT_EFFECT_DURATION).intValue(),
Spells.poison_bomb.getProperty(Spell.DIRECT_EFFECT_STRENGTH).intValue()));
}
// Notify clients to play the explosion effect
WizardryPacketHandler.net.sendToAllAround(
new PacketBombExplosion.Message(PacketBombExplosion.POISON_BOMB, posX, posY, posZ, blastMultiplier),
new NetworkRegistry.TargetPoint(world.provider.getDimension(), posX, posY, posZ, 64));
this.playSound(WizardrySounds.ENTITY_POISON_BOMB_SMASH, 1.5F, rand.nextFloat() * 0.4F + 0.6F);
this.playSound(WizardrySounds.ENTITY_POISON_BOMB_POISON, 1.2F, 1.0f);
@@ -88,8 +74,8 @@ public class EntityPoisonBomb extends EntityBomb {
Spells.poison_bomb.getProperty(Spell.SPLASH_EFFECT_STRENGTH).intValue()));
}
}
this.setDead();
}
this.setDead();
}
}
@@ -1,17 +1,17 @@
package electroblob.wizardry.entity.projectile;
import electroblob.wizardry.packet.PacketBombExplosion;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.EntityUtils;
import electroblob.wizardry.util.ParticleBuilder;
import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.init.MobEffects;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.util.List;
@@ -29,27 +29,13 @@ public class EntitySmokeBomb extends EntityBomb {
@Override
protected void onImpact(RayTraceResult rayTrace){
// Particle effect
if(world.isRemote){
ParticleBuilder.create(Type.FLASH).pos(this.getPositionVector()).scale(5 * blastMultiplier).clr(0, 0, 0).spawn(world);
this.world.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, this.posX, this.posY, this.posZ, 0, 0, 0);
for(int i = 0; i < 60 * blastMultiplier; i++){
float brightness = rand.nextFloat() * 0.1f + 0.1f;
ParticleBuilder.create(Type.CLOUD, rand, posX, posY, posZ, 2*blastMultiplier, false)
.clr(brightness, brightness, brightness).time(80 + this.rand.nextInt(12)).shaded(true).spawn(world);
brightness = rand.nextFloat() * 0.3f;
ParticleBuilder.create(Type.DARK_MAGIC, rand, posX, posY, posZ, 2*blastMultiplier, false)
.clr(brightness, brightness, brightness).spawn(world);
}
}
if(!this.world.isRemote){
// Notify clients to play the explosion effect
WizardryPacketHandler.net.sendToAllAround(
new PacketBombExplosion.Message(PacketBombExplosion.SMOKE_BOMB, posX, posY, posZ, blastMultiplier),
new NetworkRegistry.TargetPoint(world.provider.getDimension(), posX, posY, posZ, 64));
this.playSound(WizardrySounds.ENTITY_SMOKE_BOMB_SMASH, 1.5F, rand.nextFloat() * 0.4F + 0.6F);
this.playSound(WizardrySounds.ENTITY_SMOKE_BOMB_SMOKE, 1.2F, 1.0f);
@@ -65,8 +51,8 @@ public class EntitySmokeBomb extends EntityBomb {
target.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, duration, 0));
}
}
this.setDead();
}
this.setDead();
}
}
@@ -1,5 +1,7 @@
package electroblob.wizardry.entity.projectile;
import electroblob.wizardry.packet.PacketBombExplosion;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.spell.Spell;
@@ -13,7 +15,9 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.util.ArrayList;
import java.util.List;
public class EntitySparkBomb extends EntityBomb {
@@ -31,47 +35,37 @@ public class EntitySparkBomb extends EntityBomb {
@Override
protected void onImpact(RayTraceResult rayTrace){
this.playSound(WizardrySounds.ENTITY_SPARK_BOMB_HIT_BLOCK, 0.5f, 0.5f);
Entity entityHit = rayTrace.entityHit;
if(!this.world.isRemote){
if(entityHit != null){
// This is if the spark bomb gets a direct hit
float damage = Spells.spark_bomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
Entity entityHit = rayTrace.entityHit;
this.playSound(WizardrySounds.ENTITY_SPARK_BOMB_HIT, 1.0F, 1.2F / (this.rand.nextFloat() * 0.2F + 0.9F));
if(entityHit != null){
// This is if the spark bomb gets a direct hit
float damage = Spells.spark_bomb.getProperty(Spell.DIRECT_DAMAGE).floatValue() * damageMultiplier;
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.SHOCK).setProjectile(),
damage);
this.playSound(WizardrySounds.ENTITY_SPARK_BOMB_HIT, 1.0F, 1.2F / (this.rand.nextFloat() * 0.2F + 0.9F));
}
entityHit.attackEntityFrom(
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.SHOCK).setProjectile(),
damage);
}
// Particle effect
if(world.isRemote){
ParticleBuilder.spawnShockParticles(world, posX, posY + height/2, posZ);
}
double seekerRange = Spells.spark_bomb.getProperty(Spell.EFFECT_RADIUS).doubleValue() * blastMultiplier;
double seekerRange = Spells.spark_bomb.getProperty(Spell.EFFECT_RADIUS).doubleValue() * blastMultiplier;
List<EntityLivingBase> targets = EntityUtils.getLivingWithinRadius(seekerRange, this.posX, this.posY,
this.posZ, this.world);
List<EntityLivingBase> targets = EntityUtils.getLivingWithinRadius(seekerRange, this.posX, this.posY,
this.posZ, this.world);
List<Integer> secondaryTargetIDs = new ArrayList<>();
for(int i = 0; i < Math.min(targets.size(), Spells.spark_bomb.getProperty(SECONDARY_MAX_TARGETS).intValue()); i++){
for(int i = 0; i < Math.min(targets.size(), Spells.spark_bomb.getProperty(SECONDARY_MAX_TARGETS).intValue()); i++){
boolean flag = targets.get(i) != entityHit && targets.get(i) != this.getThrower()
&& !(targets.get(i) instanceof EntityPlayer
&& ((EntityPlayer)targets.get(i)).isCreative());
boolean flag = targets.get(i) != entityHit && targets.get(i) != this.getThrower()
&& !(targets.get(i) instanceof EntityPlayer
&& ((EntityPlayer)targets.get(i)).isCreative());
// Detects (client side) if target is the thrower, to stop particles being spawned around them.
//if(flag && world.isRemote && targets.get(i).getEntityId() == this.playerID) flag = false;
if(flag){
EntityLivingBase target = targets.get(i);
if(!this.world.isRemote){
if(flag){
EntityLivingBase target = targets.get(i);
target.playSound(WizardrySounds.ENTITY_SPARK_BOMB_CHAIN, 1.0F, rand.nextFloat() * 0.4F + 1.5F);
@@ -79,11 +73,18 @@ public class EntitySparkBomb extends EntityBomb {
MagicDamage.causeIndirectMagicDamage(this, this.getThrower(), DamageType.SHOCK),
Spells.spark_bomb.getProperty(Spell.SPLASH_DAMAGE).floatValue() * damageMultiplier);
}else{
ParticleBuilder.create(Type.LIGHTNING).pos(this.getPositionVector()).target(target).spawn(world);
ParticleBuilder.spawnShockParticles(world, target.posX, target.posY + target.height/2, target.posZ);
secondaryTargetIDs.add(target.getEntityId());
}
}
// Notify clients to play the explosion effect, including lightning arcs to secondary targets
int[] idArray = secondaryTargetIDs.stream().mapToInt(Integer::intValue).toArray();
WizardryPacketHandler.net.sendToAllAround(
new PacketBombExplosion.Message(PacketBombExplosion.SPARK_BOMB, posX, posY + height / 2, posZ,
blastMultiplier, idArray),
new NetworkRegistry.TargetPoint(world.provider.getDimension(), posX, posY, posZ, 64));
this.playSound(WizardrySounds.ENTITY_SPARK_BOMB_HIT_BLOCK, 0.5f, 0.5f);
}
this.setDead();
@@ -1,13 +1,20 @@
package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.constants.Tier;
import electroblob.wizardry.registry.WizardryTabs;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.EnumRarity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.NonNullList;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@@ -51,6 +58,20 @@ public class ItemArcaneTome extends Item {
return EnumRarity.COMMON;
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@SideOnly(Side.CLIENT)
@Override
public void addInformation(ItemStack stack, World world, List<String> tooltip, net.minecraft.client.util.ITooltipFlag showAdvanced){
@@ -1,6 +1,7 @@
package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.constants.Constants;
import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.registry.Spells;
@@ -8,12 +9,18 @@ import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.registry.WizardryTabs;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.SpellProperties;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
public class ItemBlankScroll extends Item implements IWorkbenchItem {
@@ -67,4 +74,18 @@ public class ItemBlankScroll extends Item implements IWorkbenchItem {
return false;
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
}
@@ -1,19 +1,23 @@
package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.event.DiscoverSpellEvent;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.registry.WizardryTabs;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.InventoryUtils;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.EnumRarity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
@@ -47,6 +51,20 @@ public class ItemIdentificationScroll extends Item {
Wizardry.proxy.addMultiLineDescription(tooltip, "item." + this.getRegistryName() + ".desc");
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@Override
public ActionResult<ItemStack> onItemRightClick(World world, EntityPlayer player, EnumHand hand){
@@ -1,6 +1,7 @@
package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
@@ -9,6 +10,7 @@ import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.WizardryTabs;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.SpellModifiers;
import net.minecraft.block.state.IBlockState;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -17,8 +19,10 @@ import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.NonNullList;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextFormatting;
@@ -122,6 +126,20 @@ public class ItemScroll extends Item implements ISpellCastingItem, IWorkbenchIte
return CASTING_TIME;
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@Override
public ActionResult<ItemStack> onItemRightClick(World world, EntityPlayer player, EnumHand hand){
@@ -160,12 +178,21 @@ public class ItemScroll extends Item implements ISpellCastingItem, IWorkbenchIte
Spell spell = Spell.byMetadata(stack.getItemDamage());
// By default, scrolls have no modifiers - but with the event system, they could be added.
SpellModifiers modifiers = new SpellModifiers();
SpellModifiers modifiers;
if(WizardData.get(player) != null){
modifiers = WizardData.get(player).itemCastingModifiers;
}else{
modifiers = new SpellModifiers();
}
int castingTick = stack.getMaxItemUseDuration() - count;
// Continuous spells (these must check if they can be cast each tick since the mana changes)
// In theory the spell is always continuous here but just in case it isn't...
if(spell.isContinuous && canCast(stack, spell, player, player.getActiveHand(), castingTick, modifiers)){
// Do not check canCast() on tick 0 as it is already done in onItemRightClick() and would duplicate modifiers
if(spell.isContinuous && (castingTick == 0 || canCast(stack, spell, player, player.getActiveHand(), castingTick, modifiers))){
cast(stack, spell, player, player.getActiveHand(), castingTick, modifiers);
}else{
// Scrolls normally work on the max use duration so this isn't ever reached by wizardry, but if the
@@ -1,6 +1,7 @@
package electroblob.wizardry.item;
import com.google.common.collect.ImmutableMap;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.WizardryGuiHandler;
import electroblob.wizardry.constants.Tier;
@@ -14,9 +15,12 @@ import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.block.state.IBlockState;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.world.World;
@@ -58,6 +62,21 @@ public class ItemSpellBook extends Item {
}
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
// Minecraft skips onBlockActivated when sneaking with a non-empty hand, so we handle it here
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@Override
public ActionResult<ItemStack> onItemRightClick(World world, EntityPlayer player, EnumHand hand){
ItemStack stack = player.getHeldItem(hand);
@@ -1,10 +1,17 @@
package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.registry.WizardryTabs;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.EnumRarity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
@@ -24,6 +31,20 @@ public class ItemWandUpgrade extends Item {
return EnumRarity.UNCOMMON;
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@Override
@SideOnly(Side.CLIENT)
public void addInformation(ItemStack stack, @Nullable World world, List<String> tooltip, net.minecraft.client.util.ITooltipFlag flag) {
@@ -2,13 +2,17 @@ package electroblob.wizardry.item;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.WizardryGuiHandler;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.registry.WizardryTabs;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import net.minecraft.util.EnumActionResult;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
@@ -34,6 +38,20 @@ public class ItemWizardHandbook extends Item {
new Style().setColor(TextFormatting.GRAY), AUTHOR));
}
@Override
public EnumActionResult onItemUse(EntityPlayer player, World world, BlockPos pos, EnumHand hand,
EnumFacing facing, float hitX, float hitY, float hitZ){
if(player.isSneaking()){
IBlockState state = world.getBlockState(pos);
if(state.getBlock() instanceof BlockBookshelf){
if(state.getBlock().onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ)){
return EnumActionResult.SUCCESS;
}
}
}
return EnumActionResult.PASS;
}
@Override
public ActionResult<ItemStack> onItemRightClick(World world, EntityPlayer player, EnumHand hand){
ItemStack stack = player.getHeldItem(hand);
@@ -78,7 +78,7 @@ public abstract class Forfeit {
public Forfeit(ResourceLocation name){
this.name = name;
this.sound = WizardrySounds.createSound("forfeit." + name.getPath());
this.sound = WizardrySounds.createSound(name.getNamespace(), "forfeit." + name.getPath());
}
public abstract void apply(World world, EntityPlayer player);
@@ -0,0 +1,79 @@
package electroblob.wizardry.packet;
import electroblob.wizardry.Wizardry;
import io.netty.buffer.ByteBuf;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
/**
* <b>[Server -> Client]</b> This packet is sent when a bomb entity impacts something. It is used to guarantee that the
* client always sees the explosion effect, even in singleplayer where the server may remove the entity before the
* client's own simulation can call {@code onImpact()}.
*/
public class PacketBombExplosion implements IMessageHandler<PacketBombExplosion.Message, IMessage> {
/** Bomb type constants, matching the order bombs are handled in {@link electroblob.wizardry.CommonProxy}. */
public static final int FIREBOMB = 0;
public static final int POISON_BOMB = 1;
public static final int SMOKE_BOMB = 2;
public static final int SPARK_BOMB = 3;
@Override
public IMessage onMessage(Message message, MessageContext ctx){
if(ctx.side.isClient()){
net.minecraft.client.Minecraft.getMinecraft().addScheduledTask(
() -> Wizardry.proxy.handleBombExplosionPacket(message));
}
return null;
}
public static class Message implements IMessage {
/** One of the bomb type constants defined in {@link PacketBombExplosion}. */
public int bombType;
public double x, y, z;
public float blastMultiplier;
/** Entity IDs of secondary targets struck by the spark bomb chain. Empty for other bomb types. */
public int[] secondaryTargetIDs;
// Required no-arg constructor
public Message(){}
public Message(int bombType, double x, double y, double z, float blastMultiplier){
this(bombType, x, y, z, blastMultiplier, new int[0]);
}
public Message(int bombType, double x, double y, double z, float blastMultiplier, int[] secondaryTargetIDs){
this.bombType = bombType;
this.x = x;
this.y = y;
this.z = z;
this.blastMultiplier = blastMultiplier;
this.secondaryTargetIDs = secondaryTargetIDs;
}
@Override
public void fromBytes(ByteBuf buf){
bombType = buf.readInt();
x = buf.readDouble();
y = buf.readDouble();
z = buf.readDouble();
blastMultiplier = buf.readFloat();
int count = buf.readInt();
secondaryTargetIDs = new int[count];
for(int i = 0; i < count; i++) secondaryTargetIDs[i] = buf.readInt();
}
@Override
public void toBytes(ByteBuf buf){
buf.writeInt(bombType);
buf.writeDouble(x);
buf.writeDouble(y);
buf.writeDouble(z);
buf.writeFloat(blastMultiplier);
buf.writeInt(secondaryTargetIDs.length);
for(int id : secondaryTargetIDs) buf.writeInt(id);
}
}
}
@@ -35,6 +35,7 @@ public class WizardryPacketHandler {
registerMessage(PacketSpellQuickAccess.class, PacketSpellQuickAccess.Message.class);
registerMessage(PacketRequestDonationPerks.class, PacketRequestDonationPerks.Message.class);
registerMessage(PacketSyncDonationPerks.class, PacketSyncDonationPerks.Message.class);
registerMessage(PacketBombExplosion.class, PacketBombExplosion.Message.class);
}
private static int nextPacketId = 0;
@@ -27,7 +27,9 @@ public class BlindingFlash extends SpellAreaEffect {
if(EntityUtils.isLiving(target)){
int duration = (int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade));
target.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, duration, 0));
if(!world.isRemote){
target.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, duration, 0));
}
}
return true;
@@ -27,9 +27,11 @@ public class Containment extends SpellRay {
protected boolean onEntityHit(World world, Entity target, Vec3d hit, EntityLivingBase caster, Vec3d origin, int ticksInUse, SpellModifiers modifiers){
if(EntityUtils.isLiving(target)){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.containment,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.containment,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
}
}
return true;
@@ -28,12 +28,14 @@ public class CurseOfEnfeeblement extends SpellRay {
if(EntityUtils.isLiving(target)){
// This will actually run out in the end, but only if you leave Minecraft running for 3.4 years
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_enfeeblement,
Integer.MAX_VALUE, getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
// Reduce the target's health to its new max health if necessary
if(((EntityLivingBase)target).getHealth() > ((EntityLivingBase)target).getMaxHealth()){
target.attackEntityFrom(MagicDamage.causeDirectMagicDamage(caster, MagicDamage.DamageType.WITHER),
((EntityLivingBase)target).getHealth() - ((EntityLivingBase)target).getMaxHealth());
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_enfeeblement,
Integer.MAX_VALUE, getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
// Reduce the target's health to its new max health if necessary
if(((EntityLivingBase)target).getHealth() > ((EntityLivingBase)target).getMaxHealth()){
target.attackEntityFrom(MagicDamage.causeDirectMagicDamage(caster, MagicDamage.DamageType.WITHER),
((EntityLivingBase)target).getHealth() - ((EntityLivingBase)target).getMaxHealth());
}
}
}
@@ -55,14 +55,16 @@ public class CurseOfSoulbinding extends SpellRay {
protected boolean onEntityHit(World world, Entity target, Vec3d hit, EntityLivingBase caster, Vec3d origin, int ticksInUse, SpellModifiers modifiers){
if(EntityUtils.isLiving(target) && caster instanceof EntityPlayer){
WizardData data = WizardData.get((EntityPlayer)caster);
if(data != null){
// Return false if soulbinding failed (e.g. if the target is already soulbound)
if(getSoulboundCreatures(data).add(target.getUniqueID())){
// This will actually run out in the end, but only if you leave Minecraft running for 3.4 years
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_soulbinding, Integer.MAX_VALUE));
}else{
return false;
if(!world.isRemote){
WizardData data = WizardData.get((EntityPlayer)caster);
if(data != null){
// Return false if soulbinding failed (e.g. if the target is already soulbound)
if(getSoulboundCreatures(data).add(target.getUniqueID())){
// This will actually run out in the end, but only if you leave Minecraft running for 3.4 years
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_soulbinding, Integer.MAX_VALUE));
}else{
return false;
}
}
}
}
@@ -28,8 +28,10 @@ public class CurseOfUndeath extends SpellRay {
if(EntityUtils.isLiving(target)){
// This will actually run out in the end, but only if you leave Minecraft running for 3.4 years
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_undeath, Integer.MAX_VALUE,
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.curse_of_undeath, Integer.MAX_VALUE,
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
}
}
return true;
@@ -42,9 +42,11 @@ public class EmpoweringPresence extends SpellAreaEffect {
int bonusAmplifier = SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY));
target.addPotionEffect(new PotionEffect(WizardryPotions.empowerment,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
if(!world.isRemote){
target.addPotionEffect(new PotionEffect(WizardryPotions.empowerment,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
}
}
return true;
@@ -74,4 +76,4 @@ public class EmpoweringPresence extends SpellAreaEffect {
}
}
}
}
@@ -33,7 +33,7 @@ public class FontOfMana extends SpellAreaEffect {
@Override
protected boolean affectEntity(World world, Vec3d origin, @Nullable EntityLivingBase caster, EntityLivingBase target, int targetCount, int ticksInUse, SpellModifiers modifiers){
if(target instanceof EntityPlayer){ // Font of mana is only useful to players
if(target instanceof EntityPlayer && !world.isRemote){ // Font of mana is only useful to players
target.addPotionEffect(new PotionEffect(WizardryPotions.font_of_mana,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
(int)(getProperty(EFFECT_STRENGTH).intValue() + (modifiers.get(SpellModifiers.POTENCY) - 1) * 2)));
@@ -58,4 +58,4 @@ public class FontOfMana extends SpellAreaEffect {
/ (2 + event.getCaster().getActivePotionEffect(WizardryPotions.font_of_mana).getAmplifier()), false);
}
}
}
}
@@ -38,9 +38,11 @@ public class ForestsCurse extends SpellAreaEffect {
int duration = (int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade));
int amplifier = (int)(getProperty(EFFECT_STRENGTH).floatValue() + bonusAmplifier);
target.addPotionEffect(new PotionEffect(MobEffects.POISON, duration, amplifier));
target.addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, duration, amplifier));
target.addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, duration, amplifier));
if(!world.isRemote){
target.addPotionEffect(new PotionEffect(MobEffects.POISON, duration, amplifier));
target.addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, duration, amplifier));
target.addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, duration, amplifier));
}
}
return true;
@@ -42,9 +42,11 @@ public class Freeze extends SpellRay {
if(!world.isRemote && caster instanceof EntityPlayer) ((EntityPlayer)caster).sendStatusMessage(
new TextComponentTranslation("spell.resist", target.getName(), this.getNameForTranslationFormatted()), true);
}else{
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.frost,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue()));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.frost,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue()));
}
}
if(target.isBurning()) target.extinguish();
@@ -61,9 +61,11 @@ public class FrostRay extends SpellRay {
// with this mechanic for their own purposes, so this line makes sure that doesn't affect wizardry.
}else{
// For frost ray the entity can move slightly, unlike freeze
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.frost,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue()));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.frost,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue()));
}
if(ticksInUse % 10 == 0){
float damage = getProperty(DAMAGE).floatValue() * modifiers.get(SpellModifiers.POTENCY);
@@ -60,7 +60,7 @@ public class IceAge extends SpellAreaEffect {
caster, (int)(getProperty(FREEZE_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){
target.playSound(WizardrySounds.MISC_FREEZE, 1.0F, world.rand.nextFloat() * 0.4F + 0.8F);
}
}else if(target instanceof EntityPlayer){
}else if(target instanceof EntityPlayer && !world.isRemote){
target.addPotionEffect(new PotionEffect(WizardryPotions.frost,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue()));
@@ -52,12 +52,14 @@ public class Intimidate extends SpellAreaEffect {
int bonusAmplifier = SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY));
NBTTagCompound entityNBT = target.getEntityData();
if(entityNBT != null) entityNBT.setUniqueId(NBT_KEY, caster.getUniqueID());
if(!world.isRemote){
NBTTagCompound entityNBT = target.getEntityData();
if(entityNBT != null) entityNBT.setUniqueId(NBT_KEY, caster.getUniqueID());
target.addPotionEffect(new PotionEffect(WizardryPotions.fear,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
target.addPotionEffect(new PotionEffect(WizardryPotions.fear,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
}
}
return true;
@@ -29,9 +29,11 @@ public class InvigoratingPresence extends SpellAreaEffect {
int bonusAmplifier = SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY));
target.addPotionEffect(new PotionEffect(MobEffects.STRENGTH,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
if(!world.isRemote){
target.addPotionEffect(new PotionEffect(MobEffects.STRENGTH,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + bonusAmplifier));
}
return true;
}
@@ -32,7 +32,7 @@ public class MarkSacrifice extends SpellRay {
@Override
protected boolean onEntityHit(World world, Entity target, Vec3d hit, EntityLivingBase caster, Vec3d origin, int ticksInUse, SpellModifiers modifiers){
if(EntityUtils.isLiving(target)){
if(EntityUtils.isLiving(target) && !world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.mark_of_sacrifice,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
@@ -120,8 +120,10 @@ public class MindControl extends SpellRay {
}
public static void startControlling(EntityLiving target, EntityLivingBase controller, int duration){
target.getEntityData().setUniqueId(NBT_KEY, controller.getUniqueID());
target.addPotionEffect(new PotionEffect(WizardryPotions.mind_control, duration, 0));
if(!target.world.isRemote){
target.getEntityData().setUniqueId(NBT_KEY, controller.getUniqueID());
target.addPotionEffect(new PotionEffect(WizardryPotions.mind_control, duration, 0));
}
}
/**
@@ -238,4 +240,4 @@ public class MindControl extends SpellRay {
}
}
}
}
@@ -64,8 +64,10 @@ public class Paralysis extends SpellRay {
}
float durationMultiplier = target instanceof EntityPlayer ? modifiers.get(PLAYER_EFFECT_DURATION_MULTIPLIER) : 1.0f;
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.paralysis,
(int)(getProperty(EFFECT_DURATION).floatValue() * durationMultiplier * modifiers.get(WizardryItems.duration_upgrade)), 0));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(WizardryPotions.paralysis,
(int)(getProperty(EFFECT_DURATION).floatValue() * durationMultiplier * modifiers.get(WizardryItems.duration_upgrade)), 0));
}
}
return false;
@@ -34,9 +34,11 @@ public class PlagueOfDarkness extends SpellAreaEffect {
if(!MagicDamage.isEntityImmune(DamageType.WITHER, target)){
target.attackEntityFrom(MagicDamage.causeDirectMagicDamage(caster, DamageType.WITHER),
getProperty(DAMAGE).floatValue() * modifiers.get(SpellModifiers.POTENCY));
target.addPotionEffect(new PotionEffect(MobEffects.WITHER,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
if(!world.isRemote){
target.addPotionEffect(new PotionEffect(MobEffects.WITHER,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
}
}
return true;
@@ -45,9 +45,11 @@ public class Poison extends SpellRay {
}else{
target.attackEntityFrom(MagicDamage.causeDirectMagicDamage(caster, DamageType.POISON),
getProperty(DAMAGE).floatValue() * modifiers.get(SpellModifiers.POTENCY));
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.POISON,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.POISON,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
}
}
}
@@ -330,9 +330,11 @@ public class Possession extends SpellRay {
victim.setPosition(player.posX, player.posY, player.posZ);
if(!player.world.isRemote) player.world.spawnEntity(victim);
for(PotionEffect effect : player.getActivePotionEffects()){
if(effect.getPotion() instanceof PotionSlowTime) continue; // Don't transfer slow time
victim.addPotionEffect(effect);
if(!player.world.isRemote){
for(PotionEffect effect : player.getActivePotionEffects()){
if(effect.getPotion() instanceof PotionSlowTime) continue; // Don't transfer slow time
victim.addPotionEffect(effect);
}
}
}
@@ -69,8 +69,10 @@ public class RayOfPurification extends SpellRay {
EntityUtils.attackEntityWithoutKnockback(target,
MagicDamage.causeDirectMagicDamage(caster, DamageType.RADIANT), damage);
// Blindness
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.BLINDNESS,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade))));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.BLINDNESS,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade))));
}
}
}
@@ -41,8 +41,10 @@ public class Shield extends Spell {
@Override
public boolean cast(World world, EntityPlayer caster, EnumHand hand, int ticksInUse, SpellModifiers modifiers){
caster.addPotionEffect(new PotionEffect(MobEffects.RESISTANCE, 10,
getProperty(EFFECT_STRENGTH).intValue(), false, false));
if(!world.isRemote){
caster.addPotionEffect(new PotionEffect(MobEffects.RESISTANCE, 10,
getProperty(EFFECT_STRENGTH).intValue(), false, false));
}
if(WizardData.get(caster).getVariable(SHIELD_KEY) == null){
@@ -35,9 +35,11 @@ public class SixthSense extends Spell {
@Override
public boolean cast(World world, EntityPlayer caster, EnumHand hand, int ticksInUse, SpellModifiers modifiers){
caster.addPotionEffect(new PotionEffect(WizardryPotions.sixth_sense,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
(int)((modifiers.get(WizardryItems.range_upgrade) - 1f) / Constants.RANGE_INCREASE_PER_LEVEL)));
if(!world.isRemote){
caster.addPotionEffect(new PotionEffect(WizardryPotions.sixth_sense,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
(int)((modifiers.get(WizardryItems.range_upgrade) - 1f) / Constants.RANGE_INCREASE_PER_LEVEL)));
}
if(world.isRemote){
Wizardry.proxy.loadShader(caster, SHADER);
@@ -907,7 +907,7 @@ public abstract class Spell extends IForgeRegistryEntry.Impl<Spell> implements C
* its JSON file using a resource pack. If called with no arguments, defaults to any context, i.e. only returns
* false if the spell is completely disabled in all contexts. */
public final boolean isEnabled(SpellProperties.Context... contexts){
return enabled && (contexts.length == 0 || properties.isEnabled(contexts));
return enabled && (contexts.length == 0 || (arePropertiesInitialised() && properties.isEnabled(contexts)));
}
/** Sets whether the spell is enabled or not. */
@@ -168,6 +168,8 @@ public class SpellBuff extends Spell {
* Returns a boolean to allow subclasses to cause the spell to fail if for some reason the effect cannot be applied
* (for example, {@link Heal} fails if the caster is on full health). */
protected boolean applyEffects(EntityLivingBase caster, SpellModifiers modifiers){
if(caster.world.isRemote) return true;
// This will generate 0 for novice and apprentice, and 1 for advanced and master
// TODO: Once we've found a way of detecting if amplifiers actually affect the potion type, implement it here.
int bonusAmplifier = getBonusAmplifier(modifiers.get(SpellModifiers.POTENCY));
@@ -157,8 +157,10 @@ public class Transportation extends Spell {
if(BlockTransportationStone.testForCircle(world, destination)){
this.playSound(world, player, 0, -1, modifiers);
player.addPotionEffect(new PotionEffect(MobEffects.NAUSEA, 150, 0));
data.setVariable(COUNTDOWN_KEY, getProperty(TELEPORT_COUNTDOWN).intValue());
if(!world.isRemote){
player.addPotionEffect(new PotionEffect(MobEffects.NAUSEA, 150, 0));
data.setVariable(COUNTDOWN_KEY, getProperty(TELEPORT_COUNTDOWN).intValue());
}
return true;
}else{
if(!world.isRemote) player.sendStatusMessage(new TextComponentTranslation("spell." + this.getUnlocalisedName() + ".missing"), true);
@@ -39,9 +39,11 @@ public class Wither extends SpellRay {
}else{
target.attackEntityFrom(MagicDamage.causeDirectMagicDamage(caster, DamageType.WITHER),
getProperty(DAMAGE).floatValue() * modifiers.get(SpellModifiers.POTENCY));
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.WITHER,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
if(!world.isRemote){
((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.WITHER,
(int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)),
getProperty(EFFECT_STRENGTH).intValue() + SpellBuff.getStandardBonusAmplifier(modifiers.get(SpellModifiers.POTENCY))));
}
}
}
@@ -235,12 +235,11 @@ public class TileEntityImbuementAltar extends TileEntity implements ITickable {
*/
public static ItemStack getImbuementResult(ItemStack input, Element[] receptacleElements, boolean fullLootGen, World world, EntityPlayer lastUser){
ItemStack eventResult = ItemStack.EMPTY;
ImbuementActivateEvent event = new ImbuementActivateEvent(input, receptacleElements, world, lastUser, ItemStack.EMPTY);
if (world != null && MinecraftForge.EVENT_BUS.post(new ImbuementActivateEvent(input, receptacleElements, world, lastUser, eventResult))) {
// return the stack if something changes the result from an empty stack.
//noinspection ConstantConditions
if (eventResult != ItemStack.EMPTY) return eventResult;
if (world != null && MinecraftForge.EVENT_BUS.post(event)) {
// Event was cancelled; return whatever result the handler set (may be empty to suppress all recipes).
return event.result;
}
if(input.getItem() instanceof ItemWizardArmour && ((ItemWizardArmour)input.getItem()).element == null){
@@ -12,16 +12,105 @@ import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiPredicate;
/**
* Contains some useful static methods for interacting with the ally designation system. Also handles the friendly fire
* setting. This was split off from {@code WizardryUtilities} as of wizardry 4.2 in an effort to make the code easier to
* navigate.
* <p></p>
* <b>Addon API:</b> External mods can extend the ally system without modifying wizardry by registering predicates via
* {@link #registerValidTargetPredicate(BiPredicate)} and {@link #registerAllyPredicate(BiPredicate)}. Predicates are
* registered once at mod initialisation and called on every ally check with the live entity references, so any
* dynamic ownership model (NBT, capabilities, in-memory maps) is supported. Predicates run after all built-in
* wizardry logic and are additive they can protect additional entity pairs but cannot remove existing protections
* such as direct ownership or scoreboard teams.
*/
@Mod.EventBusSubscriber
public final class AllyDesignationSystem {
private AllyDesignationSystem(){} // No instances!
/**
* Predicates registered by external mods for {@link #isValidTarget}. Using {@link CopyOnWriteArrayList} so that
* registration from any thread during mod initialisation is safe, while iteration on the server tick thread is
* lock-free.
*/
private static final List<BiPredicate<Entity, Entity>> validTargetPredicates = new CopyOnWriteArrayList<>();
/**
* Predicates registered by external mods for {@link #isAllied}. See {@link #validTargetPredicates}.
*/
private static final List<BiPredicate<EntityLivingBase, EntityLivingBase>> allyPredicates = new CopyOnWriteArrayList<>();
/**
* Registers a predicate that extends the ally check performed by {@link #isValidTarget(Entity, Entity)}.
* <p></p>
* Call this method <b>once</b> during your mod's initialisation (e.g. {@code FMLInitializationEvent}). The
* predicate is stored statically and invoked on every {@code isValidTarget} call, so it must be cheap avoid
* world queries or heavy computation inside the lambda.
* <p></p>
* <b>Predicate semantics:</b> return {@code true} to declare that {@code target} should be treated as an ally of
* {@code attacker} and therefore must not be attacked. Return {@code false} to defer to other registered
* predicates or the default result.
* <p></p>
* <b>Scope:</b> Predicates run after all built-in wizardry rules (direct ownership, scoreboard teams, player ally
* list, mind-control, etc.) and only when none of those rules already produced a result. They are therefore
* additive they can protect additional pairs but cannot override existing protections.
* <p></p>
* <b>Recursion note:</b> {@code isValidTarget} recurses automatically for owned attackers (e.g. a summoned zombie
* delegates to its owner). Predicates are only invoked at the root of that recursion (i.e. when the attacker is
* not itself an {@link IEntityOwnable}), so each logical query triggers the predicate exactly once regardless of
* ownership chain depth.
* <p></p>
* <b>Example</b> protecting minions of allied players in an external mod that tracks ownership via a capability:
* <pre>{@code
* AllyDesignationSystem.registerValidTargetPredicate((attacker, target) -> {
* UUID summonerOfTarget = MyMod.getOwnerUUID(target); // reads your capability / NBT
* if (summonerOfTarget == null) return false; // not one of our mobs
* UUID summonerOfAttacker = MyMod.getOwnerUUID(attacker);
* if (summonerOfAttacker == null) return false;
* // Protect if both are owned by the same player, or by players who are wizardry allies
* if (summonerOfTarget.equals(summonerOfAttacker)) return true;
* EntityPlayer ownerA = ...; EntityPlayer ownerB = ...;
* return AllyDesignationSystem.isPlayerAlly(ownerA, ownerB);
* });
* }</pre>
*
* @param predicate A {@link BiPredicate} receiving {@code (attacker, target)}. Must be non-null and thread-safe.
*/
public static void registerValidTargetPredicate(BiPredicate<Entity, Entity> predicate){
validTargetPredicates.add(predicate);
}
/**
* Registers a predicate that extends the ally check performed by {@link #isAllied(EntityLivingBase, EntityLivingBase)}.
* <p></p>
* Call this method <b>once</b> during your mod's initialisation (e.g. {@code FMLInitializationEvent}). The
* predicate is stored statically and invoked on every {@code isAllied} call.
* <p></p>
* <b>Predicate semantics:</b> return {@code true} to declare that {@code possibleAlly} should be considered allied
* with {@code allyOf}. Returning {@code true} affects any system that calls {@code isAllied}, including healing
* AoE spells, {@code EntityRadiantTotem}, and the friendly-fire event handler. Return {@code false} to defer.
* <p></p>
* <b>Scope:</b> Same additive-only guarantee as {@link #registerValidTargetPredicate} predicates run after all
* built-in rules and cannot remove existing ally relationships.
* <p></p>
* <b>Example</b> treating faction members as allies for healing spells:
* <pre>{@code
* AllyDesignationSystem.registerAllyPredicate((allyOf, possibleAlly) ->
* MyFactionMod.sameFaction(allyOf, possibleAlly)
* );
* }</pre>
*
* @param predicate A {@link BiPredicate} receiving {@code (allyOf, possibleAlly)}. Must be non-null and thread-safe.
*/
public static void registerAllyPredicate(BiPredicate<EntityLivingBase, EntityLivingBase> predicate){
allyPredicates.add(predicate);
}
/** Set of constants for each of the four friendly fire settings. */
public enum FriendlyFire {
@@ -117,6 +206,9 @@ public final class AllyDesignationSystem {
// Always return true if the attacker is null - this must be after the target null check!
if(attacker == null) return true;
// Teammates are allies
if(attacker.isOnSameTeam(target)) return false;
// Tests whether the target is the attacker
if(target == attacker) return false;
@@ -194,6 +286,15 @@ public final class AllyDesignationSystem {
}
}
// Addon predicate registry only evaluated at the root of the ownership recursion (i.e. when the attacker is
// not itself an IEntityOwnable). Owned attackers already delegate upward via the recursive call at the top of
// this method, so skipping them here ensures each logical query fires the predicates exactly once.
if(!validTargetPredicates.isEmpty() && !(attacker instanceof IEntityOwnable)){
for(BiPredicate<Entity, Entity> predicate : validTargetPredicates){
if(predicate.test(attacker, target)) return false;
}
}
return true;
}
@@ -203,6 +304,10 @@ public final class AllyDesignationSystem {
* generally used to determine targets for healing or other group buffs. */
public static boolean isAllied(EntityLivingBase allyOf, EntityLivingBase possibleAlly){
if (allyOf == null || possibleAlly == null) {
return false;
}
// Owned entities inherit their owner's allies
if(allyOf instanceof IEntityOwnable){
Entity owner = ((IEntityOwnable)allyOf).getOwner();
@@ -220,6 +325,10 @@ public final class AllyDesignationSystem {
if(allyOf instanceof EntityPlayer && isOwnerAlly((EntityPlayer)allyOf, pet)) return true;
}
if(possibleAlly.isOnSameTeam(allyOf)) {
return true;
}
// Check if the possibleAlly is mind controlled by the allyOf entity
if(possibleAlly instanceof EntityLiving && possibleAlly.isPotionActive(WizardryPotions.mind_control)){
NBTTagCompound entityNBT = possibleAlly.getEntityData();
@@ -230,6 +339,13 @@ public final class AllyDesignationSystem {
}
}
// Addon predicate registry runs after all built-in rules; additive only.
if(!allyPredicates.isEmpty()){
for(BiPredicate<EntityLivingBase, EntityLivingBase> predicate : allyPredicates){
if(predicate.test(allyOf, possibleAlly)) return true;
}
}
return false;
}
@@ -247,7 +363,10 @@ public final class AllyDesignationSystem {
WizardData data = WizardData.get(allyOf);
if(data == null) return false;
Entity owner = ownable.getOwner();
return owner instanceof EntityPlayer ? data.isPlayerAlly((EntityPlayer)owner) : data.isPlayerAlly(ownable.getOwnerId());
if(owner == null) return data.isPlayerAlly(ownable.getOwnerId()); // offline owner
if(owner instanceof EntityPlayer) return data.isPlayerAlly((EntityPlayer)owner);
if(owner instanceof IEntityOwnable) return isOwnerAlly(allyOf, (IEntityOwnable)owner); // recurse
return false;
}
@SubscribeEvent
@@ -524,6 +524,7 @@ item.ebwizardry\:charm_move_speed.name=Icarus Medallion
item.ebwizardry\:charm_move_speed.desc=Move at near-full speed when casting spells
item.ebwizardry\:charm_spell_discovery.name=Archivist's Eyeglass
item.ebwizardry\:charm_spell_discovery.desc=Spell books found in chests are much more likely to be ones you have not yet discovered
item.ebwizardry\:charm_spell_discovery.desc.color=When equipped, you can see the element of a known spell book by the color of its bookmark
item.ebwizardry\:charm_auto_smelt.name=Metallurgist's Mark
item.ebwizardry\:charm_auto_smelt.desc=Pocket furnace triggers automatically when bound to a wand on your hotbar
item.ebwizardry\:charm_lava_walking.name=Nether Ice Core
@@ -1692,6 +1693,10 @@ config.ebwizardry.replace_vanilla_fall_damage=Replace Vanilla Fall Damage
config.ebwizardry.replace_vanilla_fall_damage.tooltip=Whether to replace Minecraft's distance-based fall damage calculation with an equivalent, velocity-based one. This is done such that mobs in freefall will take exactly the same damage as normal, so it will not break falling-based mob farms. Disable this if you experience falling-related weirdness! If this is disabled, some spells revert to a more simplistic method of resetting the player's fall damage in certain cases.
config.ebwizardry.replace_vanilla_fall_damage.true=Yes - it's parkour time
config.ebwizardry.replace_vanilla_fall_damage.false=No - break my legs normally
config.ebwizardry.spell_book_colors_require_charm=Spell Book Colors Require Archivist's Eyeglass
config.ebwizardry.spell_book_colors_require_charm.tooltip=Whether to require the Archivist's Eyeglass charm artifact to see colored spell book bookmarks.
config.ebwizardry.spell_book_colors_require_charm.true=Yes - I want to see the colours only if the charm is worn
config.ebwizardry.spell_book_colors_require_charm.false=No - I want to see the colours all the time
config.ebwizardry.blindness_tweak=Blindness Tweak
config.ebwizardry.blindness_tweak.tooltip=Whether to tweak the blindness effect to reduce follow distance when used on non-players. This automatically disables itself in favour of Potion Core's implementation if installed.
config.ebwizardry.mob_loot_table_whitelist=Mob Loot Table Whitelist
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,14 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book"
},
"parent": "ebwizardry:item/spell_book_undiscovered",
"overrides": [
{
"predicate": {
"festive": 1
},
"model": "ebwizardry:item/festive_spell_book"
}
{"predicate": {"ebwizardry:element": 1}, "model": "ebwizardry:item/spell_book_discovered_magic"},
{"predicate": {"ebwizardry:element": 2}, "model": "ebwizardry:item/spell_book_discovered_fire"},
{"predicate": {"ebwizardry:element": 3}, "model": "ebwizardry:item/spell_book_discovered_ice"},
{"predicate": {"ebwizardry:element": 4}, "model": "ebwizardry:item/spell_book_discovered_lightning"},
{"predicate": {"ebwizardry:element": 5}, "model": "ebwizardry:item/spell_book_discovered_necromancy"},
{"predicate": {"ebwizardry:element": 6}, "model": "ebwizardry:item/spell_book_discovered_earth"},
{"predicate": {"ebwizardry:element": 7}, "model": "ebwizardry:item/spell_book_discovered_sorcery"},
{"predicate": {"ebwizardry:element": 8}, "model": "ebwizardry:item/spell_book_discovered_healing"},
{"predicate": {"festive": 1}, "model": "ebwizardry:item/festive_spell_book"}
]
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_earth"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_fire"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_healing"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_ice"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_lightning"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_magic"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_necromancy"
}
}
@@ -0,0 +1,7 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book",
"layer1": "ebwizardry:items/spell_book_overlay_sorcery"
}
}
@@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "ebwizardry:items/spell_book"
}
}
@@ -0,0 +1,865 @@
{
"bookmark_start_section": "introduction",
"colours": {
"text": "#000000",
"caption": "#666666",
"hyperlink": "#601ba0",
"highlight": "#dd4c1d",
"new_section": "#ee88f7"
},
"images": {
"workbench": {
"location": "ebwizardry:textures/gui/handbook/pictures/arcane_workbench.png",
"caption": "Чарівний верстат",
"u": 0,
"v": 0,
"width": 110,
"height": 110
},
"crystal_ore": {
"location": "ebwizardry:textures/gui/handbook/pictures/crystal_ore.png",
"caption": "Кришталева руда",
"u": 0,
"v": 0,
"width": 64,
"height": 64
},
"magic_crystal": {
"location": "ebwizardry:textures/items/crystal_magic.png",
"caption": "Чарівний кристал",
"u": 6,
"v": 6,
"width": 36,
"height": 33,
"texture_width": 48,
"texture_height": 48,
"border": false
},
"magic_wand": {
"location": "ebwizardry:textures/items/wand_novice.png",
"caption": "Чарівна паличка",
"u": 0,
"v": 0,
"width": 64,
"height": 64,
"border": false
},
"crystal_flower": {
"location": "ebwizardry:textures/gui/handbook/pictures/crystal_flower.png",
"caption": "Кришталева квітка",
"u": 0,
"v": 0,
"width": 48,
"height": 48
},
"wizard_tower": {
"location": "ebwizardry:textures/gui/handbook/pictures/wizard_tower.png",
"caption": "Вежа чаклуна",
"u": 0,
"v": 0,
"width": 110,
"height": 110
},
"wizard_armour": {
"location": "ebwizardry:textures/gui/handbook/pictures/wizard_armour.png",
"caption": "Набір чаклунської броні",
"u": 0,
"v": 0,
"width": 90,
"height": 90
},
"classes": {
"location": "ebwizardry:textures/gui/handbook/pictures/classes.png",
"caption": "три класи",
"u": 0,
"v": 0,
"width": 96,
"height": 32,
"border": false
},
"obelisk": {
"location": "ebwizardry:textures/gui/handbook/pictures/obelisk.png",
"caption": "обеліск",
"u": 0,
"v": 0,
"width": 110,
"height": 67
},
"shrine": {
"location": "ebwizardry:textures/gui/handbook/pictures/shrine.png",
"caption": "святилище землі",
"u": 0,
"v": 0,
"width": 110,
"height": 67
},
"library_ruins": {
"location": "ebwizardry:textures/gui/handbook/pictures/library_ruins.png",
"caption": "руїни бібліотеки",
"u": 0,
"v": 0,
"width": 110,
"height": 57
},
"remnant": {
"location": "ebwizardry:textures/gui/handbook/pictures/remnant.png",
"caption": "Вогняний залишок",
"u": 0,
"v": 0,
"width": 110,
"height": 73
},
"tiers": {
"location": "ebwizardry:textures/gui/handbook/pictures/tiers.png",
"caption": "4 рівні жезлів ",
"u": 0,
"v": 0,
"width": 96,
"height": 24,
"border": false
},
"elements": {
"location": "ebwizardry:textures/gui/handbook/pictures/elements.png",
"caption": "7 арканічних елементів",
"u": 0,
"v": 0,
"width": 106,
"height": 20,
"border": false
}
},
"recipes": {
"arcane_workbench": {
"locations": [ "ebwizardry:arcane_workbench" ]
},
"magic_wand": {
"locations": [ "ebwizardry:magic_wand" ]
},
"novice_wands": {
"locations": [
"ebwizardry:magic_wand",
"ebwizardry:wand_fire",
"ebwizardry:wand_ice",
"ebwizardry:wand_lightning",
"ebwizardry:wand_necromancy",
"ebwizardry:wand_earth",
"ebwizardry:wand_sorcery",
"ebwizardry:wand_healing"
]
},
"magic_missile_spell_book": {
"locations": [ "ebwizardry:magic_missile_spell_book" ]
},
"wizard_handbook": {
"locations": [ "ebwizardry:wizard_handbook" ]
},
"crystal_flower_to_crystals": {
"locations": [ "ebwizardry:crystal_flower_to_crystals" ]
},
"crystal_blocks": {
"locations": [
"ebwizardry:crystal_block",
"ebwizardry:crystal_block_fire",
"ebwizardry:crystal_block_ice",
"ebwizardry:crystal_block_lightning",
"ebwizardry:crystal_block_necromancy",
"ebwizardry:crystal_block_earth",
"ebwizardry:crystal_block_sorcery",
"ebwizardry:crystal_block_healing"
]
},
"crystal_blocks_to_crystals": {
"locations": [
"ebwizardry:crystal_block_to_crystals",
"ebwizardry:crystal_block_to_crystals_fire",
"ebwizardry:crystal_block_to_crystals_ice",
"ebwizardry:crystal_block_to_crystals_lightning",
"ebwizardry:crystal_block_to_crystals_necromancy",
"ebwizardry:crystal_block_to_crystals_earth",
"ebwizardry:crystal_block_to_crystals_sorcery",
"ebwizardry:crystal_block_to_crystals_healing"
]
},
"crystal_shard": {
"locations": [ "ebwizardry:crystal_shard" ]
},
"small_mana_flask": {
"locations": [ "ebwizardry:small_mana_flask" ]
},
"medium_mana_flask": {
"locations": [ "ebwizardry:medium_mana_flask" ]
},
"large_mana_flask": {
"locations": [ "ebwizardry:large_mana_flask" ]
},
"runestone": {
"locations": [
"ebwizardry:runestone_fire",
"ebwizardry:runestone_ice",
"ebwizardry:runestone_lightning",
"ebwizardry:runestone_necromancy",
"ebwizardry:runestone_earth",
"ebwizardry:runestone_sorcery",
"ebwizardry:runestone_healing"
]
},
"runestone_pedestal": {
"locations": [
"ebwizardry:runestone_pedestal_fire",
"ebwizardry:runestone_pedestal_ice",
"ebwizardry:runestone_pedestal_lightning",
"ebwizardry:runestone_pedestal_necromancy",
"ebwizardry:runestone_pedestal_earth",
"ebwizardry:runestone_pedestal_sorcery",
"ebwizardry:runestone_pedestal_healing"
]
},
"transportation_stone": {
"locations": [ "ebwizardry:transportation_stone" ]
},
"magic_silk": {
"locations": [ "ebwizardry:magic_silk" ]
},
"wizard_hat": {
"locations": [ "ebwizardry:wizard_hat" ]
},
"wizard_robe": {
"locations": [ "ebwizardry:wizard_robe" ]
},
"wizard_leggings": {
"locations": [ "ebwizardry:wizard_leggings" ]
},
"wizard_boots": {
"locations": [ "ebwizardry:wizard_boots" ]
},
"blank_scroll": {
"locations": [ "ebwizardry:blank_scroll" ]
},
"firebomb": {
"locations": [ "ebwizardry:firebomb" ]
},
"poison_bomb": {
"locations": [ "ebwizardry:poison_bomb" ]
},
"smoke_bomb": {
"locations": [ "ebwizardry:smoke_bomb" ]
},
"spark_bomb": {
"locations": [ "ebwizardry:spark_bomb" ]
},
"gilded_wood": {
"locations": [
"ebwizardry:gilded_oak_wood",
"ebwizardry:gilded_spruce_wood",
"ebwizardry:gilded_birch_wood",
"ebwizardry:gilded_jungle_wood",
"ebwizardry:gilded_acacia_wood",
"ebwizardry:gilded_dark_oak_wood"
]
},
"bookshelves": {
"locations": [
"ebwizardry:oak_bookshelf",
"ebwizardry:spruce_bookshelf",
"ebwizardry:birch_bookshelf",
"ebwizardry:jungle_bookshelf",
"ebwizardry:acacia_bookshelf",
"ebwizardry:dark_oak_bookshelf"
]
},
"lecterns": {
"locations": [
"ebwizardry:oak_lectern",
"ebwizardry:spruce_lectern",
"ebwizardry:birch_lectern",
"ebwizardry:jungle_lectern",
"ebwizardry:acacia_lectern",
"ebwizardry:dark_oak_lectern"
]
},
"receptacle": {
"locations": [ "ebwizardry:receptacle" ]
}
},
"sections": {
"inside_cover": {
"centre": {
"x": true,
"y": true
},
"text": [
"Підручник чарівника",
"За видавництвом Electroblob"
]
},
"introduction": {
"title": "Introduction",
"text": [
"Вітаю, чарівники! Ця книга пояснює численні шляхи володіння магією та способи її використання. Проте це не звичайна книга — її сторінки з’являтимуться перед вами по мірі того, як ви відкриватимете для себе чарівний світ.",
"Використовуйте стрілки, щоб переходити від сторінки до сторінки, а кнопки з подвійними стрілками — щоб швидко переходити між розділами. Використовуйте центральну кнопку меню, щоб повернутися на головну сторінку змісту.",
"Натисніть на будь-який фіолетовий текст-посилання, щоб одразу перейти на відповідну сторінку. Натисніть праву кнопку миші на закладці, щоб перемістити її на поточну сторінку, і ліву кнопку — щоб повернутися до закладеної сторінки."
]
},
"main_contents": {
"title": "Contents",
"contents": {
"id": "main_contents",
"hyperlinks": true,
"page_numbers": true,
"separator": "."
}
},
"setting_up": {
"title": "налаштування",
"include_in_contents": "main_contents",
"text": [
"Задля того що б розпочати свій шлях становлення чарівником вам знадобится:",
"- Створення чарівної палички:",
"#recipe magic_wand",
"- An @arcane_workbench Чарівний верстат@, створюєтся так:",
"#recipe arcane_workbench",
"- A @spells spell@ book. Їх можна знайти в скринях, отримати як здобич, купити у чарівників, або створити початкове заклинання — «Магічну ракету», як показано:",
"#recipe magic_missile_spell_book",
"також вам знадобится чимало магічних кристалів що б у вашої палични не закінчилась @mana@."
]
},
"mana": {
"title": "Mana",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:crystal"
],
"text": [
"Мана — це таємнича енергія, що надає чарівникам їхні здібності. Вона не має власної фізичної форми; скоріше, це всюдисуща аура, яка пронизує весь світ. Особливо вона проявляється у природних кристалах, що зустрічаються в надрах скель. @wands@ здатні зберігати ману, і саме з них вона спрямовується у @spells@, які ви накладаєте. Кожне заклинання потребує різної кількості мани — залежно від його сили та тривалості дії.",
"#image crystal_ore",
"#image magic_crystal",
"Загальноприйнято, що ману неможливо створити чи знищити, і що коли накладається заклинання, мана, яку воно використовує, просто розсіюється у навколишньому світі. Насправді саме так існує більшість мани — тонким шаром, розподіленим по всьому світу. Проте, щоб вона стала корисною, її потрібно сконцентрувати: природним шляхом протягом тисячоліть, як у випадку з підземними кристалами, або штучно — складна тема, яка не розглядається у цій книзі. Існують також різні способи зробити заклинання більш економними щодо мани та відновлювати частину мани, що розсіюється під час накладання заклинань."
]
},
"wands": {
"title": "Wands",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:arcane_initiate"
],
"text": [
"Чарівна паличка — головний інструмент чарівника. За її допомогою ви можете накладати будь-яке @spells@, за умови що паличка здатна вмістити його силу (див. @tiers@). Існує багато різновидів чарівних паличок, але майже напевно ви почнете з базової магічної палички.",
"#image magic_wand",
"Більшість чарівних паличок починають своє існування як просте поєднання певного магічного кристала, прикріпленого до дерев’яної палиці, з золотим самородком на іншому кінці. Кристал, безперечно, є джерелом сили палички, адже саме він забезпечує фокус, необхідний для спрямування @spells@. Проте решта палички також має значення: вона не лише дозволяє зручно тримати її в руці, а й слугує шляхом, яким @mana@ може проходити, спрямовуватися та набувати форми.",
"У міру того як за допомогою палички накладається дедалі більше заклинань, вона стає ефективнішою у спрямуванні @spells@ і, відповідно, здатною використовувати заклинання більшої потужності. Про це свідчать ледь помітні зміни у формі та зовнішньому вигляді самої палички: зі зростанням її сили кристал стає яскравішим, його колір може змінюватися залежно від @elements@, а з часом деревина, з якої зроблено паличку, починає рости й закручуватися у дедалі складніші форми.",
"Коли ви тримаєте чарівну паличку, в куті екрана з’являється невеликий інтерфейс. Він показує поточне вибране заклинання разом із його піктограмою, а також — якщо заклинання було застосоване — смугу перезарядки, що відображає час до можливості повторного використання. Також відображаються назви наступного та попереднього заклинань, прив’язаних до палички.\nЩоб перемикатися між заклинаннями, використовуйте клавіші #next_spell_key та #previous_spell_key (їх можна змінити в меню «Налаштування → Керування»). Крім того, ви можете перемикати заклинання, прокручуючи коліщатко миші під час присідання.",
"Під час перегляду інвентарю наведення курсора на чарівну паличку показує, скільки @mana@ у ній збережено, а також поточне вибране @spells заклинання@ і будь-які особливі здібності, які вона може мати. Більш детальну інформацію про паличку можна переглянути, розмістивши її на @arcane_workbench чарівному верстаку@."
]
},
"spells": {
"title": "Spells",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:handbook/spells"
],
"text": [
"A @wands wand@ марні без заклинань, які можна з їх допомогою накладати. Заклинання зберігаються у двох формах: у книгах, які є постійними, та у сувоях, що мають тимчасову дію.",
"Книги заклинань самі по собі мають небагато користі; натомість їх використовують, щоб прив’язати заклинання, яке вони містять, до @wands@. Книги заклинань можна знайти по всьому світу, і вам не доведеться довго шукати, щоб натрапити на одну з них. Коли ви знайдете таку книгу, наведіть на неї курсор, щоб з першого погляду побачити основну інформацію про заклинання. Ви також можете клацнути правою кнопкою миші, тримаючи книгу заклинань, щоб прочитати детальніший опис.",
"Сувої, навпаки, виконують іншу функцію: клацання правою кнопкою миші, тримаючи сувій, накладає заклинання, прив’язане до нього, при цьому сувій знищується. Як і книги заклинань, сувої можна знайти по всьому світу, але їх також можна створювати самостійно (див. @enchanting_scrolls@).",
"Сувої дуже корисні, коли потрібно використати заклинання один-два рази для певного завдання, але не хочеться займати ним слот у вашій @wands@. Вони також мають перевагу в тому, що дозволяють миттєво накладати заклинання, які зазвичай потребують часу на зарядку. Слід зауважити, однак, що порівняно з використанням палички, сувої є менш ефективним способом накладання заклинань.",
"Для непідготовленого ока магічні руни, якими написані заклинання, є нерозбірливими. Щоб ідентифікувати заклинання, можна, звісно, накласти його і подивитися, що станеться — хоча це може спричинити небажані наслідки, а багато заклинань потребують певних умов для свого застосування. Крім того, завжди існує ймовірність помилкового прочитання невідомого заклинання під час його накладання, з потенційно небезпечними наслідками залежно від сили заклинання. Більш безпечним і надійним способом є використання сувою ідентифікації, який можна знайти в скринях або придбати у чарівника. Клацання правою кнопкою миші, тримаючи такий сувій, ідентифікує першу невідому книгу заклинань або сувій на вашій панелі швидкого доступу, при цьому сувій ідентифікації витрачається."
]
},
"arcane_workbench": {
"title": "Arcane Workbench",
"include_in_contents": "main_contents",
"contents": {
"id": "arcane_workbench_subsections",
"hyperlinks": true,
"page_numbers": true,
"separator": "."
},
"triggers": [
"ebwizardry:handbook/arcane_workbench"
],
"text": [
"Чарівний верстат — це блок, зовні схожий на стіл чарувань, на якому можна заряджати, покращувати та прив’язувати @spells@ до ваших @wands@. Його можна створити за таким рецептом:",
"#recipe arcane_workbench",
"Наступні сторінки пояснюють різні дії, які можна виконувати на чарівному верстаті.",
"#image workbench"
],
"sections": {
"binding_spells": {
"title": "Binding Spells",
"include_in_contents": "arcane_workbench_subsections",
"triggers": [
"ebwizardry:handbook/arcane_workbench"
],
"text": [
"Що б прив'язати @spells spell@ до вашої @wands палички@, просто помістіть паличку в центральний слот верстаку, потім покладіть книгу заклинань в один із навколишніх слотів і натисніть «Застосувати». Ви помітите, що книга заклинань при цьому залишається цілою — це тому, що процес прив’язування заклинання не «забирає» його з книги, а налаштовує паличку на це заклинання. Таким чином, ви можете змінювати заклинання, прив’язані до вашої палички, скільки забажаєте, просто повторюючи процес прив’язування. Одночасно до палички можна прив’язати до п’яти заклинань, хоча це число можна збільшити за допомогою покращень налаштування."
]
},
"charging_wands": {
"title": "Charging Wands",
"include_in_contents": "arcane_workbench_subsections",
"triggers": [
"ebwizardry:handbook/arcane_workbench"
],
"text": [
"Що б зарядити @wands wand@, помістіть їх в центральний слот арканної верстати та покладіть кілька магічних кристалів у нижній лівий слот, після чого натисніть «Застосувати». Кожен кристал дає #mana_per_crystal @mana@, і паличка візьме лише стільки, скільки їй потрібно, тому ви можете тримати запас кристалів у верстаті для подальшого використання. Проте пам’ятайте, що паличка може взяти лише ціле число кристалів, тому якщо, наприклад, паличці потрібно ще 30 одиниць мани, #example_charging_loss мани буде втрачено при заряджанні."
]
},
"upgrading_wands": {
"title": "Upgrading Wands",
"include_in_contents": "arcane_workbench_subsections",
"triggers": [
"ebwizardry:handbook/tome_of_arcana"
],
"text": [
"Що б покращити @wands wand@, вам знадобиться том аркани відповідного @tiers@ або спеціальне покращення для палички. Обидва ці предмети можна знайти в здобичі або купити у чарівника. Щоб покращити паличку, помістіть її в центральний слот, а покращення — у верхній правий слот, після чого натисніть «Застосувати».",
"Спеціальні покращення для палички покращують певний аспект палички. Кожен тип покращення можна застосувати до трьох разів, а загальна кількість покращень, які можна встановити на паличку, залежить від її рівня. Покращення не можна видалити."
]
},
"enchanting_scrolls": {
"title": "Enchanting Scrolls",
"include_in_contents": "arcane_workbench_subsections",
"triggers": [
"ebwizardry:handbook/scrolls"
],
"text": [
"Таємнича робоча майстерня також може використовуватися для чарування @spells spell@ scrolls. Для цього вам знадобиться чистий сувій, створений з паперу та нитки, книга заклинань для обраного заклинання та достатня кількість магічних кристалів, щоб надати @mana@ для його використання.",
"#recipe blank_scroll",
"Помістіть кристали в нижній лівий слот, чистий сувій у центральний слот, а книгу заклинань у верхній слот над ним, потім натисніть «Підтвердити», щоб зачарувати сувій."
]
}
}
},
"wizard_armour": {
"title": "Wizard Armor",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:arcane_initiate"
],
"text": [
"Як магу, вам знадобиться щось, що захистить вас від створінь, з якими ви боретеся. Звичайна броня не підійде. Щоб максимально використовувати свої заклинання, вам знадобиться магічна броня: капелюх, ризи, штани та чоботи. На відміну від звичайної броні, вона не ламається. Замість цього вона використовує магічну енергію, щоб захищати носія. Це, звісно, означає, що її потрібно заряджати магічними кристалами, так само як і чарівну паличку. На щастя, ці предмети не витрачають багато @mana@. Заряджайте їх так само, як паличку, у @arcane_workbench арканічній майстерні@ або за допомогою @mana_flasks@, але будьте обережні – якщо у них закінчиться мана, ви залишитесь беззахисними.",
"#image wizard_armour",
"Магічну броню можна створити з магічного шовку:",
"#recipe magic_silk",
"#recipe wizard_hat",
"#recipe wizard_robe",
"#recipe wizard_leggings",
"#recipe wizard_boots",
"Магічну броню можна зачарувати за допомогою звичайного стола зачарувань. Через особливі властивості магічного шовку, вона добре утримує чари. Особливо корисними для чарівника є зачарування «Захист від магії», «Захист від холоду» та «Захист від електрики», які допомагають оборонятися від інших магів.",
"Чарівники, що присвячують себе практиці одного елемента, іноді носять особливий одяг, який надає бонуси для цього елемента. Повні комплекти такого одягу дуже цінуються серед чарівників і знайти їх не так просто.",
"Опановуючи основи магії, багато чарівників обирають певний шлях або клас, щоб продовжити своє навчання. Існує три основні класи, до яких чарівник може належати: мудрець, бойовий маг і чорнокнижник. Членів кожного класу легко впізнати за їхніми ризами, які модифіковані — зазвичай із рідкісних та екзотичних матеріалів — щоб краще відповідати їхнім потребам.",
"#image classes",
"Мудрець",
"Присвятивши життя пошуку знань, мудрець вдосконалив мистецтво ефективного накладання заклять, що дозволяє йому використовувати мінімальну кількість мани. Вишуканість його шовкових риз приховує значний захист, адже мудрець також є вправним чарівником — хоча він все ще не рівняється з міцною металевою бронею. Проте тривале перебування у безпеці бібліотеки робить його дещо повільним у бою.",
"Бойовий маг",
"Бойовий маг готовий до бою, одягнений у поєднання міцної тканини та кристально-срібної броні, створеної найвмілішими майстрами. Завдяки дисципліні та тренуванням, бойовий маг також покращив швидкість і реакцію, що дозволяє йому частіше накладати закляття. Проте це не обходиться дешево, і бойовий маг має бути обережним, щоб не витратити ману занадто швидко.",
"Чорнокнижник",
"Чорнокнижник живе для досліджень, постійно розширюючи межі можливого, щоб віднайти забуті таємниці магії. Це, разом із готовністю експериментувати з тим, що інші вважають забороненим, дозволяє їм порушувати обмеження традиційного навчання, накладати закляття з вражаючою швидкістю та зменшувати витрати мани. Їхні легкі шатра, хоч і зручні для досліджень, забезпечують менший захист у бою порівняно з іншими магами."
]
},
"magical_world": {
"title": "Magic in the World",
"include_in_contents": "main_contents",
"contents": {
"id": "magical_world_subsections",
"hyperlinks": true,
"page_numbers": true,
"separator": "."
},
"triggers": [
"ebwizardry:handbook/on_subsection_unlock"
],
"text": [
"Не можна опанувати магію, лишаючись вдома — світ повен чарів, які чекають на дослідження! Стародавні руїни, реліквії та загадкові істоти, добрі і злі, чекають на відкриття — якщо знати, де шукати. Найвеличніші чарівники також є найбільш допитливими і здобувають багато своїх знань та сили через дослідження оточення та експерименти з тим, що вони знаходять.",
"Хоча більшу частину часу чарівник проводить наодинці, обмін знаннями також є життєво важливим для опанування магії. Шукай інших практиків магії та навчайся від них якнайбільше: спілкування, можливо, є найпотужнішою силою з усіх."
],
"sections": {
"crystal_flowers": {
"title": "Crystal Flowers",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:handbook/crystal_flowers"
],
"text": [
"Під час своїх мандрів ти, ймовірно, час від часу натрапиш на дивовижні осяйні квіти, що ростуть у дикій природі. Ці характерні рослини відомі як кристалічні квіти, і вони дивовижно ефективні в концентрації @mana@ — досі ніхто точно не знає, чому. Ми, однак, знаємо, що їх можна збирати та переробляти для отримання кристалів мани, що робить їх доволі корисним джерелом мани у верхньому світі. Проте кількість мани, яку можна отримати таким способом, обмежена через невеликий розмір квітів та через те, що вони ростуть лише невеликими групами.",
"#image crystal_flower",
"#recipe crystal_flower_to_crystals"
]
},
"wizard_towers": {
"title": "Wizard Towers",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:wizard_tower"
],
"text": [
"Не бійтеся — ви не єдиний досліджувач магії в цьому світі. Під час своїх мандрів ви, ймовірно, натрапите на одну чи дві високі вежі з характерним гострим дахом. Це оселя іншого мага. Життя у самоті іноді робить чарівників трохи дратівливими, проте зазвичай вони дружні й готові поділитися своїми знаннями з вами — хоча й за певну ціну. Очікуйте, що доведеться заплатити дорогоцінними металами та камінням, а натомість ви отримаєте безліч чудових таємничих винаходів.",
"Якщо ж ви шукаєте закляття рівня @tiers майстер@, вам доведеться звернутися до спеціаліста.",
"#image wizard_tower",
"На жаль, у цьому світі трапляються й чарівники, які не раді гостям. Зазвичай це вигнанці, ворожі до кожного, хто стає на їхньому шляху. Наближаючись до таких осіб, будьте обережні та будьте готові захищатися від могутньої магії. Проте здолавши їх, ви зможете заволодіти їхніми знаннями.",
"Пам’ятайте також: жоден чарівник не стерпить нападу чи злодійства. У таких випадках вони без роздумів самі вершитимуть правосуддя."
]
},
"obelisks": {
"title": "Обеліски",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:handbook/obelisks"
],
"text": [
"Сліди давньої магії розкидані по всьому світу, і найпомітнішими з них є висічені з каменю споруди, вкриті безліччю символів і рун. Ці руїни — усе, що залишилося від, імовірно, якоїсь давньої цивілізації. Хто б або що б їх не створив, вони безсумнівно володіли значними знаннями магії та використали їх, щоб накласти на ці місця захисні чари, які зберігаються й донині.",
"Ці споруди загалом поділяються на два типи. Перший, і найпоширеніший з них, — це обеліски: високі шпилі з різьбленого каменю, відомого як рунний камінь, з відкритою нижньою частиною, у якій зберігаються незначні арканні реліквії забутого минулого.",
"#image obelisk",
"Ці споруди зазвичай захищені чарами, що призивають ворожих @creatures магічних істот@, якщо будь-яка людина підійде надто близько. Відбийте атаку цих істот і знищіть джерело їхньої сили — і реліквії стануть вашими."
]
},
"shrines": {
"title": "Святилища",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:visit_shrine"
],
"text": [
"Другий, рідкісніший, тип споруд відомий як святилище. Ці споруди складаються з кола стовпів із рунного каменю, що оточують центральний п’єдестал, на якому стоїть скриня, наповнена древніми @artefacts артефактами@. Ця скриня зазвичай захищена чарами арканного замка, які не дозволяють нікому, окрім власника, відкрити її. Уся споруда також перебуває під дією поля утримання, яке не дозволяє нічому, що підійшло надто близько, вибратися назовні.",
"#image shrine",
"Через свою величезну арканну силу й значущість, не кажучи вже про скарби всередині, ці споруди особливо приваблюють будь-якого початківця-чарівника — але будьте обережні. Існує чимало повідомлень про чарівників, які опинялися замкненими всередині полів утримання й поступово божеволіли, можливо, через клаустрофобію. Втім, дедалі більше людей вірять, що такі випадки є навмисною частиною захисної магії святилища, а чарівники, які опинилися в пастці, насправді перебувають під контролем і змушені охороняти споруду. Воістину моторошна перспектива для кожного, хто наважиться підійти надто близько..."
]
},
"library_ruins": {
"title": "Руїни бібліотеки",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:find_library_ruins"
],
"text": [
"Чарівники зазвичай накопичують чималу кількість книг заклять протягом свого життя, тож для них цілком звично будувати бібліотеку, аби зберігати свою колекцію. Деякі особливо потайні чарівники навіть зберігали свої арканні знання під землею, подалі від сторонніх очей.\nОднак бібліотека потребує догляду, і якщо її власник помирає, не залишивши учня, який успадкує книги, споруда швидко занепадає. Тисячі років використання магії призвели до того, що дедалі більше таких бібліотек залишилися покинутими, і під час мандрівок вам цілком може трапитися одна з них. Їх часто варто обшукати в надії знайти книги заклять або інші предмети, які змогли пережити руйнівний плин часу..",
"#image library_ruins",
"Бібліотеки використовуються не лише для зберігання книг. Чарівники минулого використовували силу жертовника для імбуювання — проведення ритуалів елементального насичення магічних матеріалів. Розташування жертовника — це точне й складне мистецтво, оскільки він повинен бути вирівняний відповідно до певних небесних рухів і місцевих коливань мани.\n\nУ сучасності практика будівництва таких жертовників стала значно рідкіснішою, адже міцний кам’яний жертовник служить століттями, і потреби у нових майже немає. Натомість більшість чарівників знаходять і відновлюють існуючі жертовники, коли потрібно виконати ритуали імбуювання — а руїни бібліотек є ідеальним місцем для таких пошуків.",
"Ви також колись можете вважати за корисне збудувати власну бібліотеку, для чого, звісно, знадобляться книжкові полиці:",
"#recipe bookshelves",
"#recipe gilded_wood",
"Ці книжкові полиці — це не просто меблі. Якщо ввести в них найпотужніші магічні кристали, вони дозволяють легко передавати знання між собою та найближчим @arcane_workbench арканічним верстаком@ — дуже зручно, коли ваша колекція зростає. Не менш корисним є й кафедра:",
"#recipe lecterns",
"Том, який левітує над кафедрою, збирає знання з навколишніх книжок зі закляттями, дозволяючи переглядати їх у зручний час, не запам’ятовуючи точне розташування кожної."
]
},
"creatures": {
"title": "Чарівні істоти",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:handbook/magical_creatures"
],
"text": [
"Окрім чарівників, численні інші істоти теж використовують магію. Багато з них мають арканне походження, і більшість можна викликати за допомогою певних магічних @spells заклять@. Ці істоти можуть охороняти @obelisks обеліск@ або служити потужному заклиначеві. Менші арканні створіння іноді зустрічаються навіть у дикій природі.",
"Вартий уваги приклад — залишок (remnant), духоподібна істота, що утворюється природно з відлуння потужної магії і зазвичай набирає одну з семи арканічних @elements елементів@. Поодинці вони майже не становлять загрози, але зграя може швидко подолати непідготовленого мандрівника, а їхня вроджена стійкість до магії робить бажаним використання лука або іншої зброї дальньої дії.",
"#image remnant",
"Once slain, their spectral dust can be collected, which, besides emitting a colorful glow, has a variety of useful properties. A receptacle, crafted as shown, provides a convenient means of containing it.",
"#recipe receptacle",
"Після того, як залишок (remnant) буде знищений, можна зібрати його спектральний пил (spectral dust), який, крім яскравого світіння, володіє різноманітними корисними властивостями. Зручним способом його зберігання є спеціальний резервуар, створений за відповідним рецептом."
]
},
"artefacts": {
"title": "Артефакти",
"include_in_contents": "magical_world_subsections",
"triggers": [
"ebwizardry:artefact"
],
"text": [
"Під час дослідження @shrines святилища@ вам може пощастити знайти стародавній магічний артефакт — предмет, здатний надавати його власникові унікальні та потужні бонуси й спеціальні здібності. Відомі три типи таких артефактів: кільця, які посилюють заклинання та додають ефекти, амулети, що покращують захисні здібності, і обереги, що надають корисні утилітарні ефекти. Ці артефакти діють лише тоді, коли вони правильно одягнені; просто носіння їх при собі недостатньо.."
]
}
}
},
"tiers": {
"title": "Tiers",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:handbook/tome_of_arcana"
],
"text":[
"@wands Жезли@ та @spells чари@ Існує чотири рівні: #colour_noviceпочатківець#colour_reset, #colour_apprenticeучень#colour_reset, #colour_advancedпідмайстер #colour_resetand #colour_masterмайстер#colour_reset. кожен рівень потужніший за попередній.",
"#image tiers",
"#colour_noviceПочаткові #colour_resetжезли Це ті, що можна створити. Вони вміщують до #novice_max_charge мани і можуть використовувати лише закляття рівня новачка. Закляття новачка досить прості, щоб їх міг застосувати будь-хто, і зазвичай вони не витрачають багато мани. Це не означає, що вони марні на вищих рівнях; їхня низька вартість робить деякі закляття новачка корисними навіть тоді, коли у вас є доступ до заклять майстерського рівня.",
"#colour_apprenticeУчнівські #colour_resetжезли Наступний рівень після жезлів новачка. Вони вміщують до #apprentice_max_charge мани і можуть використовувати закляття рівня новачка та учня. Закляття учня трохи потужніші за закляття новачка, але зазвичай коштують більше мани..",
"#colour_advancedПідмайстерські #colour_resetжезли Досить рідкісні та значно потужніші. Вони можуть використовувати всі закляття, крім майстерських, і вміщують #advanced_max_charge мани. Закляття рівня «підмайстер» можуть надавати надлюдські здібності, такі як невидимість, і завдавати серйозної шкоди ворогам.",
"#colour_resetжезли #colour_masterМайстрів #colour_resetНаймогутніші чарівні палички, що існують. Вони вміщують до #master_max_charge мани та можуть використовувати будь-яке закляття. Книги заклять #colour_masterмайстерського #colour_resetрівня надзвичайно рідкісні, а їхні закляття здатні завдати повного спустошення не лише ворогам, а й самому світу. Використовуйте обережно.",
"Щоб паличка могла бути підвищена до просунутішого ступеню, вона спершу повинна стати достатньо сильною, щоб проводити закляття цього рівня. Палички набувають сили, коли з їхньою допомогою використовують закляття. Різноманітність заклять, їхня сила, ефекти стихій та інші зовнішні чинники можуть впливати на те, як швидко паличка досягає повної зрілості.",
"Як тільки паличка стане достатньо сильною, том аркани надає останній каталізатор, необхідний для підвищення палички до наступного рівня (див. @upgrading_wands Підвищення рівня жезлів@), і після цього процес дозрівання може початися знову для наступного рівня."
]
},
"elements": {
"title": "Елементи",
"include_in_contents": "main_contents",
"triggers": [
"ebwizardry:handbook/elements"
],
"text": [
"@spells чарам@ притаманні різні елементи, які визначають природу закляття і також надають бонуси при використанні з певними @wands жезлами@.",
"#image elements",
"#colour_fireВогонь#colour_reset \nМожливо, найруйнівніший елемент — вогонь, який пов’язаний із жаром, лавою та вибухами. Потужний піромант може випустити справжнє пекло на своїх ворогів і, ймовірно, спалити навколишній світ у процесі. Більшість заклять вогню підпалюють ціль, завдаючи поступової шкоди з часом. Проте остерігайтеся: закляття вогню можуть не мати ефекту на істот із Пекла.",
"#colour_iceЛід#colour_reset \nЕлемент льоду складається зі всього холодного. Закляття морозу часто сповільнюють ворогів або повністю їх заморожують, і вони особливо ефективні проти вогняних істот. Магія морозу також може бути корисною поза боєм, наприклад, для заморожування води, щоб перейти через річку.",
"#colour_lightningБлискавка#colour_reset \nЦей елемент пов’язаний із блискавкою, бурями та погодою. Потужний маг бурі є силою, з якою варто рахуватися, і деякі здатні викликати блискавку на вимогу. Закляття блискавки часто завдають шкоди кільком ворогам одночасно та зазвичай самостійно шукають свою ціль, що робить їх ефективною атакою проти будь-яких істот… але остерігайтеся кріперів.",
"#colour_necromancyНекромантія#colour_reset \nНекромантія — це елемент темряви, хаосу та нежиті. Некроманти загадкові і часто вважаються злими, хоча насправді це трапляється рідко. Закляття некромантії зазвичай використовуються для виклику @creatures сутностей@, щоб вони боролися на вашому боці, або навіть для підкорення волі ваших ворогів.",
"#colour_earthЗемля#colour_reset \nЕлемент Землі зосереджений на природному світі: тваринах, рослинах, вітрі та подібному. Магія Землі дуже різноманітна і може проявлятися у багатьох формах — від отруєння ворогів до вивільнення гніву погоди. Закляття Землі поєднують атаку, захист і корисні ефекти.",
"#colour_sorcerySorcery#colour_reset \nМістика це елемент сили та змін. Чарівники маніпулюють світлом, гравітацією і навіть самою реальністю, щоб відповідати своїм потребам. Містичні чари можуть використовуватися, зокрема, для надання магічних сил своєму творцю або переміщення предметів за своєю волею.",
"#colour_healingЗцілення#colour_reset \nЕлемент лікування стосується захисту та відновлення. Цілителі прагнуть захищати себе та своїх союзників якомога ефективніше, і тому вони можуть бути дуже складними противниками. Хоча зазвичай закляття лікування не використовуються для атаки, очищаюче світло деяких лікувальних заклять завдає значної шкоди нежиті. Закляття лікування є незамінними в бою.",
"Межі між різними елементами не завжди чіткі, і деякі закляття мають властивості інших елементів, відмінних від їхнього власного.",
"Якщо вам пощастить, ви можете натрапити на елементальний кристал. Елементальні кристали — це магічні кристали, які поглинули властивості певного елемента під час свого зростання, хоча точні умови та механізм цього явища залишаються загадкою. Відомо лише, що такі кристали можна використовувати для створення елементарних жезлів, які дозволяють накладати закляття відповідного елемента з більшою силою, ніж зазвичай."
]
},
"miscellaneous": {
"title": "Miscellaneous",
"include_in_contents": "main_contents",
"contents": {
"id": "miscellaneous_subsections",
"hyperlinks": true,
"page_numbers": true,
"separator": "."
},
"triggers": [
"ebwizardry:on_subsection_unlock"
],
"sections": {
"mana_flasks": {
"title": "Mana Flasks",
"include_in_contents": "miscellaneous_subsections",
"triggers": [
"ebwizardry:handbook/mana_flasks"
],
"text": [
"Аркендурова Компанія Магічних Принад – магічні предмети на будь-який смак!",
"Потрібно підзарядити @wands жезл@ на ходу? Та нема питань! @mana Мана@ тепер може бути закоркована. Просто створіть Ману у пляшці з скляної пляшки та восьми магічних кристалів і візьміть її з собою. Коли вам знадобиться її використати, просто застосуйте її разом зі своєю паличкою, і вона відновить частину її заряду. *",
"#recipe medium_mana_flask",
"НОВИНА! Представляємо абсолютно нові малі та великі пляшки з маною – тепер ви можете обрати розмір пляшки відповідно до своїх потреб!",
"#recipe small_mana_flask",
"#recipe large_mana_flask",
"* Цей процес зруйнує пляшку. Певна кількість мани втрачається під час розливу..."
]
},
"throwable_items": {
"title": "Throwable Items",
"include_in_contents": "miscellaneous_subsections",
"triggers": [
"ebwizardry:handbook/throwables"
],
"text": [
"Різні @spells заклинання@ створюють фізичні предмети, деякі з яких також можна виготовити безпосередньо. Вогняні бомби, отруйні бомби, іскрові бомби та димові бомби можна скрафтити:",
"#recipe firebomb",
"#recipe poison_bomb",
"#recipe smoke_bomb",
"#recipe spark_bomb"
]
},
"automated_casting": {
"title": "Automated Casting",
"include_in_contents": "miscellaneous_subsections",
"triggers": [
"ebwizardry:enchant_scroll"
],
"text": [
"Недавні експерименти показали, що можна автоматизувати використання @spells заклинань@ до певного рівня, використовуючи лише простий диспенсер. Ніхто точно не знає чому, але здається, що дивні властивості редстоуну навіть поширюються на активацію заклинальних сувоїв. Помістивши кілька сувоїв у диспенсер і подавши живлення, можна досягти потрібного ефекту...."
]
}
}
},
"crafting_recipes": {
"title": "Crafting Recipes",
"include_in_contents": "main_contents",
"text": [
"#recipe arcane_workbench",
"#recipe novice_wands",
"#recipe magic_missile_spell_book",
"#recipe wizard_handbook",
"#recipe crystal_flower_to_crystals",
"#recipe crystal_blocks",
"#recipe crystal_blocks_to_crystals",
"#recipe crystal_shard",
"#recipe small_mana_flask",
"#recipe medium_mana_flask",
"#recipe large_mana_flask",
"#recipe runestone",
"#recipe runestone_pedestal",
"#recipe transportation_stone",
"#recipe magic_silk",
"#recipe wizard_hat",
"#recipe wizard_robe",
"#recipe wizard_leggings",
"#recipe wizard_boots",
"#recipe blank_scroll",
"#recipe firebomb",
"#recipe poison_bomb",
"#recipe smoke_bomb",
"#recipe spark_bomb",
"#recipe gilded_wood",
"#recipe bookshelves",
"#recipe lecterns",
"#recipe receptacle"
]
},
"credits": {
"title": "Credits",
"include_in_contents": "main_contents",
"text": [
"Electroblob's Wizardry \nVersion #version \nFor Minecraft #mcversion",
"Розроблено, запрограмовано та оформлено Electroblob’ом",
"Дякуємо Minecraft Forge та MCP, без яких цей мод не був би можливим.",
"Також дякую спільноті моддерів Minecraft, яка завжди знаходить відповіді на мої проблеми з модами!",
"Крім того, я хотів би подякувати наступним особам за їхній внесок у мод:",
"Код:",
"- Corail31 \n- 12foo \n- Shadows-of-Fire \n- HellFirePvP \n- Tora-B \n- Avatair \n- Aeronica \n- UltraHex \n- Azim-Palmer \n- raoulvdberge \n- rafasoares \n- xinyuan-liu \n- SettingDust",
"Переклад:",
"- Spanish: MadWrist, Alsentar \n- Mexican Spanish: MadWrist \n- Russian: VilagVil, kellixon, bigenergy, MugGod2, DrHesperus \n- French: Hahdrim \n- Brazilian Portuguese: lorrampi \n- Chinese (Simplified): ZHENGLOC, dragon-evol, Hokorizero, TUsama, Determancer \n- Korean: shejery, rewi_wire, 방통, red1854th \n- Polish: Trozuu, Olej \n- German: BirdyDragon, Lemopav \n- Chinese (Traditional): chesterccj305 \n- Hungarian: Bombadil",
"Звукові ефекти:",
"- OhhWowProductions\n- fredzed\n- deleted_user_3277771\n- DiscoveryME\n- OGSoundFX\n- leosalom\n- juskiddink",
"For more information, check out the @https://github.com/Electroblob77/Wizardry/wiki wiki@.",
"Can't get enough wizardry? Join the @https://discord.gg/hs8yJP2 Discord server@ для останніх новин, обговорень та додаткових матеріалів!"
]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

+1 -1
View File
@@ -2,7 +2,7 @@
{
"modid" : "ebwizardry",
"name" : "Electroblob's Wizardry",
"version" : "4.3.15",
"version" : "4.3.18",
"mcversion" : "1.12.2",
"url" : "https://minecraft.curseforge.com/projects/electroblobs-wizardry",
"credits" : "\nDesigned, coded and textured by Electroblob.\nDiscord Moderators: FavouriteDragon, WinDanesz\nCode contributed by: Corail31, 12foo, Shadows-of-Fire, Tora-B, Avatair, Aeronica, UltraHex, Azim-Palmer, raoulvdberge, rafasoares, xinyuan-liu, SettingDust, Aralu115, WinDanesz, ZettaSword.\nTranslators: Alsentar (Spanish), MadWrist (Mexican Spanish), VilagVil, kellixon, bigenergy, MugGod2 & DrHesperus (Russian), Hahdrim & Crowller (French), lorrampi (Brazilian Portuguese), ZHENGLOC, dragon-evol, Hokorizero, TUsama & Determancer (Chinese - Simplified), shejery, rewi_wire, 방통 & red1854th (Korean), Trozuu & Olej (Polish), BirdyDragon & Lemopav (German), chesterccj305 (Chinese - Traditional), Bombadil (Hungarian).\nSound Effects: OhhWowProductions, fredzed, deleted_user_3277771, DiscoveryME, OGSoundFX, leosalom, juskiddink",