diff --git a/build.gradle b/build.gradle index 6104eeb..aa88ff0 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,16 @@ task createModInfoFile(type: JavaExec) { // Makes compiling also create mod info file classes.dependsOn("createModInfoFile") +task preAntialiasTextures(type: JavaExec) { + group "necesse" + description "Runs pre-antialiasing on all textures in the resources folder" + + classpath = files(gameDirectory + "/Necesse.jar") + + main "PreAntialiasTextures" + args "-folders", "${sourceSets.main.resources.srcDirs.stream().map {file -> file.path}.toArray().join(";")}" +} + task runClient(type: JavaExec) { group "necesse" description "Run client with current mod" diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 6cc822c..7319641 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,17 +1,10 @@ package examplemod; -import examplemod.examples.*; -import examplemod.examples.items.ExampleFoodItem; -import examplemod.examples.items.ExampleHuntIncursionMaterialItem; -import examplemod.examples.items.ExampleMaterialItem; -import examplemod.examples.items.ExamplePotionItem; -import necesse.engine.commands.CommandsManager; +import examplemod.Loaders.*; +import examplemod.examples.maps.biomes.ExampleBiome; import necesse.engine.modLoader.annotations.ModEntry; -import necesse.engine.registries.*; -import necesse.gfx.gameTexture.GameTexture; -import necesse.inventory.recipe.Ingredient; -import necesse.inventory.recipe.Recipe; -import necesse.inventory.recipe.Recipes; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; import necesse.level.maps.biomes.Biome; @ModEntry @@ -19,108 +12,39 @@ public class ExampleMod { // We define our static registered objects here, so they can be referenced elsewhere public static ExampleBiome EXAMPLE_BIOME; + public static GameSound EXAMPLESOUND; + public static SoundSettings EXAMPLESOUNDSETTINGS; public void init() { System.out.println("Hello world from my example mod!"); - // Register a simple biome that will not appear in natural world gen. - EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); - - // Register the incursion biome with tier requirement 1. - IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); - - // Register the level class used for the incursion. - LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); - - // Register our tiles - TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); - - // Register our objects - ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); - - // Register our items - ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); - ItemRegistry.registerItem("examplehuntincursionitem", new ExampleHuntIncursionMaterialItem(), 50, true); - ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); - ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); - ItemRegistry.registerItem("examplepotionitem", new ExamplePotionItem(), 10, true); - ItemRegistry.registerItem("examplefooditem", new ExampleFoodItem(),15, true); - - // Register our mob - MobRegistry.registerMob("examplemob", ExampleMob.class, true); - - // Register our projectile - ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); - - // Register our buff - BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); - - // Register our packet - PacketRegistry.registerPacket(ExamplePacket.class); + // The examples are split into different classes here for readability, but you can register them directly here in init if you wish + ExampleModCategories.load(); + ExampleModEvents.load();; + ExampleModBiomes.load(); + ExampleModIncursions.load(); + ExampleModTiles.load(); + ExampleModObjects.load(); + ExampleModItems.load(); + ExampleModMobs.load(); + ExampleModProjectiles.load(); + ExampleModBuffs.load(); + ExampleModPackets.load(); } public void initResources() { - // Sometimes your textures will have a black or other outline unintended under rotation or scaling - // This is caused by alpha blending between transparent pixels and the edge - // To fix this, run the preAntialiasTextures gradle task - // It will process your textures and save them again with a fixed alpha edge color - - ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); + ExampleModResources.load(); } public void postInit() { - // Add recipes - // Example item recipe, crafted in inventory for 2 iron bars - Recipes.registerModRecipe(new Recipe( - "exampleitem", - 1, - RecipeTechRegistry.NONE, - new Ingredient[]{ - new Ingredient("ironbar", 2) - } - ).showAfter("woodboat")); // Show recipe after wood boat recipe + // load our recipes from the ExampleRecipes class so we can keep this class easy to read + ExampleModRecipes.registerRecipes(); - // Example sword recipe, crafted in iron anvil using 4 example items and 5 copper bars - Recipes.registerModRecipe(new Recipe( - "examplesword", - 1, - RecipeTechRegistry.IRON_ANVIL, - new Ingredient[]{ - new Ingredient("exampleitem", 4), - new Ingredient("copperbar", 5) - } - )); - - // Example staff recipe, crafted in workstation using 4 example items and 10 gold bars - Recipes.registerModRecipe(new Recipe( - "examplestaff", - 1, - RecipeTechRegistry.WORKSTATION, - new Ingredient[]{ - new Ingredient("exampleitem", 4), - new Ingredient("goldbar", 10) - } - ).showAfter("exampleitem")); // Show the recipe after example item recipe - - // Example food item recipe - Recipes.registerModRecipe(new Recipe( - "examplefooditem", - 1, - RecipeTechRegistry.COOKING_POT, - new Ingredient[]{ - new Ingredient("bread", 1), - new Ingredient("strawberry", 2), - new Ingredient("sugar", 1) - } - )); // Add our example mob to default cave mobs. // Spawn tables use a ticket/weight system. In general, common mobs have about 100 tickets. Biome.defaultCaveMobs .add(100, "examplemob"); - - // Register our server chat command - CommandsManager.registerServerCommand(new ExampleChatCommand()); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModBiomes.java b/src/main/java/examplemod/Loaders/ExampleModBiomes.java new file mode 100644 index 0000000..b24b3f8 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBiomes.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.maps.biomes.ExampleBiome; +import necesse.engine.registries.BiomeRegistry; + +public class ExampleModBiomes { + public static void load() { + // Register a simple biome that will not appear in natural world gen. + ExampleMod.EXAMPLE_BIOME = BiomeRegistry.registerBiome("examplebiome", new ExampleBiome(), false); + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java new file mode 100644 index 0000000..f7a5ce7 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -0,0 +1,13 @@ +package examplemod.Loaders; + +import examplemod.examples.buffs.*; +import necesse.engine.registries.BuffRegistry; + +public class ExampleModBuffs { + public static void load(){ + // Register our buff + BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBuff()); + BuffRegistry.registerBuff("examplearrowbuff", new ExampleArrowBuff()); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java new file mode 100644 index 0000000..65dd8c0 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -0,0 +1,118 @@ +package examplemod.Loaders; + +import necesse.engine.localization.message.LocalMessage; +import necesse.inventory.item.ItemCategory; + +public final class ExampleModCategories { + private ExampleModCategories() {} + + /* + * IMPORTANT (Creative Menu requirement) + * ------------------------------------ + * The Creative menu tabs currently only browse a small set of hard-coded ROOT categories but this will be changed in the future. + * + * Placeables tab roots: tiles / objects / wiring + * Items tab roots: equipment / consumable / materials / misc + * Mobs tab roots: mobs + * + * So: your itemCategoryTree MUST start with one of those roots, otherwise the item/object + * will not appear in Creative even though it is registered. + */ + + // VANILLA PLACABLE + // ===== ROOT ===== + public static final String ROOT_TILES = "tiles"; + public static final String ROOT_OBJECTS = "objects"; + public static final String ROOT_WIRING = "wiring"; + + // ===== VANILLA: TILES children ===== + public static final String TILES_FLOORS = "floors"; + public static final String TILES_LIQUIDS = "liquids"; + public static final String TILES_TERRAIN = "terrain"; + + // ===== VANILLA: OBJECTS children ===== + public static final String OBJECTS_SEEDS = "seeds"; + public static final String OBJECTS_CRAFTINGSTATIONS = "craftingstations"; + public static final String OBJECTS_LIGHTING = "lighting"; + public static final String OBJECTS_FURNITURE = "furniture"; + public static final String OBJECTS_DECORATIONS = "decorations"; + public static final String OBJECTS_WALLSANDDOORS = "wallsanddoors"; + public static final String OBJECTS_FENCESANDGATES = "fencesandgates"; + public static final String OBJECTS_COLUMNS = "columns"; + public static final String OBJECTS_TRAPS = "traps"; + public static final String OBJECTS_LANDSCAPING = "landscaping"; + public static final String OBJECTS_MISC = "misc"; + + // ===== VANILLA: FURNITURE children ===== + public static final String FURNITURE_MISC = "misc"; + public static final String FURNITURE_OAK = "oak"; + public static final String FURNITURE_SPRUCE = "spruce"; + public static final String FURNITURE_PINE = "pine"; + public static final String FURNITURE_MAPLE = "maple"; + public static final String FURNITURE_BIRCH = "birch"; + public static final String FURNITURE_WILLOW = "willow"; + public static final String FURNITURE_DUNGEON = "dungeon"; + public static final String FURNITURE_BONE = "bone"; + public static final String FURNITURE_DRYAD = "dryad"; + public static final String FURNITURE_BAMBOO = "bamboo"; + public static final String FURNITURE_DEADWOOD = "deadwood"; + + // ===== VANILLA: DECORATIONS children ===== + public static final String DECORATIONS_PAINTINGS = "paintings"; + public static final String DECORATIONS_CARPETS = "carpets"; + public static final String DECORATIONS_POTS = "pots"; + public static final String DECORATIONS_BANNERS = "banners"; + + // ===== VANILLA: LANDSCAPING children ===== + public static final String LANDSCAPING_FORESTROCKSANDORES = "forestrocksandores"; + public static final String LANDSCAPING_SNOWROCKSANDORES = "snowrocksandores"; + public static final String LANDSCAPING_PLAINSROCKSANDORES = "plainsrocksandores"; + public static final String LANDSCAPING_SWAMPROCKSANDORES = "swamprocksandores"; + public static final String LANDSCAPING_DESERTROCKSANDORES = "desertrocksandores"; + public static final String LANDSCAPING_INCURSIONROCKSANDORES = "incursionrocksandores"; + public static final String LANDSCAPING_CRYSTALS = "crystals"; + public static final String LANDSCAPING_TABLEDECORATIONS = "tabledecorations"; + public static final String LANDSCAPING_PLANTS = "plants"; + public static final String LANDSCAPING_MASONRY = "masonry"; + public static final String LANDSCAPING_MISC = "misc"; + + // ===== VANILLA: WIRING children ===== + public static final String WIRING_LOGICGATES = "logicgates"; + + + // YOUR MOD ROOT CATEGORY + public static final String MOD = "examplemod"; + // YOUR MOD SUB CATEGORY + public static final String MOD_OBJECTS = "objects"; + + public static final String EXAMPLEWOOD = "examplewood"; + + public static void load() { + + // ITEM CATEGORIES (not Creative-visible right now, but valid categories) + ItemCategory.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectsubcat"), + MOD, MOD_OBJECTS); + + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD, MOD_OBJECTS,EXAMPLEWOOD); + + // CRAFTING CATEGORIES + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectsubcat"), + MOD,MOD_OBJECTS); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD,MOD_OBJECTS,EXAMPLEWOOD); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCommands.java b/src/main/java/examplemod/Loaders/ExampleModCommands.java new file mode 100644 index 0000000..e60f03b --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCommands.java @@ -0,0 +1,12 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleChatCommand; +import necesse.engine.commands.CommandsManager; + +public class ExampleModCommands { + public static void load(){ + + // Register our server chat command + CommandsManager.registerServerCommand(new ExampleChatCommand()); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModEvents.java b/src/main/java/examplemod/Loaders/ExampleModEvents.java new file mode 100644 index 0000000..e3a3fad --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModEvents.java @@ -0,0 +1,33 @@ +package examplemod.Loaders; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEventListener; +import necesse.engine.GameEvents; +import necesse.engine.network.server.ServerClient; +import necesse.engine.registries.LevelEventRegistry; + +public class ExampleModEvents { + public static void load() { + // Register our Level Event to the registry + LevelEventRegistry.registerEvent("examplelevelevent", ExampleLevelEvent.class); + + // Register our ExampleEvent Listener + GameEvents.addListener(ExampleEvent.class, new GameEventListener() { + @Override + public void onEvent(ExampleEvent event) { + if (event.level == null || !event.level.isServer()) return; + + ServerClient client = event.level.getServer().getClient(event.clientSlot); + if (client != null) { + client.sendChatMessage(event.message); + client.sendChatMessage("PONG: this message was sent from the ExampleEvent Listener "); + } + } + }); + + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModIncursions.java b/src/main/java/examplemod/Loaders/ExampleModIncursions.java new file mode 100644 index 0000000..6f328d7 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModIncursions.java @@ -0,0 +1,17 @@ +package examplemod.Loaders; + +import examplemod.examples.maps.incursion.ExampleIncursionBiome; +import examplemod.examples.maps.incursion.ExampleIncursionLevel; +import necesse.engine.registries.IncursionBiomeRegistry; +import necesse.engine.registries.LevelRegistry; + +public class ExampleModIncursions { + public static void load() { + + // Register the incursion biome with tier requirement 1. + IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); + + // Register the level class used for the incursion. + LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java new file mode 100644 index 0000000..2b37fda --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -0,0 +1,46 @@ +package examplemod.Loaders; + +import examplemod.examples.items.ammo.ExampleArrowItem; +import examplemod.examples.items.armor.ExampleBootsArmorItem; +import examplemod.examples.items.armor.ExampleChestArmorItem; +import examplemod.examples.items.armor.ExampleHelmetArmorItem; +import examplemod.examples.items.consumable.ExampleFoodItem; +import examplemod.examples.items.consumable.ExamplePotionItem; +import examplemod.examples.items.materials.*; +import examplemod.examples.items.tools.ExampleMagicStaffWeapon; +import examplemod.examples.items.tools.ExampleMeleeSwordWeapon; +import examplemod.examples.items.tools.ExampleRangedBowWeapon; +import examplemod.examples.items.tools.ExampleSummonOrbWeapon; +import necesse.engine.registries.ItemRegistry; + +public class ExampleModItems { + public static void load(){ + + // Materials + ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); + ItemRegistry.registerItem("examplestone", new ExampleStoneItem(), 15, true); + ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); + ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); + ItemRegistry.registerItem("examplelog", new ExampleLogItem().setItemCategory("materials","logs"),10,true); + ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(),1,true); + + // Tools + ItemRegistry.registerItem("examplemeleesword", new ExampleMeleeSwordWeapon(), 20, true); + ItemRegistry.registerItem("examplemagicstaff", new ExampleMagicStaffWeapon(), 30, true); + ItemRegistry.registerItem("examplesummonorb", new ExampleSummonOrbWeapon(),40,true); + ItemRegistry.registerItem("examplerangedbow", new ExampleRangedBowWeapon(),10,true); + + // Armor + ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200f, true); + ItemRegistry.registerItem("examplechestplate", new ExampleChestArmorItem(), 250f, true); + ItemRegistry.registerItem("exampleboots", new ExampleBootsArmorItem(), 180f, true); + + // Consumables + ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); + ItemRegistry.registerItem("examplefood", new ExampleFoodItem(), 15, true); + + // Ammo + ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(),5,true); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java new file mode 100644 index 0000000..80acd22 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -0,0 +1,19 @@ +package examplemod.Loaders; + +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; +import necesse.engine.registries.MobRegistry; + +public class ExampleModMobs { + public static void load(){ + // Register our mob + MobRegistry.registerMob("examplemob", ExampleMob.class, true); + + // Register boss mob + MobRegistry.registerMob("examplebossmob", ExampleBossMob.class,true,true); + + // Register summon mob + MobRegistry.registerMob("examplesummonmob", ExampleSummonWeaponMob.class,true,false); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java new file mode 100644 index 0000000..4d316d4 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -0,0 +1,45 @@ +package examplemod.Loaders; + +import examplemod.examples.objects.*; +import necesse.engine.registries.ObjectRegistry; + +//NOTE item and crafting categories subject to change +public class ExampleModObjects { + + public static void load(){ + // Register our objects + + ObjectRegistry.registerObject("exampleobject", new ExampleObject() + .setItemCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS) + .setCraftingCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS), 2, true); + + + // Register a rock object + ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); + ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); + + // Register an ore rock object that overlays onto our incursion rock + ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + + // Register a wall object, window object and door object + ExampleWallWindowDoorObject.registerWallsDoorsWindows(); + + // Register a tree object + ObjectRegistry.registerObject("exampletree",new ExampleTreeObject(),0.0F,false,false,false); + + // Register a sapling object + ObjectRegistry.registerObject("examplesapling", new ExampleTreeSaplingObject(),10,true); + + // Register a furnature object this won't currently display in creative due to how creative is coded but this is subject to change + ObjectRegistry.registerObject("examplechair", new ExampleWoodChairObject() + .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD) + .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD),50,true); + + // Register a grass object + ObjectRegistry.registerObject("examplegrass",new ExampleGrassObject(),1,true); + + // Register an object which uses a level event + ObjectRegistry.registerObject("exampleleveleventobject", new ExampleLevelEventObject(),1,true); + + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModPackets.java b/src/main/java/examplemod/Loaders/ExampleModPackets.java new file mode 100644 index 0000000..8f09fd9 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModPackets.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.examples.packets.ExamplePacket; +import examplemod.examples.packets.ExamplePlaySoundPacket; +import necesse.engine.registries.PacketRegistry; + +public class ExampleModPackets { + + public static void load() { + // Register our packets + PacketRegistry.registerPacket(ExamplePacket.class); + + PacketRegistry.registerPacket(ExamplePlaySoundPacket.class); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModProjectiles.java b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java new file mode 100644 index 0000000..a6c190d --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.examples.projectiles.ExampleArrowProjectile; +import examplemod.examples.projectiles.ExampleProjectile; +import necesse.engine.registries.ProjectileRegistry; + +public class ExampleModProjectiles { + public static void load(){ + // Register our projectile + ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); + + // Register our arrow projectile + ProjectileRegistry.registerProjectile("examplearrowprojectile", ExampleArrowProjectile.class, "examplearrowprojectile","arrow_shadow"); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java new file mode 100644 index 0000000..deab3f5 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -0,0 +1,181 @@ +package examplemod.Loaders; + +import necesse.engine.registries.RecipeTechRegistry; +import necesse.inventory.recipe.Ingredient; +import necesse.inventory.recipe.Recipe; +import necesse.inventory.recipe.Recipes; + +/* +here is where we will register our recipes into the game. + there is potentially quite a few of them so this will allow us to maintain cleaner code +*/ +public class ExampleModRecipes { + + //Put your recipe registrations in here + public static void registerRecipes(){ + + // Example item recipe, crafted in inventory for 2 iron bars + Recipes.registerModRecipe(new Recipe( + "exampleitem", + 1, + RecipeTechRegistry.NONE, + new Ingredient[]{ + new Ingredient("examplebar", 2) + } + ).showAfter("woodboat")); // Show recipe after wood boat recipe + + + //FORGE RECIPES + Recipes.registerModRecipe(new Recipe( + "examplebar", + 1, + RecipeTechRegistry.FORGE, + new Ingredient[]{ + new Ingredient("exampleore",2) + }) + ); + + //IRON ANVIL RECIPES + Recipes.registerModRecipe(new Recipe( + "examplemeleesword", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 4), + new Ingredient("examplebar", 5) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplemagicstaff", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 5), + new Ingredient("examplebar", 4) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplesummonorb", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 3), + new Ingredient("examplebar", 2) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplehelmet", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 8), + new Ingredient("exampleitem", 2) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplechestplate", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 14), + new Ingredient("exampleitem", 4) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleboots", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 10), + new Ingredient("exampleitem", 3) + } + )); + + //WORKSTATION RECIPES + Recipes.registerModRecipe(new Recipe( + "examplewall", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampledoor", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleobject", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); + + //COOKING POT RECIPES + Recipes.registerModRecipe(new Recipe( + "examplefood", + 1, + RecipeTechRegistry.COOKING_POT, + new Ingredient[]{ + new Ingredient("bread", 1), + new Ingredient("strawberry", 2), + new Ingredient("sugar", 1) + } + )); + + //ALCHEMY RECIPES + Recipes.registerModRecipe(new Recipe( + "examplepotion", + 1, + RecipeTechRegistry.ALCHEMY, + new Ingredient[]{ + new Ingredient("speedpotion", 1), + } + )); + + //LANDSCAPING RECIPES + Recipes.registerModRecipe(new Recipe( + "examplebaserock", + 1, + RecipeTechRegistry.LANDSCAPING, + new Ingredient[]{ + new Ingredient("examplestone", 5), + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleorerock", + 1, + RecipeTechRegistry.LANDSCAPING, + new Ingredient[]{ + new Ingredient("examplestone", 5), + new Ingredient("exampleore", 5), + } + )); + + //CARPENTER RECIPES + Recipes.registerModRecipe(new Recipe( + "examplechair", + 1, + RecipeTechRegistry.CARPENTER, + new Ingredient[]{ + new Ingredient("examplelog", 5), + } + )); + + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModResources.java b/src/main/java/examplemod/Loaders/ExampleModResources.java new file mode 100644 index 0000000..0244312 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -0,0 +1,33 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; +import necesse.gfx.gameTexture.GameTexture; + +public class ExampleModResources { + public static void load(){ + // Sometimes your textures will have a black or other outline unintended under rotation or scaling + // This is caused by alpha blending between transparent pixels and the edge + // To fix this, run the preAntialiasTextures gradle task + // It will process your textures and save them again with a fixed alpha edge color + + ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); + ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); + ExampleSummonWeaponMob.texture = GameTexture.fromFile("mobs/examplesummonmob"); + + //initialising the sound to be used by our boss mob + ExampleMod.EXAMPLESOUND = GameSound.fromFile("examplesound"); + + // Optional settings (volume/pitch/falloff) – used when playing via SoundSettings + ExampleMod.EXAMPLESOUNDSETTINGS = new SoundSettings(ExampleMod.EXAMPLESOUND) + .volume(0.8f) + .basePitch(1.0f) + .pitchVariance(0.08f) + .fallOffDistance(900); + } +} + diff --git a/src/main/java/examplemod/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java new file mode 100644 index 0000000..f97d9eb --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -0,0 +1,14 @@ +package examplemod.Loaders; + +import examplemod.examples.tiles.ExampleGrassTile; +import examplemod.examples.tiles.ExampleTile; +import necesse.engine.registries.TileRegistry; + +public class ExampleModTiles { + + public static void load(){ + // Register our tiles + TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + TileRegistry.registerTile("examplegrasstile", new ExampleGrassTile(),1,false,false,true); + } +} diff --git a/src/main/java/examplemod/examples/ExampleBiome.java b/src/main/java/examplemod/examples/ExampleBiome.java deleted file mode 100644 index a6ae51e..0000000 --- a/src/main/java/examplemod/examples/ExampleBiome.java +++ /dev/null @@ -1,36 +0,0 @@ -package examplemod.examples; - -import necesse.engine.AbstractMusicList; -import necesse.engine.MusicList; -import necesse.engine.registries.MusicRegistry; -import necesse.entity.mobs.PlayerMob; -import necesse.level.maps.Level; -import necesse.level.maps.biomes.Biome; -import necesse.level.maps.biomes.MobSpawnTable; - -// A minimalist biome used solely for the ExampleIncursion -// the Example Mob is used here as the enemy spawn -public class ExampleBiome extends Biome { - - public static MobSpawnTable critters = new MobSpawnTable() - .include(Biome.defaultCaveCritters); - - public static MobSpawnTable mobs = new MobSpawnTable() - .add(100,"examplemob"); - - @Override - public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { - return new MusicList(MusicRegistry.ForestPath); - } - - @Override - public MobSpawnTable getCritterSpawnTable(Level level) { - return critters; - } - - @Override - public MobSpawnTable getMobSpawnTable(Level level) { - return mobs; - } - -} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleConstructorPatch.java b/src/main/java/examplemod/examples/ExampleConstructorPatch.java index 7b48c45..0ad921a 100644 --- a/src/main/java/examplemod/examples/ExampleConstructorPatch.java +++ b/src/main/java/examplemod/examples/ExampleConstructorPatch.java @@ -22,5 +22,4 @@ static void onExit(@Advice.This RabbitMob rabbitMob) { // Debug message to know it's working System.out.println("Exited RabbitMob constructor: " + rabbitMob.getStringID()); } - } diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java new file mode 100644 index 0000000..8ad567a --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -0,0 +1,52 @@ +package examplemod.examples; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.LootItem; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.inventory.lootTable.lootItem.OneOfLootItems; + +/** + * This loot table can be referenced from presets, object entities (like storage boxes), + * mobs, or any system that accepts a LootTable instance. + */ +public class ExampleLootTable { + + /** + * A reusable LootTable instance. + * The LootTable constructor takes a list of "loot entries" which are rolled when loot is generated. + * Each entry can be: + * - guaranteed items (LootItem) + * - probabilistic items (ChanceLootItem) + * - groups like "pick one of these" (OneOfLootItems) + */ + public static final LootTable exampleloottable = new LootTable( + + // Guaranteed drops: + // LootItem(String itemStringID, int amount) + // These are always added when the table is rolled. + new LootItem("exampleore", 8), + new LootItem("examplebar", 20), + new LootItem("examplepotion", 1), + new LootItem("examplefood", 1), + new LootItem("examplesapling", 1), + + // Group entry: OneOfLootItems will attempt to pick ONE option from the list below. + // In your case, the options are "chance-based" items. + new OneOfLootItems( + + // ChanceLootItem(float chance, String itemStringID) + // 0.60f = 60% chance for this item to be granted IF this option is selected. + // Because these are inside OneOfLootItems, the group will choose a single option, + // then that option rolls its chance. + new ChanceLootItem(0.60f, "examplesword"), + new ChanceLootItem(0.60f, "examplestaff") + ) + ); + + /** + * Private constructor to prevent instantiation. + * This class is intended to be used statically: ExampleLootTable.exampleloottable + */ + private ExampleLootTable() { + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleObjectEntity.java b/src/main/java/examplemod/examples/ExampleObjectEntity.java new file mode 100644 index 0000000..74eae65 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleObjectEntity.java @@ -0,0 +1,97 @@ +package examplemod.examples; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEvents; +import necesse.engine.network.server.ServerClient; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.level.maps.Level; + +import java.awt.*; + +public class ExampleObjectEntity extends ObjectEntity { + // Tracks whether a player was on it last tick (so we only trigger once per step-on) + private boolean pressed = false; + + // Small cooldown to avoid rapid re-triggers if the player jitters on the edge + private long nextTriggerTime = 0L; + + + public ExampleObjectEntity(Level level, int tileX, int tileY) { + // Create an ObjectEntity instance for the object placed at (tileX, tileY) on this level. + // The string is this ObjectEntity's type/id used by the engine for identification (save/load/sync). + super(level, "examplelevelevent", tileX, tileY); + + // no need to save this is only a demo + this.shouldSave = false; + } + + private Rectangle getWorldTileRect() { + // Full tile area in world pixels + return new Rectangle(tileX * 32, tileY * 32, 32, 32); + } + + @Override + public void serverTick() { + super.serverTick(); + + // Get the level + Level level = getLevel(); + + // Get the current time + long now = level.getTime(); + Rectangle tileRect = getWorldTileRect(); + + boolean hasPlayerOnTile = false; + + + // Check all connected server clients + for (ServerClient client : level.getServer().getClients()) { + if (client == null || client.playerMob == null) continue; + + // we want to specifically target the player rather than any mob + if (client.playerMob.getCollision().intersects(tileRect)) { + hasPlayerOnTile = true; + break; + } + } + + // if there's a player on the tile, and it's not pressed, and it's time to check + if (hasPlayerOnTile && !pressed && now >= nextTriggerTime) { + pressed = true; + nextTriggerTime = now + 300; // 300 time units cooldown (tweak as you like) + + int px = tileX * 32 + 16; + int py = tileY * 32 + 16; + + // If your ExampleLevelEvent targets a player slot, pick the first player standing on it: + int targetSlot = -1; + for (ServerClient client : level.getServer().getClients()) { + if (client != null && client.playerMob != null && client.playerMob.getCollision().intersects(tileRect)) { + targetSlot = client.slot; + + break; + } + } + + /* + * This is an example of triggering a level event (in this case ExampleLevelEvent). + * Use events.add(...) when both client and server need to be sent it. + * Use events.addHidden(...) when only the server needs to be sent it. + */ + level.entityManager.events.add(new ExampleLevelEvent(targetSlot, px, py)); + + /* + * This is an example of triggering an event (in this case ExampleEvent) + * which will fire the event listener for ExampleEvent to run its code + */ + GameEvents.triggerEvent(new ExampleEvent(level, targetSlot)); + } + + // Reset when nobody is standing on it + if (!hasPlayerOnTile) { + pressed = false; + } + } +} + diff --git a/src/main/java/examplemod/examples/ExamplePreset.java b/src/main/java/examplemod/examples/ExamplePreset.java new file mode 100644 index 0000000..708721b --- /dev/null +++ b/src/main/java/examplemod/examples/ExamplePreset.java @@ -0,0 +1,121 @@ +package examplemod.examples; + +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +/** + * ExamplePreset (Script-based) + * + * This preset is the same idea as your code-built room, but it is created using a big text string + * in Necesse's "PRESET script" format. + * + * Think of it like this: + * - The big string contains a saved layout (tiles + objects + rotations) + * - applyScript(...) reads that string and loads it into this Preset + * - Then we do extra steps (like filling a chest with loot) + */ +public class ExamplePreset extends Preset { + + /** + * Constructor + * + * You pass in GameRandom so anything random (like loot) can be rolled properly. + * In world generation, Necesse often uses a seeded random so the same world seed + * produces the same results every time. + */ + public ExamplePreset(GameRandom random) { + + // Create a preset that is 11 tiles wide and 11 tiles tall. + // The Preset parent class uses this to create arrays for tiles/objects/rotations. + super(11, 11); + + /* + * This is a PRESET script string. + * + * It's basically a "saved blueprint" of a structure. + * The game can export these, and you can paste them into code like this. + * + * The important parts (high level): + * + * width / height + * - Size of the structure. + * + * tileIDs + tiles + * - "tileIDs" is a small list (palette) of tile types used in this preset. + * - "tiles" is the full grid. + * - Each number in "tiles" refers to an entry from tileIDs. + * + * objectIDs + objects + * - Same idea as tiles, but for objects (walls, torches, air, storagebox). + * - "objectIDs" is the palette. + * - "objects" is the full grid. + * + * rotations + * - Rotation for each placed object (same length/order as the objects grid). + * - Most objects use rotation 0/1/2/3 for directions. + * + * ...Clear flags... + * - These tell the game whether it should clear decorations/walls/etc when stamping the preset. + * + * The string is huge because it contains *every tile* in the 11x11 grid. + * 11 x 11 = 121 entries, which matches the long arrays you see. + */ + String examplePresetScript = + "PRESET={width=11,height=11," + + "tileIDs=[98, exampletile]," + + "tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98]," + + "objectIDs=[0, air, 290, storagebox, 85, woodwall, 298, walltorch]," + + "objects=[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 290, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85]," + + "rotations=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2]," + + "tileObjectsClear=true,wallDecorObjectsClear=true,tableDecorObjectsClear=true," + + "clearOtherWires=false}\n"; + + /* + * applyScript(...) reads that big PRESET string and fills in: + * - which tiles exist at each coordinate + * - which objects exist at each coordinate + * - which rotations the objects use + * + * After this line runs, this Preset now "contains" that room layout. + */ + this.applyScript(examplePresetScript); + + /* + * Add loot into the storage box inside the preset. + * + * IMPORTANT IDEA: + * Coordinates here are PRESET coordinates, not world coordinates. + * + * So (5, 5) means: + * - 5 tiles from the left edge of the preset + * - 5 tiles from the top edge of the preset + * + * We are assuming the storage box was placed at that coordinate in the script. + */ + addInventory(ExampleLootTable.exampleloottable, random, 5, 5); + + /* + * Optional placement rule: + * + * addCanApplyRectEachPredicate checks a rectangle area and decides if the preset is allowed + * to be stamped there. + * + * This can prevent things like: + * - placing the room on top of an existing base + * - overwriting important tiles + * + * The lambda (level, levelX, levelY, dir) -> ... is a short way to write a function. + * + * Our rule says: + * "If the world tile is already a floor, do NOT allow the preset to be placed." + * + * The ! means "not". + * So: + * - if isFloor is true, !isFloor is false (so placement fails) + * - if isFloor is false, !isFloor is true (so placement is allowed) + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor + ); + } +} diff --git a/src/main/java/examplemod/examples/ExamplePresetCode.java b/src/main/java/examplemod/examples/ExamplePresetCode.java new file mode 100644 index 0000000..e4c644b --- /dev/null +++ b/src/main/java/examplemod/examples/ExamplePresetCode.java @@ -0,0 +1,127 @@ +package examplemod.examples; + +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +/** + * ExamplePresetCode + * This class describes a small "structure" (a room) that the game can stamp into the world. + * In Necesse, a "Preset" is basically a small grid of tiles + objects that can be placed onto a level. + * This version builds the room using normal Java code (loops and variables), + * instead of using a big PRESET={...} text script. + */ +public class ExamplePresetCode extends Preset { + + /** + * Constructor + * Constructors run when you create the object: new ExamplePresetCode(random) + * The GameRandom is passed in so things like loot can be randomized, + * but still be repeatable (important for world generation). + */ + public ExamplePresetCode(GameRandom random) { + + // This calls the Preset parent class constructor. + // It sets the size of the preset to 15 tiles wide and 11 tiles tall. + super(15, 11); + + /* + * Tiles and Objects in Necesse use numeric IDs internally. + * + * - TileRegistry.getTileID("name") looks up a TILE by its string ID + * - ObjectRegistry.getObjectID("name") looks up an OBJECT by its string ID + * + * We store those numbers in variables so we can use them repeatedly. + */ + int floor = TileRegistry.getTileID("stonefloor"); // ground tile + int wall = ObjectRegistry.getObjectID("stonewall"); // wall object + int air = ObjectRegistry.getObjectID("air"); // "nothing here" object + int storagebox = ObjectRegistry.getObjectID("storagebox"); // chest/container object + + /* + * Fill the entire preset area with a base: + * + * - Every tile becomes stone floor + * - Every object becomes air (empty) + * + * width and height are fields from the Preset parent class (because we called super(15, 11)). + */ + for (int x = 0; x < width; x++) { // loop across columns (left -> right) + for (int y = 0; y < height; y++) { // loop across rows (top -> bottom) + setTile(x, y, floor); // place the floor tile at (x, y) + setObject(x, y, air); // clear any object at (x, y) + } + } + + /* + * Build the walls around the edge of the preset. + * + * First: top wall (y = 0) and bottom wall (y = height - 1) + */ + for (int x = 0; x < width; x++) { + setObject(x, 0, wall); // top edge + setObject(x, height - 1, wall); // bottom edge + } + + /* + * Next: left wall (x = 0) and right wall (x = width - 1) + */ + for (int y = 0; y < height; y++) { + setObject(0, y, wall); // left edge + setObject(width - 1, y, wall); // right edge + } + + /* + * Choose a position in the middle of the room for the storage box. + * + * width / 2 and height / 2 are integer division in Java. + * Example: 15 / 2 becomes 7 (Java drops the .5) + */ + int storageboxX = width / 2; + int storageboxY = height / 2; + + /* + * Place the storage box object at the center. + * + * setObject(x, y, objectID, rotation) + * + * Some objects use rotation to decide which way they face. + * 0/1/2/3 usually mean different directions. + * For a storage box it usually doesn't matter much, but we set it anyway. + */ + setObject(storageboxX, storageboxY, storagebox, 1); + + /* + * Fill the storage box with loot. + * + * ExampleLootTable.exampleloottable is your custom LootTable from the other class. + * + * addInventory(...) searches for an object with an inventory at that position + * (like a storagebox) and then generates loot into it. + */ + addInventory(ExampleLootTable.exampleloottable, random, storageboxX, storageboxY); + + /* + * OPTIONAL SAFETY RULE (CanApply predicate): + * + * This says: "Only allow this preset to be placed if the area is suitable." + * + * addCanApplyRectEachPredicate(...) checks every tile in a rectangle. + * If ANY tile fails the test, the preset cannot be applied there. + * + * Our test: + * !level.getTile(levelX, levelY).isFloor + * + * Meaning: + * - If the tile already IS a floor, then this returns false (bad) + * - If the tile is NOT a floor, then this returns true (good) + * + * In plain English: + * "Don't place this room on top of an area that already has flooring." + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor + ); + } +} diff --git a/src/main/java/examplemod/examples/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java new file mode 100644 index 0000000..22e4b2b --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -0,0 +1,65 @@ +package examplemod.examples.ai; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.composites.SelectorAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.CollisionChaserAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; +import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; + +public class ExampleAI extends SelectorAINode { + + // My custom “fix spawn position” leaf. + // This runs first so the mob gets put somewhere sensible before doing anything else. + public final ExampleAILeaf teleporter; + + // Vanilla AI that does: find player -> chase -> when close enough, call attackTarget(). + // We keep it as a field so we can reuse the damage/knockback values from it. + public final CollisionPlayerChaserAI chaser; + + // Vanilla “walk around randomly” node. This is what happens when there’s no player to chase. + public final WandererAINode wanderer; + + public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { + + // A Selector is basically: "try child #1, if it can run then use it, + // otherwise try child #2, otherwise child #3..." + // + // So the ORDER we add children is the ORDER of priority. + + // 1) Teleport / reposition leaf (highest priority). + // (In my leaf: 8 tiles = how far to check for open space, 10 tiles = how far to search for a valid spot) + this.teleporter = new ExampleAILeaf<>(8, 10); + addChild(this.teleporter); + + // 2) Chase + attack (second priority). + this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { + + // The chaser decides WHEN it should attack, but it asks us HOW to attack. + // So we override this and forward it to our own method below. + @Override + public boolean attackTarget(T mob, Mob target) { + return ExampleAI.this.attackTarget(mob, target); + } + }; + addChild(this.chaser); + + // 3) Wander around if we aren’t teleporting, and we aren’t chasing anyone. + this.wanderer = new WandererAINode<>(wanderFrequency); + addChild(this.wanderer); + } + + // This is the “how to attack” part used by the chaser above. + // Keeping it here makes it easy to change later (special attacks, effects, etc). + public boolean attackTarget(T mob, Mob target) { + + // simpleAttack is the vanilla helper for a basic melee hit. + // It handles cooldown/range stuff internally and returns true if an attack happened. + return CollisionChaserAINode.simpleAttack( + mob, + target, + this.chaser.damage, // use the damage we configured in the chaser + this.chaser.knockback // use the knockback we configured in the chaser + ); + } +} diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java new file mode 100644 index 0000000..30d128a --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -0,0 +1,204 @@ +package examplemod.examples.ai; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +import examplemod.examples.packets.ExamplePlaySoundPacket; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.AINode; +import necesse.entity.mobs.ai.behaviourTree.AINodeResult; +import necesse.entity.mobs.ai.behaviourTree.Blackboard; +import necesse.level.maps.IncursionLevel; +import necesse.level.maps.Level; + +/* + This is a "run once after spawn" AI leaf. + + The problem it solves: + • Sometimes a mob spawns in a bad spot in an Incursion (not in the cleared entrance area) + • If that happens, we move it into the entrance area so the fight / behaviour starts properly + + What we consider the "entrance area": + • The centre is the Incursion return portal position + • The biome generator clears open space around that portal +*/ +public class ExampleAILeaf extends AINode { + + // We only want to do the check once per mob. + private boolean didCheck = false; + + // How close you need to be to the portal to count as "in the entrance area" + // (in tiles, converted to pixels later). + private final int openRadiusTiles; + + // How far we search around the portal to find a safe landing spot. + private final int searchRadiusTiles; + + public ExampleAILeaf(int openRadiusTiles, int searchRadiusTiles) { + // Just making sure we don't get silly values. + this.openRadiusTiles = Math.max(1, openRadiusTiles); + this.searchRadiusTiles = Math.max(this.openRadiusTiles, searchRadiusTiles); + } + + @Override + protected void onRootSet(AINode aiNode, T t, Blackboard blackboard) { + // Nothing needed here for this leaf. + } + + @Override + public void init(T mob, Blackboard blackboard) { + // Nothing to init. We just do everything in tick() one time. + } + + @Override + public AINodeResult tick(T mob, Blackboard blackboard) { + + // Don’t keep doing this every tick. We only run it once. + if (didCheck) return AINodeResult.FAILURE; + didCheck = true; + + // Only do this on the server. + // The server is the “real” source of truth for mob position. + if (!mob.isServer()) return AINodeResult.FAILURE; + + Level level = mob.getLevel(); + + // This only applies to Incursion levels. + if (!(level instanceof IncursionLevel)) return AINodeResult.FAILURE; + + IncursionLevel incursion = (IncursionLevel) level; + + // The "entrance centre" is the return portal position. + // If this is null, something is wrong / not generated yet, so bail. + Point portalPos = incursion.getReturnPortalPosition(); + if (portalPos == null) return AINodeResult.FAILURE; + + float centerX = portalPos.x; + float centerY = portalPos.y; + + // If we're already close enough to the portal, we count as “in the entrance area”. + // (Tiles -> pixels. Tiles are 32x32 in Necesse.) + float openRadiusPx = this.openRadiusTiles * 32.0f; + if (mob.getDistance(centerX, centerY) <= openRadiusPx) { + + // Sounds must be played on clients, not on the server. + // So we send a packet to nearby clients telling them to play the sound here if teleport doesnt happen . + mob.getLevel().getServer().network.sendToClientsWithEntity( + new ExamplePlaySoundPacket(mob.x, mob.y), + mob + ); + // Already fine, do nothing. + return AINodeResult.FAILURE; + } + + // We’re NOT in the entrance area, so we need to move the mob. + // Look for a valid spot near the portal. + Point2D.Float dest = findValidTeleportPos(incursion, mob, centerX, centerY, this.searchRadiusTiles); + + // If we found somewhere safe, teleport the mob there. + if (dest != null) { + + // Stop any current movement so we don't fight with pathing. + mob.stopMoving(); + + // Move instantly. + // The "true" here is important: it makes sure the move is synced properly. + mob.setPos(dest.x, dest.y, true); + + // Sounds must be played on clients, not on the server. + // So we send a packet to nearby clients telling them to play the sound here if teleport happens . + mob.getLevel().getServer().network.sendToClientsWithEntity( + new ExamplePlaySoundPacket(mob.x, mob.y), + mob + ); + } + + // We always return FAILURE here because this leaf isn't meant to "take over" the AI. + // It's just a one-time fix, then the parent Selector can move on to chase/wander normally. + return AINodeResult.FAILURE; + } + + /* + Find a safe tile near the portal to teleport to. + + We search in "rings" around the centre: + • r = 0 is the centre tile + • r = 1 is the tiles touching it + • r = 2 is the next ring out, etc + + For each tile we check: + • Not liquid + • Not shore + • Mob doesn't collide with the map + • Mob doesn't collide with another mob/player + */ + private static Point2D.Float findValidTeleportPos(IncursionLevel level, Mob mob, float centerX, float centerY, int searchRadiusTiles) { + int centerTileX = GameMath.getTileCoordinate(centerX); + int centerTileY = GameMath.getTileCoordinate(centerY); + + // Try rings from centre outward until we find something. + for (int r = 0; r <= searchRadiusTiles; r++) { + ArrayList ring = buildRing(centerTileX, centerTileY, r); + + // Shuffle-ish: pick random points so we don't always choose the same spot every time. + while (!ring.isEmpty()) { + Point p = ring.remove(GameRandom.globalRandom.nextInt(ring.size())); + + // Don't teleport into water/shore. + if (level.isLiquidTile(p.x, p.y)) continue; + if (level.isShore(p.x, p.y)) continue; + + // Convert tile coords to world pixel coords. + // +16 puts us in the centre of the tile. + int px = p.x * 32 + 16; + int py = p.y * 32 + 16; + + // Make sure the mob can actually stand there. + if (mob.collidesWith(level, px, py)) continue; + + // Also don't land inside another mob/player. + if (mob.collidesWithAnyMob(level, px, py)) continue; + + // This spot looks good. + return new Point2D.Float(px, py); + } + } + + // Couldn't find a valid spot. + return null; + } + + /* + Build a list of tile points that make up a square "ring" around (cx, cy). + + r = 0: just the centre + r = 1: the outer edge of a 3x3 square + r = 2: the outer edge of a 5x5 square + etc + */ + private static ArrayList buildRing(int cx, int cy, int r) { + ArrayList points = new ArrayList<>(); + + if (r == 0) { + points.add(new Point(cx, cy)); + return points; + } + + // Top + bottom edges + for (int dx = -r; dx <= r; dx++) { + points.add(new Point(cx + dx, cy - r)); + points.add(new Point(cx + dx, cy + r)); + } + + // Left + right edges (skip corners because top/bottom already added them) + for (int dy = -r + 1; dy <= r - 1; dy++) { + points.add(new Point(cx - r, cy + dy)); + points.add(new Point(cx + r, cy + dy)); + } + + return points; + } +} diff --git a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java new file mode 100644 index 0000000..ec17866 --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java @@ -0,0 +1,24 @@ +package examplemod.examples.buffs; + +import necesse.engine.modifiers.ModifierValue; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.BuffModifiers; +import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.setBonusBuffs.SimpleSetBonusBuff; + +public class ExampleArmorSetBuff extends SimpleSetBonusBuff { + public ExampleArmorSetBuff() { + super( + new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), + new ModifierValue<>(BuffModifiers.SPEED, 0.10f) + ); + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + this.canCancel = false; + this.isPassive = true; + this.isVisible = true; + super.init(buff, eventSubscriber); + } +} diff --git a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java new file mode 100644 index 0000000..cedb0e2 --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -0,0 +1,52 @@ +package examplemod.examples.buffs; + +import necesse.entity.levelEvent.mobAbilityLevelEvent.MobHealthChangeEvent; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.staticBuffs.Buff; + +public class ExampleArrowBuff extends Buff { + + // Heal interval in milliseconds + private static final int HEAL_INTERVAL_MS = 250; + + public ExampleArrowBuff() { + this.canCancel = false; + this.isVisible = false; + this.isPassive = false; + this.shouldSave = false; + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + buff.getGndData().setInt("timePassed", 0); + } + @Override + public void serverTick(ActiveBuff buff) { + Mob m = buff.owner; + if (m == null) return; + + int heal = buff.getGndData().getInt("healPerTick"); + if (heal <= 0) return; + + int accum = buff.getGndData().getInt("timePassed"); + accum += 50; + + if (accum < HEAL_INTERVAL_MS) { + buff.getGndData().setInt("timePassed", accum); + return; + } + + accum -= HEAL_INTERVAL_MS; + buff.getGndData().setInt("timePassed", accum); + + int before = m.getHealth(); + int finalHealth = Math.min(m.getMaxHealth(), before + heal); + int applied = finalHealth - before; + + if (applied > 0) { + m.getLevel().entityManager.events.add(new MobHealthChangeEvent(m, finalHealth, applied)); + } + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleBuff.java b/src/main/java/examplemod/examples/buffs/ExampleBuff.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleBuff.java rename to src/main/java/examplemod/examples/buffs/ExampleBuff.java index 2dec99d..e2310f5 100644 --- a/src/main/java/examplemod/examples/ExampleBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleBuff.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.buffs; import necesse.entity.mobs.buffs.ActiveBuff; import necesse.entity.mobs.buffs.BuffEventSubscriber; diff --git a/src/main/java/examplemod/examples/events/ExampleEvent.java b/src/main/java/examplemod/examples/events/ExampleEvent.java new file mode 100644 index 0000000..4c4cc7e --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleEvent.java @@ -0,0 +1,35 @@ +package examplemod.examples.events; + +import necesse.engine.events.GameEvent; +import necesse.level.maps.Level; + +/* + * ExampleEvent is a small "notification" object for our mod. + * + * Compared to a LevelEvent + * it does not exist in the world + * it does not tick + * it does not draw anything + * + * Instead, we create one and pass it through GameEvents.triggerEvent(...) + * so any registered listeners can react. + */ +public class ExampleEvent extends GameEvent { + + // The level where the event happened + public final Level level; + + // The slot id of the player this event relates to + public final int clientSlot; + + // Simple data payload for the demo + public final String message; + + public ExampleEvent(Level level, int clientSlot) { + this.level = level; + this.clientSlot = clientSlot; + + // For a real mod you would usually pass the message into the constructor. + this.message = "PING: this message was sent from the ExampleEvent"; + } +} diff --git a/src/main/java/examplemod/examples/events/ExampleLevelEvent.java b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java new file mode 100644 index 0000000..82fefe1 --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java @@ -0,0 +1,176 @@ +package examplemod.examples.events; + +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.server.ServerClient; +import necesse.entity.levelEvent.LevelEvent; +import necesse.entity.levelEvent.mobAbilityLevelEvent.MobHealthChangeEvent; +import necesse.entity.mobs.GameDamage; +import necesse.entity.particle.Particle; + +import java.awt.Color; + +/** + * LevelEvent + * Server: sends a chat message to a specific player. + * Client: spawns a quick burst of particles at a world position. + * events.addHidden(ev) server only + * events.add(ev) server + clients + */ +public class ExampleLevelEvent extends LevelEvent { + + // Which ServerClient (player) to message. -1 = invalid/unset. + private int targetSlot = -1; + + // Message to send. + private String message = ""; + + // World position (pixels) where the particle effect should appear. + private int worldX; + private int worldY; + + // Simple lifetime for the client effect (in ticks). + private int ticksLeft = 20; + + // Small guard so the server only sends the message once. + private boolean sentServerMessage = false; + + // Required empty constructor for registry/network spawning + public ExampleLevelEvent() { + } + + /** + * targetSlot player to message + * worldX particle position X in pixels (tileX * 32 + 16 is tile center) + * worldY particle position Y in pixels + */ + public ExampleLevelEvent(int targetSlot, int worldX, int worldY) { + this.targetSlot = targetSlot; + this.worldX = worldX; + this.worldY = worldY; + this.message = "ZING: this message was sent from the ExampleLevelEvent"; + } + + @Override + public void init() { + super.init(); + + // Server side: send the message once. + if (isServer() && !sentServerMessage) { + sentServerMessage = true; + + //target the specific client that triggered the event and make sure it's not returned null + ServerClient client = level.getServer().getClient(targetSlot); + if (client != null) { + + GameDamage dmg = new GameDamage(30F); + + //Apply damage + int healthBefore = client.playerMob.getHealth(); + client.playerMob.isServerHit(dmg, worldX, worldY, 0.0F, null); + int healthAfter = client.playerMob.getHealth(); + + //How much damage was actually taken + int damageTaken = healthBefore - healthAfter; + + if (damageTaken > 0) { + //Restore that amount clamped to current max health, and compute the healing applied because this is a demo, and we aren't mean xD + int finalHealth = Math.min(client.playerMob.getMaxHealth(), healthAfter + damageTaken); + int healApplied = finalHealth - healthAfter; + + //send a health change event to apply + level.entityManager.events.add(new MobHealthChangeEvent(client.playerMob, finalHealth, healApplied)); + } + + client.sendChatMessage(message); + } + + // Do NOT call over() here if you want clients to see the event. + // Let it exist long enough for the spawn packet to be processed. + } + } + + @Override + public void serverTick() { + super.serverTick(); + + // If this event was added as hidden, it will never go to clients, + // so we can end it right away after doing server work. + if (sentServerMessage) { + over(); + } + } + + @Override + public void clientTick() { + super.clientTick(); + + // Quick particle burst for a short time, then end. + if (ticksLeft-- <= 0) { + over(); + return; + } + + Color c = new Color(120, 200, 255); + + for (int i = 0; i < 6; i++) { + float ox = (float) (Math.random() * 24.0 - 12.0); // -12..12 px + float oy = (float) (Math.random() * 24.0 - 12.0); + + level.entityManager + .addParticle(worldX + ox, worldY + oy, Particle.GType.COSMETIC) + .color(c) + .sizeFades(30, 60) + .fadesAlphaTime(250, 150) + .lifeTime(350); + } + } + + @Override + public void setupSpawnPacket(PacketWriter writer) { + super.setupSpawnPacket(writer); + + /* + * setupSpawnPacket(...) is called when the server spawns this LevelEvent to clients + * (when you use level.entityManager.events.add/level.entityManager.events.addHidden) + * + * Anything you want the client-side version of this event to know must be written here. + * The client will read these values in applySpawnPacket(...) in the exact same order. + */ + + // Who the event should target (which player/server client slot) + writer.putNextInt(targetSlot); + + // Any text payload we want associated with the event + writer.putNextString(message); + + // World position for effects (these should be pixel coords, not tile coords) + writer.putNextInt(worldX); + writer.putNextInt(worldY); + + // How long the client-side effect should run + writer.putNextInt(ticksLeft); + } + + @Override + public void applySpawnPacket(PacketReader reader) { + super.applySpawnPacket(reader); + + /* + * applySpawnPacket(...) is called on the client when it receives the spawn packet + * for this LevelEvent. + * + * Make sure you read values back in the same order you wrote them in setupSpawnPacket(...), + * otherwise you'll desync fields and get confusing bugs. + */ + + // Read target player slot + message payload + targetSlot = reader.getNextInt(); + message = reader.getNextString(); + + // Read effect position + lifetime + worldX = reader.getNextInt(); + worldY = reader.getNextInt(); + ticksLeft = reader.getNextInt(); + } +} diff --git a/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java new file mode 100644 index 0000000..058b331 --- /dev/null +++ b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java @@ -0,0 +1,37 @@ +package examplemod.examples.items.ammo; + +import necesse.engine.registries.ProjectileRegistry; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.itemAttacker.ItemAttackerMob; +import necesse.entity.projectile.Projectile; +import necesse.inventory.item.arrowItem.ArrowItem; + +public class ExampleArrowItem extends ArrowItem { + + public ExampleArrowItem() { + super(5000); // stack size like vanilla arrows + + // These fields are on ArrowItem (public), and BowProjectileToolItem uses them via modDamage/modVelocity. + this.damage = 8; // adds +8 damage to the bow’s base damage + this.armorPen = 2; // adds armor pen + this.critChance = 0.05f; // +5% base crit + this.speedMod = 1.10f; // 10% faster arrow velocity + } + + @Override + public Projectile getProjectile(float x, float y, float targetX, float targetY, + float velocity, int range, GameDamage damage, int knockback, + ItemAttackerMob owner) { + + // Same exact pattern as StoneArrowItem / IronArrowItem, etc. + return ProjectileRegistry.getProjectile( + "examplearrowprojectile", // your projectile stringID + owner.getLevel(), + x, y, targetX, targetY, + velocity, range, + damage, knockback, + (Mob) owner + ); + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java new file mode 100644 index 0000000..1297dfc --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java @@ -0,0 +1,16 @@ +package examplemod.examples.items.armor; + +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.BootsArmorItem; +import necesse.inventory.lootTable.presets.FeetArmorLootTable; + +public class ExampleBootsArmorItem extends BootsArmorItem { + public ExampleBootsArmorItem() { + + super(2, //armorValue + 300, //enchantCost + Item.Rarity.UNCOMMON, //rarity + "exampleboots", //textureName + FeetArmorLootTable.feetArmor); //lootTableCategory + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java new file mode 100644 index 0000000..6831006 --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java @@ -0,0 +1,17 @@ +package examplemod.examples.items.armor; + +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.ChestArmorItem; +import necesse.inventory.lootTable.presets.BodyArmorLootTable; + +public class ExampleChestArmorItem extends ChestArmorItem { + public ExampleChestArmorItem() { + super(4, //armorValue + 300, //enchantCost + Item.Rarity.UNCOMMON, //rarity + "examplechest", //bodyTextureName + "examplearms", //armsTextureName + BodyArmorLootTable.bodyArmor); //lootTableCategory + + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java new file mode 100644 index 0000000..965fe75 --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java @@ -0,0 +1,24 @@ +package examplemod.examples.items.armor; + +import necesse.engine.registries.DamageTypeRegistry; +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.SetHelmetArmorItem; +import necesse.inventory.lootTable.presets.ArmorSetsLootTable; +import necesse.inventory.lootTable.presets.HeadArmorLootTable; + +public class ExampleHelmetArmorItem extends SetHelmetArmorItem { + public ExampleHelmetArmorItem() { + super( + 3, //armor value + DamageTypeRegistry.MELEE, //damage class for enchant scaling etc + 300, //enchant cost + HeadArmorLootTable.headArmor, //head armor loot category + ArmorSetsLootTable.armorSets, //armor SETS loot category + Item.Rarity.UNCOMMON, + "examplehelmet", //helmet texture name + "examplechestplate", //chest item STRING ID + "exampleboots", //boots item STRING ID + "examplearmorsetbonus" //buff STRING ID + ); + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleFoodItem.java b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java similarity index 95% rename from src/main/java/examplemod/examples/items/ExampleFoodItem.java rename to src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java index 1f5ce13..ce4279b 100644 --- a/src/main/java/examplemod/examples/items/ExampleFoodItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.consumable; import necesse.engine.modifiers.ModifierValue; import necesse.entity.mobs.buffs.BuffModifiers; diff --git a/src/main/java/examplemod/examples/items/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java similarity index 85% rename from src/main/java/examplemod/examples/items/ExamplePotionItem.java rename to src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java index 17bdcdc..a2293fa 100644 --- a/src/main/java/examplemod/examples/items/ExamplePotionItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.consumable; import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; diff --git a/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java new file mode 100644 index 0000000..7251215 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java @@ -0,0 +1,12 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleBarItem extends MatItem { + + public ExampleBarItem() { + super(500, Item.Rarity.UNCOMMON); + + } +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java new file mode 100644 index 0000000..7e8164b --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -0,0 +1,21 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.tileItem.GrassSeedItem; + +/** + * A seed item that turns dirt into our custom grass tile when placed. + * + * Vanilla uses GrassSeedItem for grass seeds. It handles: + * Only placing on dirt + * Tile placement + preview + * Consuming the item (unless in god mode) + * "Grass seed" style tooltip and crafting ingredients + */ +public class ExampleGrassSeedItem extends GrassSeedItem { + + public ExampleGrassSeedItem() { + // This must match your TileRegistry stringID + // i.e. TileRegistry.registerTile("examplegrasstile", ...) + super("examplegrasstile"); + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java similarity index 81% rename from src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java index 8c15c37..f54d3fb 100644 --- a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java new file mode 100644 index 0000000..5d2a978 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java @@ -0,0 +1,11 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.matItem.MatItem; + +public class ExampleLogItem extends MatItem { + + public ExampleLogItem() { + super(500,Rarity.UNCOMMON, new String[]{"anylog"}); + + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java similarity index 80% rename from src/main/java/examplemod/examples/items/ExampleMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java index a20ddb6..12e9220 100644 --- a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java new file mode 100644 index 0000000..6a04fcb --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java @@ -0,0 +1,12 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleOreItem extends MatItem { + + public ExampleOreItem() { + super(500, Item.Rarity.UNCOMMON); + + } +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java new file mode 100644 index 0000000..faeff08 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java @@ -0,0 +1,9 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.StonePlaceableItem; + +public class ExampleStoneItem extends StonePlaceableItem { + public ExampleStoneItem(){ + super(100); + } +} diff --git a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java index 31aff93..b604b4e 100644 --- a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java @@ -1,5 +1,6 @@ -package examplemod.examples; +package examplemod.examples.items.tools; +import examplemod.examples.projectiles.ExampleProjectile; import necesse.engine.localization.Localization; import necesse.engine.network.gameNetworkData.GNDItemMap; import necesse.engine.sound.SoundEffect; @@ -17,13 +18,13 @@ import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleProjectileWeapon extends MagicProjectileToolItem { +public class ExampleMagicStaffWeapon extends MagicProjectileToolItem { // This weapon will shoot out some projectiles. // Different classes for specific projectile weapon are already in place that you can use: // GunProjectileToolItem, BowProjectileToolItem, BoomerangToolItem, etc. - public ExampleProjectileWeapon() { + public ExampleMagicStaffWeapon() { super(400, null); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); @@ -41,7 +42,7 @@ public ExampleProjectileWeapon() { @Override public ListGameTooltips getPreEnchantmentTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { ListGameTooltips tooltips = super.getPreEnchantmentTooltips(item, perspective, blackboard); - tooltips.add(Localization.translate("itemtooltip", "examplestafftip")); + tooltips.add(Localization.translate("itemtooltip", "examplemagicstafftip")); return tooltips; } diff --git a/src/main/java/examplemod/examples/ExampleSwordItem.java b/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java similarity index 80% rename from src/main/java/examplemod/examples/ExampleSwordItem.java rename to src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java index 68fc649..1e72fee 100644 --- a/src/main/java/examplemod/examples/ExampleSwordItem.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java @@ -1,14 +1,14 @@ -package examplemod.examples; +package examplemod.examples.items.tools; import necesse.inventory.item.Item; import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; // Extends SwordToolItem -public class ExampleSwordItem extends SwordToolItem { +public class ExampleMeleeSwordWeapon extends SwordToolItem { // Weapon attack textures are loaded from resources/player/weapons/ - public ExampleSwordItem() { + public ExampleMeleeSwordWeapon() { super(400, null); rarity = Item.Rarity.UNCOMMON; attackAnimTime.setBaseValue(300); // 300 ms attack time @@ -16,6 +16,7 @@ public ExampleSwordItem() { .setUpgradedValue(1, 95); // Upgraded tier 1 damage attackRange.setBaseValue(120); // 120 range knockback.setBaseValue(100); // 100 knockback + } } diff --git a/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java new file mode 100644 index 0000000..16740cd --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java @@ -0,0 +1,31 @@ +package examplemod.examples.items.tools; + +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.projectileToolItem.bowProjectileToolItem.BowProjectileToolItem; +import necesse.inventory.lootTable.presets.BowWeaponsLootTable; + +public class ExampleRangedBowWeapon extends BowProjectileToolItem { + public ExampleRangedBowWeapon() { + // (enchantCost, lootTableCategory) + super(100, BowWeaponsLootTable.bowWeapons); + + this.rarity = Item.Rarity.NORMAL; + + // Core stats + this.attackAnimTime.setBaseValue(800); // ms per shot + this.attackDamage.setBaseValue(12.0F); // base bow damage (arrows further modify) + this.attackRange.setBaseValue(600); // tiles-ish range value used by bows + this.velocity.setBaseValue(100); // base projectile velocity (arrows further modify) + this.knockback.setBaseValue(25); + + // Sprite offsets (tune until it looks right in-hand) + this.attackXOffset = 8; + this.attackYOffset = 20; + + // How much the bow sprite “stretches” while charging + this.attackSpriteStretch = 4; + + // Optional + this.canBeUsedForRaids = true; + } +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java new file mode 100644 index 0000000..77d32d7 --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java @@ -0,0 +1,29 @@ +package examplemod.examples.items.tools; + +import necesse.entity.mobs.itemAttacker.FollowPosition; +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.summonToolItem.SummonToolItem; +import necesse.inventory.lootTable.presets.SummonWeaponsLootTable; + +public class ExampleSummonOrbWeapon extends SummonToolItem { + public ExampleSummonOrbWeapon() { + // mobStringID, followPosition, summonSpaceTaken, enchantCost, lootTableCategory + super("examplesummonmob", + FollowPosition.PYRAMID, + 1.0F, + 400, + SummonWeaponsLootTable.summonWeapons); + + this.rarity = Item.Rarity.UNCOMMON; + + // This damage is what gets injected into your minion via mob.updateDamage(getAttackDamage(item)) + this.attackDamage.setBaseValue(50.0F).setUpgradedValue(1.0F, 45.0F); + + // Offset the X location of the attack texture + this.attackXOffset = 15; + // Offset the X location of the attack texture + this.attackYOffset = 10; + + + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java new file mode 100644 index 0000000..3ff999c --- /dev/null +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -0,0 +1,178 @@ +package examplemod.examples.maps.biomes; + +import necesse.engine.AbstractMusicList; +import necesse.engine.MusicList; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.engine.world.biomeGenerator.BiomeGeneratorStack; +import necesse.entity.mobs.PlayerMob; +import necesse.level.maps.Level; +import necesse.level.maps.biomes.Biome; +import necesse.level.maps.biomes.MobSpawnTable; +import necesse.level.maps.regionSystem.Region; + +import java.awt.Color; + +/** + * Example overworld biome (1.1.x "infinite surface" style). + * + * Key idea for new modders: + * ------------------------ + * In Necesse 1.1.x, the overworld surface is generated in *regions* by the game's SurfaceLevel. + * SurfaceLevel does NOT create a special "ExampleBiomeLevel" per biome anymore. + * + * Instead, SurfaceLevel asks the Biome for: + * - which ground tile to paint (getGenerationTerrainTileID) + * - what "noise/vein" patterns to use for placing objects (initializeGeneratorStack) + * - what objects to place in each region (generateRegionSurfaceTerrain) + * + * So: if you want a biome that changes the overworld terrain + trees, these are the methods to override. + */ +public class ExampleBiome extends Biome { + + // -------------------------- + // Spawn tables + // -------------------------- + // MobSpawnTable controls what can spawn in the biome. + // Vanilla provides some defaults; we "include" them and then add our own entries. + + /** Small critters (e.g. butterflies, squirrels) for surface regions of this biome. */ + public static final MobSpawnTable surfaceCritters = new MobSpawnTable() + .include(Biome.defaultSurfaceCritters); + + /** Critters for cave levels (if the player is in caves). */ + public static final MobSpawnTable caveCritters = new MobSpawnTable() + .include(Biome.defaultCaveCritters); + + /** Hostile/neutral mobs for the surface. We add our custom mob "examplemob". */ + public static final MobSpawnTable surfaceMobs = new MobSpawnTable() + .include(Biome.defaultSurfaceMobs) + .add(30, "examplemob"); // weight: higher = more likely relative to other entries + + /** Hostile/neutral mobs for caves. Usually a different balance than the surface. */ + public static final MobSpawnTable caveMobs = new MobSpawnTable() + .include(Biome.defaultCaveMobs) + .add(100, "examplemob"); + + public ExampleBiome() { + super(); + + // Generation weight decides how often this biome is chosen when the world is generating new regions. + // Vanilla values are typically around ~0.5 to ~1.5. Keep it in that range while testing. + // (Huge numbers can cause problems with spawn finding and biome distribution.) + this.setGenerationWeight(1.0F); + } + + // -------------------------- + // Overworld world generation hooks (SurfaceLevel -> Biome) + // -------------------------- + + /** + * This is the *base ground tile* that SurfaceLevel paints for this biome in new surface regions. + * + * We look up our custom tile by string ID. If it isn't registered (returns -1), + * we fall back to vanilla grass so the game doesn't break and we can still load the world. + */ + @Override + public int getGenerationTerrainTileID() { + int exampleGrass = TileRegistry.getTileID("examplegrasstile"); + if (exampleGrass == -1) { + // -1 means "not found in registry" (usually a typo or missing registration). + return TileRegistry.grassID; + } + return exampleGrass; + } + + /** + * The BiomeGeneratorStack is a helper that stores "noise patterns" used during worldgen. + * + * Think of it like: "make a map of blobs/veins for trees", then when placing objects + * we can say "place trees only on those blobs" to get natural clumps. + * + * This method is called as part of generator setup, not every tick. + */ + @Override + public void initializeGeneratorStack(BiomeGeneratorStack stack) { + super.initializeGeneratorStack(stack); + + // Register a simplex-based "vein" pattern named "exampleTrees". + // We'll reference this by name later when placing our trees. + // + // The parameters control clump size / branching / frequency. + // If you want denser or larger clumps, we can tweak these values. + stack.addRandomSimplexVeinsBranch("exampleTrees", 2.0F, 0.2F, 1.0F, 0); + } + + /** + * Called when SurfaceLevel is generating a specific *region* of the overworld surface. + * + * This is where you place objects like: + * - trees + * - grass tufts + * - flowers + * - rocks, etc. + * + * IMPORTANT: This runs during world generation for new/unexplored regions. + * It does NOT retroactively change already-generated terrain. + */ + @Override + public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionSurfaceTerrain(region, stack, random); + + // Cache our terrain tile ID so we place objects only on the correct ground. + final int grassTile = getGenerationTerrainTileID(); + + // Place our custom tree object using the "exampleTrees" vein pattern. + // This creates forest-like clusters instead of a perfectly even distribution. + stack.startPlaceOnVein(this, region, random, "exampleTrees") + .onlyOnTile(grassTile) // only place on our biome's land tile + .chance(0.10D) // density inside valid vein areas (tweak for more/less) + .placeObject("exampletree"); + + // Place vanilla "grass" decoration objects on top of our tile. + // This is purely visual and helps the biome feel "alive". + stack.startPlace(this, region, random) + .chance(0.40D) // overall density of grass objects + .onlyOnTile(grassTile) + .placeObject("grass"); + } + + /** + * Used by some debug tools/views to show biomes as solid colors. + * Not required for gameplay, but helpful when testing generation. + */ + @Override + public Color getDebugBiomeColor() { + return new Color(128, 0, 128); + } + + // -------------------------- + // Ambient music + spawns + // -------------------------- + + /** + * Music selection for this biome. Here we reuse vanilla forest music. + * You can swap to a different MusicRegistry path if you want. + */ + @Override + public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { + return new MusicList(MusicRegistry.ForestPath); + } + + /** + * Critter spawns depend on whether the current level is a cave level. + */ + @Override + public MobSpawnTable getCritterSpawnTable(Level level) { + return level.isCave ? caveCritters : surfaceCritters; + } + + /** + * Mob spawns depend on whether the current level is a cave level. + */ + @Override + public MobSpawnTable getMobSpawnTable(Level level) { + return level.isCave ? caveMobs : surfaceMobs; + } +} diff --git a/src/main/java/examplemod/examples/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java similarity index 93% rename from src/main/java/examplemod/examples/ExampleIncursionBiome.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java index 6671f98..5a4d9d6 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.maps.incursion; import necesse.engine.network.server.Server; import necesse.engine.registries.ItemRegistry; @@ -26,13 +26,13 @@ public class ExampleIncursionBiome extends IncursionBiome { public ExampleIncursionBiome() { - super("reaper"); // The boss mob string ID for this incursion + super("examplebossmob"); // The boss mob string ID for this incursion } // Items required to be obtained when completing an extraction objective in this incursion @Override public Collection getExtractionItems(IncursionData data) { - return Collections.singleton(ItemRegistry.getItem("tungstenore")); + return Collections.singleton(ItemRegistry.getItem("exampleore")); } /** @@ -42,7 +42,7 @@ public Collection getExtractionItems(IncursionData data) { @Override public LootTable getHuntDrop(IncursionData incursionData) { return new LootTable( - new ChanceLootItem(0.66F, "examplehuntincursionitem") + new ChanceLootItem(0.66F, "examplehuntincursionmaterial") ); } diff --git a/src/main/java/examplemod/examples/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java similarity index 62% rename from src/main/java/examplemod/examples/ExampleIncursionLevel.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java index bfcecef..5118631 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java @@ -1,6 +1,7 @@ -package examplemod.examples; +package examplemod.examples.maps.incursion; import examplemod.ExampleMod; +import examplemod.examples.ExamplePreset; import necesse.engine.GameEvents; import necesse.engine.events.worldGeneration.GenerateCaveLayoutEvent; import necesse.engine.events.worldGeneration.GeneratedCaveOresEvent; @@ -15,6 +16,9 @@ import necesse.level.maps.incursion.BiomeExtractionIncursionData; import necesse.level.maps.incursion.BiomeMissionIncursionData; import necesse.level.maps.incursion.IncursionBiome; +import necesse.level.maps.presets.Preset; + +import java.awt.*; /** * Example incursion level. @@ -44,46 +48,69 @@ public ExampleIncursionLevel(LevelIdentifier identifier, BiomeMissionIncursionDa } public void generateLevel(BiomeMissionIncursionData incursionData, AltarData altarData) { - // Create the cave generator using deep rock tiles for floors and walls - CaveGeneration cg = new CaveGeneration(this, "deeprocktile", "deeprock"); - - // Seed the generator so this incursion layout is deterministic per mission + CaveGeneration cg = new CaveGeneration(this, "deeprocktile", "examplebaserock"); cg.random.setSeed(incursionData.getUniqueID()); - // Fire the cave layout generation event, allowing mods or perks to modify - // or cancel cave generation before the default logic runs GameEvents.triggerEvent( new GenerateCaveLayoutEvent(this, cg), - e -> { - cg.generateLevel(0.38F, 4, 3, 6); - } + e -> cg.generateLevel(0.38F, 4, 3, 6) ); - // Used to reserve space so later generation steps avoid overwriting the entrance + //entrance + perks (anything that must never be overwritten by perk presets) PresetGeneration entranceAndPerkPresets = new PresetGeneration(this); - // Generate an incursion entrance that clears terrain, - // blends edges, reserves space, and places the return portal - IncursionBiome.generateEntrance( + //your own structures (custom rooms, etc.) + PresetGeneration structurePresets = new PresetGeneration(this); + + // Generate entrance (this reserves space inside entranceAndPerkPresets) + int spawnSize = 32; + Point entranceMid = IncursionBiome.generateEntrance( this, entranceAndPerkPresets, cg.random, - 32, + spawnSize, cg.rockTile, "exampletile", "exampletile", "exampleobject" ); - // Now call incursion perks to generate their presets + // reserve the entrance space in structurePresets too, so your own structures don't overwrite the entrance area. + int ex = entranceMid.x - spawnSize / 2; + int ey = entranceMid.y - spawnSize / 2; + structurePresets.addOccupiedSpace(ex, ey, spawnSize, spawnSize); + + + //EXAMPLE PRESET + Preset examplePreset = new ExamplePreset(cg.random); + Point placedAt = structurePresets.findRandomValidPositionAndApply( + cg.random, + 2500,//realistically this would be lower if you didn't want it to be guaranteed + examplePreset, + 8, + true,// randomizeMirrorX + true, // randomizeMirrorY + true, // randomizeRotation + false // overrideCanPlace (false = respect canApply rules) + ); + + if (placedAt != null) { + structurePresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + entranceAndPerkPresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + } + + /* + Perk presets use entranceAndPerkPresets, so they avoid the entrance as well as any presets + you added to structurePresets + */ generatePresetsBasedOnPerks(altarData, entranceAndPerkPresets, cg.random, baseBiome); // This call clears all invalid objects/tiles, so that there are no cut in half beds, etc. GenerationTools.checkValid(this); - // For extraction incursions, guarantee tungsten ore veins for objectives + // For extraction incursions, guarantee example ore veins for objectives if (incursionData instanceof BiomeExtractionIncursionData) { - cg.generateGuaranteedOreVeins(40, 4, 8, ObjectRegistry.getObjectID("tungstenoredeeprock")); + cg.generateGuaranteedOreVeins(40, 4, 8, ObjectRegistry.getObjectID("exampleorerock")); } // Generate upgrade shard and alchemy shard ores cg.generateGuaranteedOreVeins(75, 6, 12, ObjectRegistry.getObjectID("upgradesharddeeprock")); diff --git a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java new file mode 100644 index 0000000..cab411e --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -0,0 +1,134 @@ +package examplemod.examples.mobs; + +import examplemod.examples.ai.ExampleAI; +import necesse.engine.eventStatusBars.EventStatusBarManager; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.sound.SoundManager; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.hostile.bosses.BossMob; +import necesse.entity.particle.FleshParticle; +import necesse.entity.particle.Particle; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptions; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.*; +import java.util.List; + +public class ExampleBossMob extends BossMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + public static LootTable lootTable = new LootTable( + ChanceLootItem.between(0.5f, "exampleitem", 1, 3) // 50% chance to drop between 1-3 example items + ); + + // MUST HAVE an empty constructor + public ExampleBossMob() { + super(200); + setSpeed(50); + setFriction(3); + this.shouldSave = false; + this.canDespawn = true; + this.isSummoned = true; + // Hitbox, collision box, and select box (for hovering) + collision = new Rectangle(-10, -7, 20, 14); + hitBox = new Rectangle(-14, -12, 28, 24); + selectBox = new Rectangle(-14, -7 - 34, 28, 48); + } + + @Override + public void init() { + super.init(); + // Setup AI + this.ai = new BehaviourTreeAI<>( + this, + new ExampleAI<>( + 1380, // search distance (in pixels) + new GameDamage(60), // collide damage + 150, // knockback + 12000 // wander frequency + ) + ); + } + + @Override + public LootTable getLootTable() { + return lootTable; + } + + @Override + public void spawnDeathParticles(float knockbackX, float knockbackY) { + // Spawn flesh debris particles + for (int i = 0; i < 4; i++) { + getLevel().entityManager.addParticle(new FleshParticle( + getLevel(), texture, + GameRandom.globalRandom.nextInt(5), // Randomize between the debris sprites + 8, + 32, + x, y, 20f, // Position + knockbackX, knockbackY // Basically start speed of the particles + ), Particle.GType.IMPORTANT_COSMETIC); + } + } + + @Override + protected void addDrawables(List list, OrderableDrawables tileList, OrderableDrawables topList, Level level, int x, int y, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + super.addDrawables(list, tileList, topList, level, x, y, tickManager, camera, perspective); + // Tile positions are basically level positions divided by 32. getTileX() does this for us etc. + GameLight light = level.getLightLevel(getTileX(), getTileY()); + int drawX = camera.getDrawX(x) - 32; + int drawY = camera.getDrawY(y) - 51; + + // A helper method to get the sprite of the current animation/direction of this mob + Point sprite = getAnimSprite(x, y, getDir()); + + drawY += getBobbing(x, y); + drawY += getLevel().getTile(getTileX(), getTileY()).getMobSinkingAmount(this); + + DrawOptions drawOptions = texture.initDraw() + .sprite(sprite.x, sprite.y, 64) + .light(light) + .pos(drawX, drawY); + + list.add(new MobDrawable() { + @Override + public void draw(TickManager tickManager) { + drawOptions.draw(); + } + }); + + addShadowDrawables(tileList, level, x, y, light, camera); + } + + @Override + public int getRockSpeed() { + // Change the speed at which this mobs animation plays + return 20; + } + + @Override + public void clientTick() { + super.clientTick(); + + // Only show boss bar when the client player is close enough + if (isClientPlayerNearby()) { + EventStatusBarManager.registerMobHealthStatusBar(this); + } + // Optional: set boss music here too if you want + SoundManager.setMusic(MusicRegistry.AscendedReturn, SoundManager.MusicPriority.EVENT, 1.5F); + } + + +} diff --git a/src/main/java/examplemod/examples/ExampleMob.java b/src/main/java/examplemod/examples/mobs/ExampleMob.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleMob.java rename to src/main/java/examplemod/examples/mobs/ExampleMob.java index c537747..c3151e4 100644 --- a/src/main/java/examplemod/examples/ExampleMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleMob.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.mobs; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.engine.util.GameRandom; diff --git a/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java b/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java new file mode 100644 index 0000000..781a1dc --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java @@ -0,0 +1,87 @@ +package examplemod.examples.mobs; + +import java.awt.*; +import java.util.List; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.ai.behaviourTree.trees.PlayerFollowerCollisionChaserAI; +import necesse.entity.mobs.summon.summonFollowingMob.attackingFollowingMob.AttackingFollowingMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptions; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +public class ExampleSummonWeaponMob extends AttackingFollowingMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + public ExampleSummonWeaponMob() { + super(20); // health + setSpeed(60.0F); + setFriction(2.0F); + this.attackCooldown = 500; + + this.collision = new Rectangle(-10, -7, 20, 14); + this.hitBox = new Rectangle(-12, -14, 24, 24); + this.selectBox = new Rectangle(-13, -14, 26, 24); + } + + @Override + public void init() { + super.init(); + + // Range, damage, knockback, cooldown etc. + // This uses this.summonDamage which SummonToolItem injects at spawn time. + this.ai = new BehaviourTreeAI<>(this, + new PlayerFollowerCollisionChaserAI<>( + 576, // target range + this.summonDamage, + 30, // knockback + 500, // attack windup? + 640, // chase range + 64 // give up / pathing distance + ) + ); + } + + @Override + protected void addDrawables(List list, OrderableDrawables tileList, OrderableDrawables topList, Level level, int x, int y, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + super.addDrawables(list, tileList, topList, level, x, y, tickManager, camera, perspective); + // Tile positions are basically level positions divided by 32. getTileX() does this for us etc. + GameLight light = level.getLightLevel(getTileX(), getTileY()); + int drawX = camera.getDrawX(x) - 32; + int drawY = camera.getDrawY(y) - 51; + + // A helper method to get the sprite of the current animation/direction of this mob + Point sprite = getAnimSprite(x, y, getDir()); + + drawY += getBobbing(x, y); + drawY += getLevel().getTile(getTileX(), getTileY()).getMobSinkingAmount(this); + + DrawOptions drawOptions = texture.initDraw() + .sprite(sprite.x, sprite.y, 64) + .light(light) + .pos(drawX, drawY); + + list.add(new MobDrawable() { + @Override + public void draw(TickManager tickManager) { + drawOptions.draw(); + } + }); + + addShadowDrawables(tileList, level, x, y, light, camera); + } + + @Override + public int getRockSpeed() { + // Change the speed at which this mobs animation plays + return 20; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java new file mode 100644 index 0000000..9379e67 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -0,0 +1,12 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; + +import java.awt.Color; + +public class ExampleBaseRockObject extends RockObject { + + public ExampleBaseRockObject() { + super("examplebaserock", new Color(92, 37, 23), "examplestone", "objects", "landscaping"); + this.toolTier = 5.0F; + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleGrassObject.java b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java new file mode 100644 index 0000000..22c1e17 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java @@ -0,0 +1,21 @@ +package examplemod.examples.objects; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GrassObject; +import necesse.level.maps.Level; + +public class ExampleGrassObject extends GrassObject { + + public ExampleGrassObject() { + // "examplegrass" is the texture name, 2 = variants/height setting used by GrassObject + super("examplegrass", 2); + } + + public LootTable getLootTable(Level level, int tileX, int tileY) { + // 4% chance, tweak to match vanilla feel + return new LootTable( + new ChanceLootItem(0.04F, "examplegrassseed") + ); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java new file mode 100644 index 0000000..51b5b83 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java @@ -0,0 +1,89 @@ +package examplemod.examples.objects; + +import examplemod.examples.ExampleObjectEntity; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.texture.TextureDrawOptionsEnd; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.gameObject.GameObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.Rectangle; +import java.util.List; + +/* + * Basic placeable demo object that: + * - draws a 32x32 sprite in the world + * - on interact (server side), spawns our ExampleLevelEvent + * - also triggers our custom ExampleEvent through GameEvents (so listeners can react) + */ +public class ExampleLevelEventObject extends GameObject { + + // Loaded once from mod resources in loadTextures() + private GameTexture texture; + + public ExampleLevelEventObject() { + //no physics shape + super(new Rectangle()); + this.isSolid = false; + } + + @Override + public void loadTextures() { + super.loadTextures(); + + // Loads: src/main/resources/objects/exampleleveleventobject.png + // (no ".png" in the string) + this.texture = GameTexture.fromFile("objects/exampleleveleventobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + + // Match sprite lighting to the level light at this tile + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coordinates to screen draw coordinates + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Build draw options once (sprite + lighting + position) + final TextureDrawOptionsEnd opts = this.texture.initDraw() + .sprite(0, 0, 32) // sprite index (0,0), size 32 + .light(light) + .pos(drawX, drawY); + + /* + */ + tileList.add(tm -> opts.draw()); + } + + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + + // Placement preview ("ghost" sprite) while holding the item + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + this.texture.initDraw() + .sprite(0, 0, 32) + .light(light) + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + return new ExampleObjectEntity(level, x, y); + } +} diff --git a/src/main/java/examplemod/examples/ExampleObject.java b/src/main/java/examplemod/examples/objects/ExampleObject.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleObject.java rename to src/main/java/examplemod/examples/objects/ExampleObject.java index 7ad35be..f611cfe 100644 --- a/src/main/java/examplemod/examples/ExampleObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleObject.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.objects; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; diff --git a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java new file mode 100644 index 0000000..6379944 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -0,0 +1,25 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; +import necesse.level.gameObject.RockOreObject; + +import java.awt.Color; + +/** + * Example ore rock that uses our ExampleIncursionDeepRockObject as its parent rock. + */ +public class ExampleOreRockObject extends RockOreObject { + + public ExampleOreRockObject(RockObject parentRock) { + super( + parentRock, + "oremask", + "exampleore", + new Color(90, 40, 160), + "exampleore", + 1, + 3, + 2, + true, + "objects", "landscaping"); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java new file mode 100644 index 0000000..ca7fae6 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java @@ -0,0 +1,27 @@ +package examplemod.examples.objects; + +import necesse.inventory.lootTable.LootTable; +import necesse.level.gameObject.TreeObject; +import necesse.level.maps.Level; + +import java.awt.*; + +public class ExampleTreeObject extends TreeObject { + public ExampleTreeObject(){ + super("exampletree", // textureName + "examplelog", // logStringID + "examplesapling", // saplingStringID + new Color(116, 69, 43), // mapColor + 45, // leavesCenterWidth + 60, // leavesMinHeight + 110, // leavesMaxHeight + "exampleleaves"); // leavesTextureName + } + + // Optional: override drops if you want something different than the base TreeObject default + // (base TreeObject drops 1-2 saplings + 4-5 logs, splitItems(5)) + @Override + public LootTable getLootTable(Level level, int layerID, int tileX, int tileY) { + return super.getLootTable(level, layerID, tileX, tileY); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java new file mode 100644 index 0000000..74c8d0b --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java @@ -0,0 +1,12 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.TreeSaplingObject; + +public class ExampleTreeSaplingObject extends TreeSaplingObject { + + public ExampleTreeSaplingObject(){ + super("examplesapling", "exampletree", 1800, 2700, true); + + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java new file mode 100644 index 0000000..2dea232 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java @@ -0,0 +1,38 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.WallObject; +import necesse.inventory.item.toolItem.ToolType; + +import java.awt.Color; + + + +public final class ExampleWallWindowDoorObject { + private ExampleWallWindowDoorObject() {} + + public static int EXAMPLEWALL; + public static int EXAMPLEDOOR; + public static int EXAMPLEDOOROPEN; + public static int EXAMPLEWINDOW; + + public static void registerWallsDoorsWindows() { + // registers wall, door pair and window objecs + int[] ids = WallObject.registerWallObjects( + "example",// prefix + "examplewall", // texture name + "walloutlines", // outline texture + 0.0F, // tool tier + new Color(255, 220, 80), + ToolType.PICKAXE, + -1.0F, // wall broker value + -1.0F, // door broker value + true, // obtainable + true // count in stats + ); + + EXAMPLEWALL = ids[0]; + EXAMPLEDOOR = ids[1]; + EXAMPLEDOOROPEN = ids[2]; + EXAMPLEWINDOW = ids[3]; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java new file mode 100644 index 0000000..eaf623f --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java @@ -0,0 +1,11 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.furniture.ChairObject; + +import java.awt.*; + +public class ExampleWoodChairObject extends ChairObject { + public ExampleWoodChairObject(){ + super("examplechair", new Color(116, 69, 43)); + } +} diff --git a/src/main/java/examplemod/examples/ExamplePacket.java b/src/main/java/examplemod/examples/packets/ExamplePacket.java similarity index 98% rename from src/main/java/examplemod/examples/ExamplePacket.java rename to src/main/java/examplemod/examples/packets/ExamplePacket.java index f0255c8..6264da2 100644 --- a/src/main/java/examplemod/examples/ExamplePacket.java +++ b/src/main/java/examplemod/examples/packets/ExamplePacket.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.packets; import necesse.engine.network.NetworkPacket; import necesse.engine.network.Packet; diff --git a/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java new file mode 100644 index 0000000..fe80682 --- /dev/null +++ b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java @@ -0,0 +1,45 @@ +package examplemod.examples.packets; + +import examplemod.ExampleMod; +import necesse.engine.network.NetworkPacket; +import necesse.engine.network.Packet; +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.client.Client; +import necesse.engine.sound.SoundEffect; + +/** + * SERVER -> CLIENT packet: + * Tells the receiving client to play the example sound at a specific world position (x, y). + */ + +public class ExamplePlaySoundPacket extends Packet { + public final float x; + public final float y; + + // Decode (CLIENT receiving) + public ExamplePlaySoundPacket(byte[] data) { + super(data); + PacketReader r = new PacketReader(this); + x = r.getNextFloat(); + y = r.getNextFloat(); + } + + // Encode (SERVER sending) + public ExamplePlaySoundPacket(float x, float y) { + this.x = x; + this.y = y; + + PacketWriter w = new PacketWriter(this); + w.putNextFloat(x); + w.putNextFloat(y); + } + + // Runs ONLY on client + @Override + public void processClient(NetworkPacket packet, Client client) { + if (ExampleMod.EXAMPLESOUNDSETTINGS != null) { + ExampleMod.EXAMPLESOUNDSETTINGS.play(SoundEffect.effect(x, y)); + } + } +} diff --git a/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java new file mode 100644 index 0000000..852c929 --- /dev/null +++ b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java @@ -0,0 +1,157 @@ +package examplemod.examples.projectiles; + +import java.awt.*; +import java.util.List; +import java.util.stream.Stream; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.network.NetworkClient; +import necesse.engine.util.GameRandom; +import necesse.engine.util.GameUtils; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.projectile.Projectile; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.texture.TextureDrawOptionsEnd; +import necesse.gfx.drawables.EntityDrawable; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.inventory.InventoryItem; +import necesse.level.maps.Level; +import necesse.level.maps.LevelObjectHit; +import necesse.level.maps.light.GameLight; + +public class ExampleArrowProjectile extends Projectile { + + + @Override + public void init() { + super.init(); + this.height = 18.0F; + this.heightBasedOnDistance = true; + setWidth(8.0F); + this.doesImpactDamage = false; + } + + @Override + public void addDrawables(List list, + OrderableDrawables tileList, OrderableDrawables topList, OrderableDrawables overlayList, + Level level, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + if (removed()) return; + + GameLight light = level.getLightLevel(this); + int drawX = camera.getDrawX(this.x) - this.texture.getWidth() / 2; + int drawY = camera.getDrawY(this.y); + + final TextureDrawOptionsEnd options = this.texture.initDraw() + .light(light) + .rotate(getAngle(), this.texture.getWidth() / 2, 0) + .pos(drawX, drawY - (int)getHeight()); + + list.add(new EntityDrawable(this) { + @Override + public void draw(TickManager tickManager) { + options.draw(); + } + }); + + // Shadow + addShadowDrawables(tileList, drawX, drawY, light, getAngle(), 0); + } + @Override + protected Stream streamTargets(Mob owner, Shape hitBounds) { + // The projectile calls this to ask: “Which mobs should I check collisions against this tick?” + + Level level = getLevel(); + // If we don’t have a level there’s nothing to test. + if (level == null || hitBounds == null) return Stream.empty(); + + // collect non player mobs + NetworkClient attackerClient = (owner == null) ? null : GameUtils.getAttackerClient(owner); + + // enemies/settlers/animals/etc) that are inside the projectile_attach hit area. + Stream mobs = level.entityManager.mobs + .streamInRegionsShape(hitBounds, 1) //query mobs in nearby regions + + // Is valid target logic + .filter(m -> owner == null || m.canBeTargeted(owner, attackerClient)); + + + // Collect players even if pvp is off + Stream players = level.entityManager.players + .streamInRegionsShape(hitBounds, 1) + + // Ignore null, the shooter, and players that were removed from the world. + .filter(p -> p != null && p != owner && !p.removed()) + + /* this makes sure the player has a network client + * the client has spawned and is not dead + * the playerMob exists and has a valid Level reference + */ + .filter(p -> { + NetworkClient c = p.getNetworkClient(); + return (c != null + && !c.isDead() + && c.hasSpawned() + && c.playerMob != null + && c.playerMob.getLevel() != null); + }); + + // Combine the two streams into one “things we can collide with” stream. + // PlayerMob is a subclass of Mob + return Stream.concat(mobs, players.map(p -> (Mob) p)); + } + + @Override + public boolean canHit(Mob mob) { + Mob owner = getOwner(); + + // Allow hitting allies + if (owner != null && (mob == owner || mob.isSameTeam(owner))) { + return true; + } + + // Otherwise use normal rules (enemies, etc.) + return super.canHit(mob); + } + + @Override + public void doHitLogic(Mob mob, LevelObjectHit object, float x, float y) { + super.doHitLogic(mob, object, x, y); + + if (!isServer() || mob == null) return; + + int durationMs = 4000; // 4 seconds regen + int healPerTick = 5; // healed every 250ms in the buff = 20 HP/sec + + // Try get existing buff + ActiveBuff existing = mob.buffManager.getBuff("examplearrowbuff"); + if (existing != null) { + // refresh duration + existing.setDurationLeft(durationMs); + + // optionally stack strength + int current = existing.getGndData().getInt("healPerTick"); + existing.getGndData().setInt("healPerTick", Math.max(current, healPerTick)); + + return; + } + + // create new + ActiveBuff regen = new ActiveBuff("examplearrowbuff", mob, durationMs, getOwner()); + regen.getGndData().setInt("healPerTick", healPerTick); + + // add the buff, send an update packet to clients and force update the buff + mob.buffManager.addBuff(regen, true,true,true); + } + + @Override + public void dropItem() { + // Optional: drop your arrow item sometimes, like vanilla StoneArrowProjectile does. + if (GameRandom.globalRandom.getChance(0.5F)) { + getLevel().entityManager.pickups.add(new InventoryItem("examplearrow").getPickupEntity(getLevel(), this.x, this.y) + ); + } + } +} diff --git a/src/main/java/examplemod/examples/ExampleProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleProjectile.java rename to src/main/java/examplemod/examples/projectiles/ExampleProjectile.java index 8e46ce8..3b247c0 100644 --- a/src/main/java/examplemod/examples/ExampleProjectile.java +++ b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.projectiles; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.GameDamage; diff --git a/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java new file mode 100644 index 0000000..d7f6c8b --- /dev/null +++ b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java @@ -0,0 +1,106 @@ +package examplemod.examples.tiles; + +import java.awt.Color; +import java.awt.Point; + +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.gfx.gameTexture.GameTextureSection; +import necesse.inventory.lootTable.LootItemInterface; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GameObject; +import necesse.level.gameTile.TerrainSplatterTile; +import necesse.level.maps.Level; +import necesse.level.maps.regionSystem.SimulatePriorityList; + +public class ExampleGrassTile extends TerrainSplatterTile { + // You can tweak these to change growth/spread speeds + public static double growChance = GameMath.getAverageSuccessRuns(7000.0D); + public static double spreadChance = GameMath.getAverageSuccessRuns(850.0D); + + private final GameRandom drawRandom = new GameRandom(); + + public ExampleGrassTile() { + // IMPORTANT: this string must match your texture name in resources/tiles/ + // (e.g. resources/tiles/examplegrass.png) + super(false, "examplegrasstile"); + + this.mapColor = new Color(70, 120, 40); // minimap color + this.canBeMined = true; + this.isOrganic = true; + } + + @Override + public LootTable getLootTable(Level level, int tileX, int tileY) { + // Option A: drop vanilla grassseed + // return new LootTable(new ChanceLootItem(0.04F, "grassseed")); + + // Option B: drop your own seed item (if you register one) + return new LootTable(new LootItemInterface[]{ + new ChanceLootItem(0.04F, "examplegrassseed") + }); + } + + @Override + public void addSimulateLogic(Level level, int x, int y, long ticks, SimulatePriorityList list, boolean sendChanges) { + addSimulateGrow(level, x, y, growChance, ticks, "examplegrass", list, sendChanges); + } + + // Same helper pattern vanilla uses (simplified) + public static void addSimulateGrow(Level level, int tileX, int tileY, double chance, long ticks, + String growObjectID, SimulatePriorityList list, boolean sendChanges) { + if (level.getObjectID(tileX, tileY) == 0) { + double runs = Math.max(1.0D, GameMath.getRunsForSuccess(chance, GameRandom.globalRandom.nextDouble())); + long remainingTicks = (long)(ticks - runs); + if (remainingTicks > 0L) { + GameObject obj = ObjectRegistry.getObject(ObjectRegistry.getObjectID(growObjectID)); + if (obj.canPlace(level, tileX, tileY, 0, false) == null) { + list.add(tileX, tileY, remainingTicks, () -> { + if (obj.canPlace(level, tileX, tileY, 0, false) == null) { + obj.placeObject(level, tileX, tileY, 0, false); + level.objectLayer.setIsPlayerPlaced(tileX, tileY, false); + if (sendChanges) level.sendObjectUpdatePacket(tileX, tileY); + } + }); + } + } + } + } + + @Override + public double spreadToDirtChance() { + // This is what makes dirt convert into your grass when adjacent + return spreadChance; + } + + @Override + public void tick(Level level, int x, int y) { + if (!level.isServer()) return; + + // Grow your grass object on empty tiles + if (level.getObjectID(x, y) == 0 && GameRandom.globalRandom.getChance(growChance)) { + GameObject grassObj = ObjectRegistry.getObject(ObjectRegistry.getObjectID("examplegrass")); + if (grassObj.canPlace(level, x, y, 0, false) == null) { + grassObj.placeObject(level, x, y, 0, false); + level.objectLayer.setIsPlayerPlaced(x, y, false); + level.sendObjectUpdatePacket(x, y); + } + } + } + + @Override + public Point getTerrainSprite(GameTextureSection terrainTexture, Level level, int tileX, int tileY) { + int tile; + synchronized (drawRandom) { + tile = drawRandom.seeded(getTileSeed(tileX, tileY)).nextInt(terrainTexture.getHeight() / 32); + } + return new Point(0, tile); // column 0, random row + } + + @Override + public int getTerrainPriority() { + return 100; // same as vanilla grass + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleTile.java b/src/main/java/examplemod/examples/tiles/ExampleTile.java similarity index 97% rename from src/main/java/examplemod/examples/ExampleTile.java rename to src/main/java/examplemod/examples/tiles/ExampleTile.java index 9189d36..2b981be 100644 --- a/src/main/java/examplemod/examples/ExampleTile.java +++ b/src/main/java/examplemod/examples/tiles/ExampleTile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.tiles; import necesse.engine.util.GameRandom; import necesse.gfx.gameTexture.GameTexture; diff --git a/src/main/resources/buffs/examplearmorsetbonus.png b/src/main/resources/buffs/examplearmorsetbonus.png new file mode 100644 index 0000000..b4edeee Binary files /dev/null and b/src/main/resources/buffs/examplearmorsetbonus.png differ diff --git a/src/main/resources/items/examplearrow.png b/src/main/resources/items/examplearrow.png new file mode 100644 index 0000000..8ad2093 Binary files /dev/null and b/src/main/resources/items/examplearrow.png differ diff --git a/src/main/resources/items/examplebar.png b/src/main/resources/items/examplebar.png new file mode 100644 index 0000000..f23f5a8 Binary files /dev/null and b/src/main/resources/items/examplebar.png differ diff --git a/src/main/resources/items/examplebaserock.png b/src/main/resources/items/examplebaserock.png new file mode 100644 index 0000000..358c39d Binary files /dev/null and b/src/main/resources/items/examplebaserock.png differ diff --git a/src/main/resources/items/exampleboots.png b/src/main/resources/items/exampleboots.png new file mode 100644 index 0000000..4f01a02 Binary files /dev/null and b/src/main/resources/items/exampleboots.png differ diff --git a/src/main/resources/items/examplechair.png b/src/main/resources/items/examplechair.png new file mode 100644 index 0000000..2c8293e Binary files /dev/null and b/src/main/resources/items/examplechair.png differ diff --git a/src/main/resources/items/examplechestplate.png b/src/main/resources/items/examplechestplate.png new file mode 100644 index 0000000..e129905 Binary files /dev/null and b/src/main/resources/items/examplechestplate.png differ diff --git a/src/main/resources/items/exampledoor.png b/src/main/resources/items/exampledoor.png new file mode 100644 index 0000000..911d60c Binary files /dev/null and b/src/main/resources/items/exampledoor.png differ diff --git a/src/main/resources/items/examplefood.png b/src/main/resources/items/examplefood.png new file mode 100644 index 0000000..1138bef Binary files /dev/null and b/src/main/resources/items/examplefood.png differ diff --git a/src/main/resources/items/examplefooditem.png b/src/main/resources/items/examplefooditem.png deleted file mode 100644 index d78f1c1..0000000 Binary files a/src/main/resources/items/examplefooditem.png and /dev/null differ diff --git a/src/main/resources/items/examplegrass.png b/src/main/resources/items/examplegrass.png new file mode 100644 index 0000000..1fba177 Binary files /dev/null and b/src/main/resources/items/examplegrass.png differ diff --git a/src/main/resources/items/examplegrassseed.png b/src/main/resources/items/examplegrassseed.png new file mode 100644 index 0000000..35103ba Binary files /dev/null and b/src/main/resources/items/examplegrassseed.png differ diff --git a/src/main/resources/items/examplehelmet.png b/src/main/resources/items/examplehelmet.png new file mode 100644 index 0000000..69341c6 Binary files /dev/null and b/src/main/resources/items/examplehelmet.png differ diff --git a/src/main/resources/items/examplehuntincursionitem.png b/src/main/resources/items/examplehuntincursionitem.png deleted file mode 100644 index d05149e..0000000 Binary files a/src/main/resources/items/examplehuntincursionitem.png and /dev/null differ diff --git a/src/main/resources/items/examplehuntincursionmaterial.png b/src/main/resources/items/examplehuntincursionmaterial.png new file mode 100644 index 0000000..bfd5c94 Binary files /dev/null and b/src/main/resources/items/examplehuntincursionmaterial.png differ diff --git a/src/main/resources/items/exampleincursiontablet.png b/src/main/resources/items/exampleincursiontablet.png index 979cfa6..7f5c3ce 100644 Binary files a/src/main/resources/items/exampleincursiontablet.png and b/src/main/resources/items/exampleincursiontablet.png differ diff --git a/src/main/resources/items/exampleleveleventobject.png b/src/main/resources/items/exampleleveleventobject.png new file mode 100644 index 0000000..957d455 Binary files /dev/null and b/src/main/resources/items/exampleleveleventobject.png differ diff --git a/src/main/resources/items/examplelog.png b/src/main/resources/items/examplelog.png new file mode 100644 index 0000000..c0f130b Binary files /dev/null and b/src/main/resources/items/examplelog.png differ diff --git a/src/main/resources/items/examplemagicstaff.png b/src/main/resources/items/examplemagicstaff.png new file mode 100644 index 0000000..eaebb5b Binary files /dev/null and b/src/main/resources/items/examplemagicstaff.png differ diff --git a/src/main/resources/items/examplemeleesword.png b/src/main/resources/items/examplemeleesword.png new file mode 100644 index 0000000..5e8aa5e Binary files /dev/null and b/src/main/resources/items/examplemeleesword.png differ diff --git a/src/main/resources/items/exampleore.png b/src/main/resources/items/exampleore.png new file mode 100644 index 0000000..d9433bd Binary files /dev/null and b/src/main/resources/items/exampleore.png differ diff --git a/src/main/resources/items/exampleorerock.png b/src/main/resources/items/exampleorerock.png new file mode 100644 index 0000000..358c39d Binary files /dev/null and b/src/main/resources/items/exampleorerock.png differ diff --git a/src/main/resources/items/examplepotion.png b/src/main/resources/items/examplepotion.png new file mode 100644 index 0000000..1164dc0 Binary files /dev/null and b/src/main/resources/items/examplepotion.png differ diff --git a/src/main/resources/items/examplepotionitem.png b/src/main/resources/items/examplepotionitem.png deleted file mode 100644 index d06e5b2..0000000 Binary files a/src/main/resources/items/examplepotionitem.png and /dev/null differ diff --git a/src/main/resources/items/examplerangedbow.png b/src/main/resources/items/examplerangedbow.png new file mode 100644 index 0000000..0310d0f Binary files /dev/null and b/src/main/resources/items/examplerangedbow.png differ diff --git a/src/main/resources/items/examplesapling.png b/src/main/resources/items/examplesapling.png new file mode 100644 index 0000000..7ba0488 Binary files /dev/null and b/src/main/resources/items/examplesapling.png differ diff --git a/src/main/resources/items/examplestaff.png b/src/main/resources/items/examplestaff.png deleted file mode 100644 index 40200b4..0000000 Binary files a/src/main/resources/items/examplestaff.png and /dev/null differ diff --git a/src/main/resources/items/examplestone.png b/src/main/resources/items/examplestone.png new file mode 100644 index 0000000..e8ce2a2 Binary files /dev/null and b/src/main/resources/items/examplestone.png differ diff --git a/src/main/resources/items/examplesummonorb.png b/src/main/resources/items/examplesummonorb.png new file mode 100644 index 0000000..d662c70 Binary files /dev/null and b/src/main/resources/items/examplesummonorb.png differ diff --git a/src/main/resources/items/examplesword.png b/src/main/resources/items/examplesword.png deleted file mode 100644 index 57758c1..0000000 Binary files a/src/main/resources/items/examplesword.png and /dev/null differ diff --git a/src/main/resources/items/exampletree.png b/src/main/resources/items/exampletree.png new file mode 100644 index 0000000..772af69 Binary files /dev/null and b/src/main/resources/items/exampletree.png differ diff --git a/src/main/resources/items/examplewall.png b/src/main/resources/items/examplewall.png new file mode 100644 index 0000000..2d82ccc Binary files /dev/null and b/src/main/resources/items/examplewall.png differ diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 3306996..57bb308 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -1,29 +1,60 @@ [tile] exampletile=Example Tile +examplegrasstile=Example Grass Tile [object] exampleobject=Example Object +exampleobject2 = Example Object Mod Category +examplebaserock=Example Rock +exampleore=Example Ore +examplesapling=Example Sapling +exampletree=Example Tree +examplegrass=Example Grass +examplewall=Example Wall +exampledoor=Example Door +examplechair=Example Chair +exampleleveleventobject=Example Level Event Object [item] exampleitem=Example Item -examplehuntincursionitem=Example Hunt Incursion Item -examplepotionitem=Example Potion -examplesword=Example Sword -examplestaff=Example Staff -examplefooditem=Example Food +examplestone=Example Stone +exampleore=Example Ore +examplebar=Example Bar +examplelog=Example Log +examplegrassseed=Example Grass Seed +examplehuntincursionmaterial=Example Hunt Incursion Material +examplepotion=Example Potion +examplefood=Example Food +examplemeleesword=Example Melee Weapon +examplemagicstaff=Example Magic Weapon +examplerangedbow= Example Ranged Weapon +examplesummonorb=Example Summon Weapon +examplearrow=Example Arrow +examplehelmet=Example Helmet +examplechestplate=Example Chestplate +exampleboots=Example Boots + [itemtooltip] -examplestafftip=Shoots a homing, piercing projectile +examplemagicstafftip=Shoots a homing, piercing projectile examplepotionitemtip= An example potion [mob] examplemob=Example Mob +examplebossmob=Example Boss +examplesummonmob=Example Summon Mob [buff] examplebuff=Example Buff +examplearmorsetbonus=Example Armor Set Bonus [biome] exampleincursion=Example Incursion [incursion] -exampleincursion=Example Incursion \ No newline at end of file +exampleincursion=Example Incursion + +[itemcategory] +examplemodrootcat=ExampleMod +examplemodobjectsubcat=ExampleMod Objects +examplemodfurnaturesubcat=ExampleMod Furnature diff --git a/src/main/resources/mobs/examplebossmob.png b/src/main/resources/mobs/examplebossmob.png new file mode 100644 index 0000000..e88afcb Binary files /dev/null and b/src/main/resources/mobs/examplebossmob.png differ diff --git a/src/main/resources/mobs/examplesummonmob.png b/src/main/resources/mobs/examplesummonmob.png new file mode 100644 index 0000000..3f85699 Binary files /dev/null and b/src/main/resources/mobs/examplesummonmob.png differ diff --git a/src/main/resources/mobs/icons/examplebossmob.png b/src/main/resources/mobs/icons/examplebossmob.png new file mode 100644 index 0000000..068bd48 Binary files /dev/null and b/src/main/resources/mobs/icons/examplebossmob.png differ diff --git a/src/main/resources/mobs/icons/examplemob.png b/src/main/resources/mobs/icons/examplemob.png new file mode 100644 index 0000000..33fdb61 Binary files /dev/null and b/src/main/resources/mobs/icons/examplemob.png differ diff --git a/src/main/resources/mobs/icons/examplesummonmob.png b/src/main/resources/mobs/icons/examplesummonmob.png new file mode 100644 index 0000000..14ad9f5 Binary files /dev/null and b/src/main/resources/mobs/icons/examplesummonmob.png differ diff --git a/src/main/resources/objects/examplebaserock.png b/src/main/resources/objects/examplebaserock.png new file mode 100644 index 0000000..1dd1765 Binary files /dev/null and b/src/main/resources/objects/examplebaserock.png differ diff --git a/src/main/resources/objects/examplechair.png b/src/main/resources/objects/examplechair.png new file mode 100644 index 0000000..24218ef Binary files /dev/null and b/src/main/resources/objects/examplechair.png differ diff --git a/src/main/resources/objects/examplegrass.png b/src/main/resources/objects/examplegrass.png new file mode 100644 index 0000000..0d241b8 Binary files /dev/null and b/src/main/resources/objects/examplegrass.png differ diff --git a/src/main/resources/objects/exampleleveleventobject.png b/src/main/resources/objects/exampleleveleventobject.png new file mode 100644 index 0000000..957d455 Binary files /dev/null and b/src/main/resources/objects/exampleleveleventobject.png differ diff --git a/src/main/resources/objects/exampleore.png b/src/main/resources/objects/exampleore.png new file mode 100644 index 0000000..d56047f Binary files /dev/null and b/src/main/resources/objects/exampleore.png differ diff --git a/src/main/resources/objects/examplesapling.png b/src/main/resources/objects/examplesapling.png new file mode 100644 index 0000000..25f306d Binary files /dev/null and b/src/main/resources/objects/examplesapling.png differ diff --git a/src/main/resources/objects/exampletree.png b/src/main/resources/objects/exampletree.png new file mode 100644 index 0000000..31c3476 Binary files /dev/null and b/src/main/resources/objects/exampletree.png differ diff --git a/src/main/resources/objects/examplewall.png b/src/main/resources/objects/examplewall.png new file mode 100644 index 0000000..a77d7c6 Binary files /dev/null and b/src/main/resources/objects/examplewall.png differ diff --git a/src/main/resources/particles/exampleleaves.png b/src/main/resources/particles/exampleleaves.png new file mode 100644 index 0000000..a33976f Binary files /dev/null and b/src/main/resources/particles/exampleleaves.png differ diff --git a/src/main/resources/player/armor/examplearms_left.png b/src/main/resources/player/armor/examplearms_left.png new file mode 100644 index 0000000..08da02e Binary files /dev/null and b/src/main/resources/player/armor/examplearms_left.png differ diff --git a/src/main/resources/player/armor/examplearms_right.png b/src/main/resources/player/armor/examplearms_right.png new file mode 100644 index 0000000..ba2aca1 Binary files /dev/null and b/src/main/resources/player/armor/examplearms_right.png differ diff --git a/src/main/resources/player/armor/exampleboots.png b/src/main/resources/player/armor/exampleboots.png new file mode 100644 index 0000000..f731a4f Binary files /dev/null and b/src/main/resources/player/armor/exampleboots.png differ diff --git a/src/main/resources/player/armor/examplechest.png b/src/main/resources/player/armor/examplechest.png new file mode 100644 index 0000000..bd8d737 Binary files /dev/null and b/src/main/resources/player/armor/examplechest.png differ diff --git a/src/main/resources/player/armor/examplehelmet.png b/src/main/resources/player/armor/examplehelmet.png new file mode 100644 index 0000000..bded8d4 Binary files /dev/null and b/src/main/resources/player/armor/examplehelmet.png differ diff --git a/src/main/resources/player/weapons/examplemagicstaff.png b/src/main/resources/player/weapons/examplemagicstaff.png new file mode 100644 index 0000000..0b2ed0c Binary files /dev/null and b/src/main/resources/player/weapons/examplemagicstaff.png differ diff --git a/src/main/resources/player/weapons/examplemeleesword.png b/src/main/resources/player/weapons/examplemeleesword.png new file mode 100644 index 0000000..be64290 Binary files /dev/null and b/src/main/resources/player/weapons/examplemeleesword.png differ diff --git a/src/main/resources/player/weapons/examplerangedbow.png b/src/main/resources/player/weapons/examplerangedbow.png new file mode 100644 index 0000000..09a87a4 Binary files /dev/null and b/src/main/resources/player/weapons/examplerangedbow.png differ diff --git a/src/main/resources/player/weapons/examplestaff.png b/src/main/resources/player/weapons/examplestaff.png deleted file mode 100644 index b277ac2..0000000 Binary files a/src/main/resources/player/weapons/examplestaff.png and /dev/null differ diff --git a/src/main/resources/player/weapons/examplesummonorb.png b/src/main/resources/player/weapons/examplesummonorb.png new file mode 100644 index 0000000..6aa88db Binary files /dev/null and b/src/main/resources/player/weapons/examplesummonorb.png differ diff --git a/src/main/resources/player/weapons/examplesword.png b/src/main/resources/player/weapons/examplesword.png deleted file mode 100644 index d2845b0..0000000 Binary files a/src/main/resources/player/weapons/examplesword.png and /dev/null differ diff --git a/src/main/resources/projectiles/examplearrowprojectile.png b/src/main/resources/projectiles/examplearrowprojectile.png new file mode 100644 index 0000000..8d6ad8e Binary files /dev/null and b/src/main/resources/projectiles/examplearrowprojectile.png differ diff --git a/src/main/resources/sound/examplesound.ogg b/src/main/resources/sound/examplesound.ogg new file mode 100644 index 0000000..08ebdb8 Binary files /dev/null and b/src/main/resources/sound/examplesound.ogg differ diff --git a/src/main/resources/tiles/examplegrasstile_splat.png b/src/main/resources/tiles/examplegrasstile_splat.png new file mode 100644 index 0000000..11981ea Binary files /dev/null and b/src/main/resources/tiles/examplegrasstile_splat.png differ