diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 6cc822c..3fa2470 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,126 +1,74 @@ 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.level.maps.biomes.Biome; +import necesse.engine.sound.gameSound.GameSound; @ModEntry public class ExampleMod { + // Global access point for mod settings + public static ExampleModSettings SETTINGS; + // We define our static registered objects here, so they can be referenced elsewhere public static ExampleBiome EXAMPLE_BIOME; + public static GameSound EXAMPLE_SOUND; + + // Load settings for the example mod from the external file defined in ExampleModSettings + public ExampleModSettings initSettings() { + SETTINGS = new ExampleModSettings(); + return SETTINGS; + } public void init() { System.out.println("Hello world from my example mod!"); + SETTINGS.logLoadedSettings(); // log the loaded settings for debug - // Register a simple biome that will not appear in natural world gen. - EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); + // Note: If you're using Intellij IDEA, you can ctrl+click the different references + // like "load()" to jump to their code and see how they work! - // Register the incursion biome with tier requirement 1. - IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); + // Register Tech Trees + ExampleModTech.load(); - // Register the level class used for the incursion. - LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); + // Register categories first: Used by Items/Objects to appear correctly in Creative/crafting trees + ExampleModCategories.load(); - // Register our tiles - TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + // Register packets early: Anything networked (mobs, settlers, job UIs, events) can safely reference packet IDs + ExampleModPackets.load(); - // Register our objects - ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + // Core content building blocks first: Tiles/Objects/Items are referenced by biomes, incursions, mobs, projectiles, buffs, etc. + ExampleModTiles.load(); + ExampleModObjects.load(); + ExampleModItems.load(); - // 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); + // Combat + entity registries next: Projectiles and buffs often reference items/mobs, and mobs can reference buffs/projectiles. + ExampleModProjectiles.load(); + ExampleModBuffs.load(); + ExampleModMobs.load(); - // Register our mob - MobRegistry.registerMob("examplemob", ExampleMob.class, true); + // Settlement systems after mobs/items exist: Settlers are mobs; jobs can reference settlers, items, and packets/UI. + ExampleModSettlers.load(); + ExampleModJobs.load(); - // Register our projectile - ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); + // World generation last-ish: Biomes/incursions can safely reference all registered tiles/objects/mobs/items now. + ExampleModBiomes.load(); + ExampleModIncursions.load(); - // Register our buff - BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + // Events after everything is registered: Lets event listeners safely reference IDs and content without ordering surprises. + ExampleModEvents.load(); - // Register our packet - PacketRegistry.registerPacket(ExamplePacket.class); + // Journal last: JournalEntry.addMobEntries() resolves MobRegistry immediately at registration time. + ExampleModJournal.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 - - // 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()); + // load our recipes from the ExampleRecipes class so we can keep this class easy to read + ExampleModRecipes.registerRecipes(); } } diff --git a/src/main/java/examplemod/ExampleModSettings.java b/src/main/java/examplemod/ExampleModSettings.java new file mode 100644 index 0000000..69627f9 --- /dev/null +++ b/src/main/java/examplemod/ExampleModSettings.java @@ -0,0 +1,39 @@ +package examplemod; + +import necesse.engine.GameLog; +import necesse.engine.modLoader.ModSettings; +import necesse.engine.save.LoadData; +import necesse.engine.save.SaveData; + +public class ExampleModSettings extends ModSettings { + + // Your config values + public boolean exampleBoolean = true; + public int exampleInt = 1; + public String exampleString = "Hello! from the config file "; + + @Override + public void addSaveData(SaveData data) { + // This is what gets written to cfg/mods/.cfg under SETTINGS { ... } + data.addBoolean("exampleBoolean", exampleBoolean); + data.addInt("exampleInt", exampleInt); + data.addSafeString("exampleString", exampleString); + } + + @Override + public void applyLoadData(LoadData data) { + // This is what gets read back from cfg/mods/.cfg + exampleBoolean = data.getBoolean("exampleBoolean", exampleBoolean); + exampleInt = data.getInt("exampleInt", exampleInt); + // If print warning is false, it won't print a warning if the data is not found and the default value is used + exampleString = data.getSafeString("exampleString", exampleString, false); + } + + public void logLoadedSettings() { + GameLog.out.println("[ExampleMod] Settings loaded:"); + GameLog.out.println(" exampleBoolean = " + exampleBoolean); + GameLog.out.println(" exampleInt = " + exampleInt); + GameLog.out.println(" exampleString = \"" + exampleString + "\""); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModBiomes.java b/src/main/java/examplemod/Loaders/ExampleModBiomes.java new file mode 100644 index 0000000..493f049 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBiomes.java @@ -0,0 +1,17 @@ +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..47a4646 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -0,0 +1,28 @@ +package examplemod.Loaders; + +import examplemod.examples.buffs.ExampleArmorSetBuff; +import examplemod.examples.buffs.ExampleArrowBuff; +import examplemod.examples.buffs.ExampleBuff; +import examplemod.examples.buffs.ExampleTrinketBuff; +import necesse.engine.registries.BuffRegistry; + +public class ExampleModBuffs { + + // We store our example arrow buff variable for later use + public static ExampleArrowBuff EXAMPLE_ARROW_BUFF; + + public static void load() { + // Register our buff + BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + + // Register our armor set bonus, used in ExampleHelmetArmorItem + BuffRegistry.registerBuff("examplearmorsetbonusbuff", new ExampleArmorSetBuff()); + + // Register our Arrow Buff + EXAMPLE_ARROW_BUFF = BuffRegistry.registerBuff("examplearrowbuff", new ExampleArrowBuff()); + + // Register our Trinket Buff + BuffRegistry.registerBuff("exampletrinketbuff",new ExampleTrinketBuff()); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java new file mode 100644 index 0000000..b2f00c4 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -0,0 +1,50 @@ +package examplemod.Loaders; + +import necesse.engine.localization.message.LocalMessage; +import necesse.inventory.item.ItemCategory; + +public class ExampleModCategories { + + public static void load() { + // Here we register our example item categories + // The first parameter is the sorting string. It allows us to define exactly where we + // want our category to be displayed. Works like this: + // The string is divided up between the hyphens (-). Then each section is alphabetically compared to the + // other categories corresponding section. This makes it so it's always possible to insert a category in + // between two existing categories no matter what sorting string they have. + + // You can see the existing base game categories here: + /// {@link necesse.inventory.item.ItemCategory} + + // ITEM CATEGORIES + ItemCategory.createCategory( + "BA-A-A", // We want it to be sorted just after the consumable root category + new LocalMessage("itemcategory", "examplemod"), + "examplemod" + ); + + ItemCategory.createCategory( + "BA-A-A-SUB", + new LocalMessage("itemcategory", "examplemodsub"), + "examplemod", "sub" + ); + + // If we want the category to appear in the placeables creative menu, we can do so by + // adding the root category to the list like this: +// CreativeMenuForm.placeablesTabMasterCategories.add("examplemod"); + + // CRAFTING CATEGORIES + // These categories are used in workstations. They define the order in which the categories are shown + ItemCategory.craftingManager.createCategory( + "BA-A-A", + new LocalMessage("itemcategory", "examplemod"), + "examplemod" + ); + + ItemCategory.craftingManager.createCategory( + "BA-A-A-SUB", + new LocalMessage("itemcategory", "examplemodsub"), + "examplemod", "sub" + ); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCommands.java b/src/main/java/examplemod/Loaders/ExampleModCommands.java new file mode 100644 index 0000000..3754142 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCommands.java @@ -0,0 +1,13 @@ +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..4b153b1 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModEvents.java @@ -0,0 +1,16 @@ +package examplemod.Loaders; + +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.registries.LevelEventRegistry; + +public class ExampleModEvents { + + public static void load() { + // Register our Level Event to the registry + LevelEventRegistry.registerEvent("examplelevelevent", ExampleLevelEvent.class); + } + +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModIncursions.java b/src/main/java/examplemod/Loaders/ExampleModIncursions.java new file mode 100644 index 0000000..53c5019 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModIncursions.java @@ -0,0 +1,19 @@ +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..1edb191 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -0,0 +1,52 @@ +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.ExampleBossSummonItem; +import examplemod.examples.items.consumable.ExampleFoodItem; +import examplemod.examples.items.consumable.ExamplePotionItem; +import examplemod.examples.items.materials.*; +import examplemod.examples.items.tools.ExampleBowRangedWeaponItem; +import examplemod.examples.items.tools.ExampleOrbSummonWeaponItem; +import examplemod.examples.items.tools.ExampleStaffMagicWeaponItem; +import examplemod.examples.items.tools.ExampleSwordMeleeWeaponItem; +import examplemod.examples.items.trinkets.ExampleTrinketItem; +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("examplelog", new ExampleLogItem(), 10, true); + ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(), 1, true); + + // Tools + ItemRegistry.registerItem("examplemeleesword", new ExampleSwordMeleeWeaponItem(), 20, true); + ItemRegistry.registerItem("examplerangedbow", new ExampleBowRangedWeaponItem(), 10, true); + ItemRegistry.registerItem("examplemagicstaff", new ExampleStaffMagicWeaponItem(), 30, true); + ItemRegistry.registerItem("examplesummonorb", new ExampleOrbSummonWeaponItem(), 40, true); + + // Armor + ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200, true); + ItemRegistry.registerItem("examplechestplate", new ExampleChestArmorItem(), 250, true); + ItemRegistry.registerItem("exampleboots", new ExampleBootsArmorItem(), 180, true); + + // Trinkets + ItemRegistry.registerItem("exampletrinket", new ExampleTrinketItem(), 5, true); + + // Consumables + ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); + ItemRegistry.registerItem("examplefood", new ExampleFoodItem(), 15, true); + ItemRegistry.registerItem("examplebosssummonitem", new ExampleBossSummonItem(), 1, true); + + // Ammo + ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(), 5, true); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModJobs.java b/src/main/java/examplemod/Loaders/ExampleModJobs.java new file mode 100644 index 0000000..947f7cc --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModJobs.java @@ -0,0 +1,26 @@ +package examplemod.Loaders; + +import examplemod.examples.settlement.jobs.ExampleLevelJob; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.registries.JobTypeRegistry; +import necesse.engine.registries.LevelJobRegistry; +import necesse.entity.mobs.job.JobType; + +public class ExampleModJobs { + + public static void load() { + // 1) Register the job type + JobTypeRegistry.registerType("examplejobtype", + new JobType( + true, // canChangePriority (shows in settlement UI) + true, // defaultDisabledBySettler (locked for normal settlers) + new LocalMessage("jobs", "examplejobname"), + new LocalMessage("jobs", "examplejobtip") + ) + ); + + // 2) Register our ExampleLevelJob //DEBUG + LevelJobRegistry.registerJob("examplejob", ExampleLevelJob .class, ExampleLevelJob::handler, "examplejobtype"); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModJournal.java b/src/main/java/examplemod/Loaders/ExampleModJournal.java new file mode 100644 index 0000000..650cc61 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModJournal.java @@ -0,0 +1,40 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import necesse.engine.journal.JournalEntry; +import necesse.engine.registries.JournalRegistry; +import necesse.engine.util.LevelIdentifier; + + +public class ExampleModJournal { + + public static void load() { + // Surface + JournalEntry exampleBiomeJournalSurface = JournalRegistry.registerJournalEntry( + "examplebiomesurface", + new JournalEntry(ExampleMod.EXAMPLE_BIOME, LevelIdentifier.SURFACE_IDENTIFIER) + ); + // Content lists inside the journal page + exampleBiomeJournalSurface.addBiomeLootEntry("examplelog"); + exampleBiomeJournalSurface.addMobEntries("examplemob"); + + // Caves + JournalEntry exampleBiomeJournalCave = JournalRegistry.registerJournalEntry( + "examplebiomecave", + new JournalEntry(ExampleMod.EXAMPLE_BIOME, LevelIdentifier.CAVE_IDENTIFIER) + ); + // Content lists inside the journal page + exampleBiomeJournalCave.addBiomeLootEntry("exampleore","examplestone"); + exampleBiomeJournalCave.addMobEntries("examplemob"); + + // Deep Caves + JournalEntry exampleBiomeJournalDeepCave = JournalRegistry.registerJournalEntry( + "examplebiomedeepcave", + new JournalEntry(ExampleMod.EXAMPLE_BIOME, LevelIdentifier.DEEP_CAVE_IDENTIFIER) + ); + // Content lists inside the journal page + exampleBiomeJournalDeepCave.addBiomeLootEntry("exampleore","examplestone"); + exampleBiomeJournalDeepCave.addMobEntries("examplemob"); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java new file mode 100644 index 0000000..c210c56 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -0,0 +1,29 @@ +package examplemod.Loaders; + +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleHumanMob; +import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; +import necesse.engine.registries.MobRegistry; + +public class ExampleModMobs { + + public static void load() { + // Register base example mob + MobRegistry.registerMob( + "examplemob", // The stringID of the mob + ExampleMob.class, // The mob class which contains the empty constructor + true // If the mob can be killed or not, and should count in player stats + ); + + // Register boss mob. This tile we also add isBossMob parameter and set that to true + MobRegistry.registerMob("exampleboss", ExampleBossMob.class,true,true); + + // Register summon weapon mob. This time we set coundKillStat to false, because you cannot kill this mob + MobRegistry.registerMob("examplesummon", ExampleSummonWeaponMob.class, false); + + // Register example human mob (ExampleHumanMob that uses ExampleSettler for settler settings and is capable of our ExampleLevelJob) + MobRegistry.registerMob("examplehuman", ExampleHumanMob.class, true); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java new file mode 100644 index 0000000..2cac0ba --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -0,0 +1,93 @@ +package examplemod.Loaders; + +import examplemod.examples.objects.*; +import necesse.engine.registries.ObjectRegistry; +import necesse.inventory.item.toolItem.ToolType; +import necesse.level.gameObject.WallObject; +import necesse.level.maps.presets.set.ChestRoomSet; +import necesse.level.maps.presets.set.ColumnSet; +import necesse.level.maps.presets.set.WallSet; + +import java.awt.*; + +public class ExampleModObjects { + + // Expose IDs for other classes (biomes, levels, etc.) + public static int EXAMPLE_BASE_ROCK_ID = -1; + public static int EXAMPLE_ORE_ROCK_ID = -1; + + // Wall and chest room sets are used for generating presets, etc. This will be set later in the load method + public static WallSet EXAMPLE_WALL_SET = null; + public static ChestRoomSet EXAMPLE_CHEST_ROOM_SET = null; + + public static void load() { + // Register our objects + + ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + + + // Register a rock object + ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); + // If you give a negative value as broker value, the game will calculate the broker value based on the recipe for this item's ingredients + // -1 will be 1*ingredient cost, -2 will be 2 * ingredient cost, etc. + EXAMPLE_BASE_ROCK_ID = ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1f, true); + + // Register an ore rock object that overlays onto our incursion rock + EXAMPLE_ORE_ROCK_ID = ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1f, true); + + // Register a wall object, window object and door object + WallObject.registerWallObjects( + "example", // Prefix used for stringIDs + "examplewall", // Texture name + 0, // Tool tier + new Color(255, 220, 80), // Map color + ToolType.PICKAXE, // Tool type used to mine it + -1f, // Wall broker value + -1f, // Door broker value + true // Obtainable + ); + // If you need the ids, registerWallObjects will return an array with the wall, door, door open and window ids + // in that order. You can also fetch them later with ObjectRegistry.getID("examplewall"), etc. + + // Register a tree object + ObjectRegistry.registerObject("exampletree",new ExampleTreeObject(),0,false,false,true); + + // Register a sapling object + ObjectRegistry.registerObject("examplesapling", new ExampleTreeSaplingObject(),10,true); + + // Register a grass object + ObjectRegistry.registerObject("examplegrass",new ExampleGrassObject(),1,true); + + // Register an object which uses a level event + ObjectRegistry.registerObject("exampleeventtriggerobject", new ExampleEventTriggerObject(),1,true); + + // Register ExampleJobObject an object that triggers our new job to happen //DEBUG + ObjectRegistry.registerObject("examplejobobject",new ExampleJobObject(),1,true); + + // Register an object that uses the mods config file + ObjectRegistry.registerObject("exampleconfigobject", new ExampleConfigObject(),1,true); + + // Register an example pressure plate object + ObjectRegistry.registerObject("examplepressureplate",new ExamplePressurePlateObject(),1,true); + + // Get the wall object we want this trap to attach to. + // ObjectRegistry stores everything as a generic "GameObject" + // so we fetch by string ID ("examplewall") and cast it to WallObject. + // Takes the texture of the wall object and overlays our "examplewalltrap" + WallObject exampleWall = (WallObject) ObjectRegistry.getObject("examplewall"); + ObjectRegistry.registerObject("examplewalltrap",new ExampleWallTrapObject(exampleWall),1,true); + EXAMPLE_WALL_SET = new WallSet("example"); + EXAMPLE_CHEST_ROOM_SET = new ChestRoomSet( + "exampletile", // Our tile stringID + "examplepressureplate", // Our pressure plate stringID + EXAMPLE_WALL_SET, // Our wall set + ColumnSet.wood, // For columns, we just use wood + "storagebox", // Normal storage box as chest + "examplewalltrap" // Our wall trap stringID + ); + + // Register example workstation + ExampleWorkstationObject.register(); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModPackets.java b/src/main/java/examplemod/Loaders/ExampleModPackets.java new file mode 100644 index 0000000..31ae03b --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModPackets.java @@ -0,0 +1,13 @@ +package examplemod.Loaders; + +import examplemod.examples.packets.ExamplePacket; +import necesse.engine.registries.PacketRegistry; + +public class ExampleModPackets { + + public static void load() { + // Register our packets. In this case we only have one + PacketRegistry.registerPacket(ExamplePacket.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..056522a --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java @@ -0,0 +1,17 @@ +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..cbc2b55 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -0,0 +1,256 @@ +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( + "examplerangedbow", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplelog", 8), + new Ingredient("examplebar", 2), + new Ingredient("exampleitem", 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( + "examplearrow", + 25, // 25 arrows per craft + RecipeTechRegistry.WORKSTATION, + new Ingredient[] { + new Ingredient("examplelog", 1), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplepressureplate", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[] { + new Ingredient("examplestone", 6), + new Ingredient("examplebar", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleworkstation", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[] { + new Ingredient("examplelog", 20), + new Ingredient("examplebar", 8) + } + )); + + + + // 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), + } + )); + + // EXAMPLE TECH RECIPES + Recipes.registerModRecipe(new Recipe( + "exampleconfigobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[] { + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplejobobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[] { + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleeventtriggerobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[] { + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[] { + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplebosssummonitem", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[] { + new Ingredient("examplestone", 10), + new Ingredient("examplelog", 10), + new Ingredient("exampleitem", 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..279bde5 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -0,0 +1,27 @@ +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.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"); + + //initializing the sound to be used by our boss mob + ExampleMod.EXAMPLE_SOUND = GameSound.fromFile("examplesound"); + } + +} + diff --git a/src/main/java/examplemod/Loaders/ExampleModSettlers.java b/src/main/java/examplemod/Loaders/ExampleModSettlers.java new file mode 100644 index 0000000..f79b2ba --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModSettlers.java @@ -0,0 +1,14 @@ +package examplemod.Loaders; + +import examplemod.examples.settlement.settlers.ExampleSettler; +import necesse.engine.registries.SettlerRegistry; + + +public class ExampleModSettlers { + + public static void load() { + // Register our settler used by ExampleHumanMob + SettlerRegistry.registerSettler("examplesettler", new ExampleSettler()); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModTech.java b/src/main/java/examplemod/Loaders/ExampleModTech.java new file mode 100644 index 0000000..ddfca80 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTech.java @@ -0,0 +1,25 @@ +package examplemod.Loaders; + +import necesse.engine.registries.RecipeTechRegistry; +import necesse.inventory.recipe.Tech; + +public class ExampleModTech { + + public static Tech EXAMPLE_TECH; + + public static void load() { + // All recipes have some tech that they are assigned to + // Crafting stations then define which techs that can be crafted there + // Even the forge is also looking at recipes for the forge tech + + // Here we register our own tech for our example crafting stations + + // stringID: how recipes refer to it internally + // itemStringID: used for icon/tooltips (usually your crafting station item id) + EXAMPLE_TECH = RecipeTechRegistry.registerTech("exampletech", "exampleworkstation"); + + // Remember to also add the tech to your locale file. The name of the tech will be + // shown in the crafting guide book, etc. (This is already done in this example) + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java new file mode 100644 index 0000000..c7f11b5 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -0,0 +1,18 @@ +package examplemod.Loaders; + +import examplemod.examples.tiles.ExampleGrassTile; +import examplemod.examples.tiles.ExampleTile; +import necesse.engine.registries.TileRegistry; + +public class ExampleModTiles { + + public static int EXAMPLE_TILE_ID; + public static int EXAMPLE_GRASS_TILE_ID; + + public static void load() { + // Register our tiles + EXAMPLE_TILE_ID = TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + EXAMPLE_GRASS_TILE_ID = 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/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java new file mode 100644 index 0000000..7921049 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -0,0 +1,58 @@ +package examplemod.examples; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.*; + +/** + * 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 or ChanceLootItemList) + * - groups like "pick one of these" (OneOfLootItems) + * Or any custom implementation of LootItemInterface + */ + public static final LootTable exampleLootTable = new LootTable( + + // Rotating entries: + // This uses the (level + AtomicInteger lootRotation) arguments that chest rooms pass in. + // If it does not get the correct arguments, it will just generate a random one in the list + RotationLootItem.presetRotation( + new LootItem("exampletrinket"), + new LootItem("examplehelmet"), + new LootItem("examplechestplate"), + new LootItem("exampleboots") + ), + // Guaranteed drops: + // LootItem(String itemStringID, int amount) + // These are always added when the table is rolled. + LootItem.between("examplebar", 2, 4), // Between 2 and 4 example bar + new LootItem("examplepotion"), // Just one potion + + // 60% chance for a single example food item + new ChanceLootItem(0.6f, "examplefood"), + + // Next, a 50% chance to generate a OneOfLootItems + // OneOfLootItems will pick ONE option from the list + new ChanceLootItemList(0.5f, new OneOfLootItems( + new LootItem("examplemeleesword"), + new LootItem("examplemagicstaff"), + new LootItem("examplesummonorb"), + new LootItem("examplerangedbow") + )) + ); + + /** + * 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/ExampleObject.java b/src/main/java/examplemod/examples/ExampleObject.java deleted file mode 100644 index 7ad35be..0000000 --- a/src/main/java/examplemod/examples/ExampleObject.java +++ /dev/null @@ -1,75 +0,0 @@ -package examplemod.examples; - -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.TextureDrawOptions; -import necesse.gfx.drawables.LevelSortedDrawable; -import necesse.gfx.drawables.OrderableDrawables; -import necesse.gfx.gameTexture.GameTexture; -import necesse.inventory.item.toolItem.ToolType; -import necesse.level.gameObject.GameObject; -import necesse.level.maps.Level; -import necesse.level.maps.light.GameLight; - -import java.awt.*; -import java.util.List; - -public class ExampleObject extends GameObject { - - private GameTexture texture; - - public ExampleObject() { - super(new Rectangle(4, 4, 26, 26)); // Collision relative to the tile it's placed on - // Remember that tiles are 32x32 pixels in size - hoverHitbox = new Rectangle(0, -32, 32, 64); // 2 tiles high mouse hover hitbox - toolType = ToolType.ALL; // Can be broken by all tools - isLightTransparent = true; // Lets light pass through - mapColor = new Color(31, 150, 148); // Also applies as debris color if not set - } - - @Override - public void loadTextures() { - super.loadTextures(); - texture = GameTexture.fromFile("objects/exampleobject"); - } - - @Override - public void addDrawables(List list, OrderableDrawables tileList, Level level, int tileX, int tileY, TickManager tickManager, GameCamera camera, PlayerMob perspective) { - GameLight light = level.getLightLevel(tileX, tileY); - int drawX = camera.getTileDrawX(tileX); - int drawY = camera.getTileDrawY(tileY); - // Use the rotation if you have rotation on your object -// int rotation = level.getObjectRotation(tileX, tileY); - TextureDrawOptions options = texture.initDraw().light(light).pos(drawX, drawY - texture.getHeight() + 32); - // Can choose sprite with texture.initDraw().sprite(...) - - list.add(new LevelSortedDrawable(this, tileX, tileY) { - @Override - public int getSortY() { - // Basically where this will be sorted on the Y axis (when it will be behind the player etc.) - // Should be in [0 - 32] range - return 16; - } - - @Override - public void draw(TickManager tickManager) { - options.draw(); - } - }); - } - - @Override - public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, PlayerMob player, GameCamera camera) { - int drawX = camera.getTileDrawX(tileX); - int drawY = camera.getTileDrawY(tileY); - texture.initDraw().alpha(alpha).draw(drawX, drawY - texture.getHeight() + 32); - } - - @Override - public ObjectEntity getNewObjectEntity(Level level, int x, int y) { - // If this object has an object entity, return something else - return null; - } -} diff --git a/src/main/java/examplemod/examples/ExampleSwordItem.java b/src/main/java/examplemod/examples/ExampleSwordItem.java deleted file mode 100644 index 68fc649..0000000 --- a/src/main/java/examplemod/examples/ExampleSwordItem.java +++ /dev/null @@ -1,21 +0,0 @@ -package examplemod.examples; - -import necesse.inventory.item.Item; -import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; - -// Extends SwordToolItem -public class ExampleSwordItem extends SwordToolItem { - - // Weapon attack textures are loaded from resources/player/weapons/ - - public ExampleSwordItem() { - super(400, null); - rarity = Item.Rarity.UNCOMMON; - attackAnimTime.setBaseValue(300); // 300 ms attack time - attackDamage.setBaseValue(20) // Base sword damage - .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/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java new file mode 100644 index 0000000..c631e13 --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -0,0 +1,49 @@ +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.decorators.InverterAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; +import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; + +// Extends the SelectorAINode class, which basically is an "OR" parent. Specifically, it does this: +// Run child #1, if it returns SUCCESS then stop and return SUCCESS. +// If it returns FAILURE, run the next child until finding one that returns SUCCESS +public abstract class ExampleAI extends SelectorAINode { + + // We store the different child nodes in variables, so that we can easily access them later if needed + + // Plays a sound when then boss appears + public final ExampleAINode soundPlay; + + // AI that does: find target -> chase -> when colliding with the target, call attackTarget(). + // In this case, attackTarget call simply damages the target. This can be overridden for something custom. + public final CollisionPlayerChaserAI chaser; + + // “walk around randomly” node. This is what happens when there’s no target to chase. + public final WandererAINode wanderer; + + public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { + // This AI is pretty similar to CollisionPlayerChaserWandererAI, + // but with added teleport mechanic and no escape node. + + // 1) Teleport / reposition leaf (highest priority). + // Since it always returns SUCCESS, we use inverter node to invert it to FAILURE + addChild(new InverterAINode<>(soundPlay = new ExampleAINode() { + @Override + public boolean teleport(T mob, int x, int y) { + return ExampleAI.this.teleport(mob, x, y); + } + })); + + // 2) Chase + attack (second priority). + addChild(chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback)); + + // 3) Wander around if we aren’t teleporting, and we aren’t chasing anyone (last priority) + addChild(wanderer = new WandererAINode<>(wanderFrequency)); + } + + public abstract boolean teleport(T mob, int x, int y); + +} diff --git a/src/main/java/examplemod/examples/ai/ExampleAINode.java b/src/main/java/examplemod/examples/ai/ExampleAINode.java new file mode 100644 index 0000000..086cb1a --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAINode.java @@ -0,0 +1,92 @@ +package examplemod.examples.ai; + +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.entity.mobs.ai.behaviourTree.event.AIEvent; +import necesse.entity.projectile.Projectile; + +import java.awt.*; +import java.util.ArrayList; + +/** + * This is essentially a recreation of existing "TeleportOnProjectileHitAINode" class. + * Simply to explain and comment a bit more on what's going on for learning purposes :) + * Always returns SUCCESS + */ +public abstract class ExampleAINode extends AINode { + + // The next time the mob can teleport away + protected long nextTeleportTime; + + protected int tileRadius = 5; + + @Override + protected void onRootSet(AINode root, T mob, Blackboard blackboard) { + // Runs exactly one time when this node was added to an AI + blackboard.onBeforeHit(e -> { + if (mob.isClient()) return; + // If hit by a projectile, not on cooldown, and we teleported to a new position + if (e.event.attacker instanceof Projectile && nextTeleportTime <= mob.getTime() && findNewPosition(mob)) { + // Prevent the hit and don't show any damage number or play hit sound + e.event.prevent(); + e.event.showDamageTip = false; + e.event.playHitSound = false; + nextTeleportTime = mob.getTime() + 10000; // 10 seconds cooldown + // We submit a path reset event to all other AI nodes + blackboard.submitEvent("resetPathTime", new AIEvent()); + } + }); + } + + public boolean findNewPosition(T mob) { + // First we get the center tile of the mob + int tileX = mob.getTileX(); + int tileY = mob.getTileY(); + // Move offset is used for larger mobs, to get the center position of + // where this mob should move relative to a target tile + Point moveOffset = mob.getPathMoveOffset(); + + // Next we iterate over all tiles in the defined tile radius + ArrayList possiblePositions = new ArrayList<>(); + for (int x = tileX - tileRadius; x <= tileX + tileRadius; x++) { + for (int y = tileY - tileRadius; y <= tileY + tileRadius; y++) { + int mobX = x * 32 + moveOffset.x; + int mobY = y * 32 + moveOffset.y; + // If the mob does not collide with the level at this position, + // we add it to the list of possible teleport positions + if (!mob.collidesWith(mob.getLevel(), mobX, mobY)) { + possiblePositions.add(new Point(mobX, mobY)); + } + } + } + + // Next we randomly select a position from the list of possible positions and attempt to teleport there + while (!possiblePositions.isEmpty()) { + int index = GameRandom.globalRandom.nextInt(possiblePositions.size()); + Point point = possiblePositions.get(index); + if (teleport(mob, point.x, point.y)) { + return true; + } + } + + // If we didn't find a position, return false + return false; + } + + public abstract boolean teleport(T mob, int x, int y); + + @Override + public void init(T mob, Blackboard blackboard) { + // Runs every tick when running the parent AI node. + // Here we can reset something that is running in tick method, if we want to. + } + + @Override + public AINodeResult tick(T mob, Blackboard blackboard) { + return AINodeResult.SUCCESS; // Always return SUCCESS + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ai/ExampleBossAI.java b/src/main/java/examplemod/examples/ai/ExampleBossAI.java new file mode 100644 index 0000000..93c9bc3 --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleBossAI.java @@ -0,0 +1,109 @@ +package examplemod.examples.ai; + +import examplemod.examples.mobs.ExampleBossMob; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.registries.BuffRegistry; +import necesse.engine.util.gameAreaSearch.GameAreaStream; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.Blackboard; +import necesse.entity.mobs.ai.behaviourTree.composites.SequenceAINode; +import necesse.entity.mobs.ai.behaviourTree.decorators.IsolateRunningAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.RemoveOnNoTargetNode; +import necesse.entity.mobs.ai.behaviourTree.leaves.TargetFinderAINode; +import necesse.entity.mobs.ai.behaviourTree.util.TargetFinderDistance; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.hostile.bosses.bossAIUtils.AttackStageManagerNode; +import necesse.entity.mobs.hostile.bosses.bossAIUtils.FlyToOppositeDirectionAttackStage; +import necesse.entity.mobs.hostile.bosses.bossAIUtils.FlyToRandomPositionAttackStage; +import necesse.entity.mobs.hostile.bosses.bossAIUtils.IdleTimeAttackStage; + +import java.awt.*; + +// Extends the SequenceAINode class, which basically is an "AND" parent. Specifically, it does this: +// Run child #1, if it returns FAILURE then stop and return FAILURE. +// If it returns SUCCESS, run the next child until finding one that returns FAILURE +public class ExampleBossAI extends SequenceAINode { + + public ExampleBossAI() { + super(); + + // We despawn the boss if it has no target for 5 seconds + addChild(new RemoveOnNoTargetNode<>(TickManager.ticksPerSec * 5)); + + // We add a target finder, which looks for all players within a 100 tile radius + addChild(new TargetFinderAINode(100 * 32) { + @Override + public GameAreaStream streamPossibleTargets(T mob, Point base, TargetFinderDistance distance) { + return TargetFinderAINode.streamPlayers(mob, base, distance); + } + }); + + // Now we add our attack stages. This makes the AI rotate between the attacks in the order they are added + // and wrap back to the beginning when completed the last stage. + AttackStageManagerNode attackStages = new AttackStageManagerNode<>(); + // We need to isolate the running node, since we still want the target finder + // to run while an attack stage returning RUNNING. + addChild(new IsolateRunningAINode<>(attackStages)); + + // We add the different stages in the order we want them to happen + + // Fly/run to a random position within 300 units of the current target. + // And don't go to next stage until we arrive. + attackStages.addChild(new FlyToRandomPositionAttackStage<>(true, 300)); + + // We idle for ½-2 seconds, based on how low on health we are + attackStages.addChild(new IdleTimeAttackStage<>(500, 2000)); + + // We "charge" the current target by flying to the opposite direction of the target, 200 units away from it + // No random angle offset + attackStages.addChild(new ChargeTargetStage()); + + // Idle again, based on how low on health we are + attackStages.addChild(new IdleTimeAttackStage<>(500, 2000)); + + // Fly to new position + attackStages.addChild(new FlyToRandomPositionAttackStage<>(true, 300)); + + // Do 3 quick charges in a row, without any idle time + attackStages.addChild(new ChargeTargetStage()); + attackStages.addChild(new ChargeTargetStage()); + attackStages.addChild(new ChargeTargetStage()); + + // Idle again + attackStages.addChild(new IdleTimeAttackStage<>(500, 2000)); + + // Go back to the beginning + + } + + // Here we define the custom attack stages we want to use. + // This stage class extends FlyToOppositeDirectionAttackStage, which charge the + // current target by flying to the opposite direction of the target. + public class ChargeTargetStage extends FlyToOppositeDirectionAttackStage { + + public ChargeTargetStage() { + super( + true, // We don't skip to the next stage before we have arrived + 250, // How many units in opposite direction + 0 // No random angle offset + ); + } + + @Override + public void onStarted(T mob, Blackboard blackboard) { + super.onStarted(mob, blackboard); + // Play the charge sound for all clients + mob.chargeSoundAbility.runAndSend(); + // We give a movespeed burst buff for 5 seconds when we start the charge + mob.buffManager.addBuff(new ActiveBuff(BuffRegistry.MOVE_SPEED_BURST, mob, 5f, null), true); + } + + @Override + public void onEnded(T mob, Blackboard blackboard) { + super.onEnded(mob, blackboard); + // When we end this attack stage, we remove the move speed burst buff + mob.buffManager.removeBuff(BuffRegistry.MOVE_SPEED_BURST, true); + } + } + +} 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..a362d78 --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java @@ -0,0 +1,22 @@ +package examplemod.examples.buffs; + +import necesse.engine.modifiers.ModifierValue; +import necesse.entity.mobs.buffs.BuffModifiers; +import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.setBonusBuffs.SimpleSetBonusBuff; + +/** + * Set bonus buff: + * When a player wears the full armor set, this buff is applied. + * It gives +10% damage and +10% movement speed. + */ +public class ExampleArmorSetBuff extends SimpleSetBonusBuff { + + public ExampleArmorSetBuff() { + // The parent class (SimpleSetBonusBuff) takes the stat boosts here. + super( + new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), // +10% damage + new ModifierValue<>(BuffModifiers.SPEED, 0.10f) // +10% speed + ); + } + +} \ No newline at end of file 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..bf6378b --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -0,0 +1,83 @@ +package examplemod.examples.buffs; + +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.staticBuffs.Buff; +import necesse.gfx.gameFont.FontOptions; +import necesse.level.maps.hudManager.floatText.DamageText; + +import java.awt.*; + +public class ExampleArrowBuff extends Buff { + + public ExampleArrowBuff() { + canCancel = false; + isVisible = false; + shouldSave = true; + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + // No modifiers to set in the buff + } + + @Override + public void clientTick(ActiveBuff buff) { + super.clientTick(buff); + // We run the tickHealing in both client and server game ticks + tickHealing(buff); + } + + @Override + public void serverTick(ActiveBuff buff) { + super.serverTick(buff); + // We run the tickHealing in both client and server game ticks + tickHealing(buff); + } + + public void tickHealing(ActiveBuff buff) { + // We get the health given per game tick + float healthPerGameTick = buff.getGndData().getFloat("healthPerGameTick"); + + // In case the health given per tick is lower than 1, we need to accumulate it over time until + // we can heal at least 1 health point. We do this in the form of a buffer + + // First we get what the buffer is right now (from previous ticks) + float healBuffer = buff.getGndData().getFloat("nextHealBuffer"); + // Next we add the health this tick + healBuffer += healthPerGameTick; + + // If we have more than 1 health to give, we do it + if (healBuffer >= 1) { + // We get the lowest value of the healAmount + int healAmount = (int) Math.floor(healBuffer); + + // Calculate the new health of the owner + Mob owner = buff.owner; + + // Update the health + owner.setHealth(owner.getHealth() + healAmount, buff.getAttacker()); + + // Show the green heal text: + // Font options is what the font should look like + FontOptions fontOptions = new FontOptions(12) + .outline() // It should have an outline + .color(Color.GREEN); // It should be green + + // The text goes up by a random amount + int heightIncrease = GameRandom.globalRandom.getIntBetween(25, 45); + + // And we add the text to the levels hud manager + owner.getLevel().hudManager.addElement(new DamageText(owner, healAmount, fontOptions, heightIncrease)); + + // Reduce the buffer + healBuffer -= healAmount; + } + + // Finally, save back the buffer amount for next tick + buff.getGndData().setFloat("nextHealBuffer", healBuffer); + } + +} \ 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..979337e 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; @@ -13,7 +13,6 @@ public ExampleBuff() { shouldSave = true; } - @Override public void init(ActiveBuff activeBuff, BuffEventSubscriber buffEventSubscriber) { // Apply modifiers here diff --git a/src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java b/src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java new file mode 100644 index 0000000..032e5ef --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java @@ -0,0 +1,29 @@ +package examplemod.examples.buffs; + +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.trinketBuffs.SimpleTrinketBuff; + +public class ExampleTrinketBuff extends SimpleTrinketBuff { + public ExampleTrinketBuff(){ + + } + + @Override + public void init(ActiveBuff activeBuff, BuffEventSubscriber buffEventSubscriber) { + // Apply modifiers here + activeBuff.setModifier(BuffModifiers.SPELUNKER,true); // +50% speed + } + + @Override + public void serverTick(ActiveBuff buff) { + // You can do server ticks here + } + + @Override + public void clientTick(ActiveBuff buff) { + // You can do client ticks here, like adding particles to buff.owner + } + +} \ No newline at end of file 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..2e23b7d --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java @@ -0,0 +1,189 @@ +package examplemod.examples.events; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.network.NetworkClient; +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.server.ServerClient; +import necesse.engine.registries.BuffRegistry; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.entity.ParticleTypeSwitcher; +import necesse.entity.levelEvent.LevelEvent; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.particle.Particle; + +import java.awt.*; + +/** + * A LevelEvent is an event happening on a specific level + * They're quite versatile. A lot of level events are temporary and never saved. But they can be saved to the level + * Some are only used to on either the server or client to run some logic over time, while others are + * synced between the server and clients + *
+ * + * A few things to note about syncing: + * If an event is over a significant amount of time, and you want newer clients entering the area to + * get the event, make sure to override isNetworkImportant() and return true. This will also make it so + * that if you call over() on the server, a packet will be sent to the client, disposing the event as well + *
+ * + * This level event specifically is used for: + * - Server: Send a chat message to the target player and give a speed burst buff + * - Client: Show a burst of particles at the target tile position + */ +public class ExampleLevelEvent extends LevelEvent { + + // The tile position of the event + protected int tileX, tileY; + + // The client we're targeting with this event + protected NetworkClient targetClient; + + // Simple lifetime for the client effect (in ticks) + // This is not synced in the spawn packet, since the event is so short + protected int ticksLeft = TickManager.ticksPerSec / 2; + + // When spawning particles, there are 3 different particle "types" you can use + // Depending on importance, they will define if the particle will actually show or not depending on + // the clients graphics settings + // The options are: + // - Cosmetic: Will only show if particle setting is set to "maximum", and there are less than + // maximum particles in the area already + // - Important cosmetic: Same as cosmetic, but will also show with "decreased" particle setting + // - Critical: Will always show, even on "minimal" particle settings + // Particles will in general not spawn, if spawned outside of the screen area. But it is possible to + // override this by giving it a "null" type. It's the same as critical, but ignore screen restrictions as well + + // Here we define which types we will use later on. It will iterate through the types for each particle and + // wrap around to the first one when complete + protected ParticleTypeSwitcher particleTypeSwitcher = new ParticleTypeSwitcher( + Particle.GType.COSMETIC, + Particle.GType.IMPORTANT_COSMETIC, + Particle.GType.CRITICAL + ); + + // Required empty constructor for registry/network spawning + public ExampleLevelEvent() { + } + + public ExampleLevelEvent(ServerClient targetClient, int tileX, int tileY) { + this.targetClient = targetClient; + this.tileX = tileX; + this.tileY = tileY; + } + + @Override + public void setupSpawnPacket(PacketWriter writer) { + super.setupSpawnPacket(writer); + // setupSpawnPacket(...) is called on the server before it sends a packet with this LevelEvent to clients + // 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 they are written + + // The tile positions of this event + writer.putNextInt(tileX); + writer.putNextInt(tileY); + + // Since the server supports a maximum of 250 slots, we use unsigned byte to write the slot + writer.putNextByteUnsigned(targetClient.slot); + } + + @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 the values you read are in the exact same order as you set them up in + // setupSpawnPacket(...), otherwise you'll desync fields and get confusing bugs + + // Read the tile positions + tileX = reader.getNextInt(); + tileY = reader.getNextInt(); + + // Read target client slot and assign the target client + int targetSlot = reader.getNextByteUnsigned(); + if (isClient()) { + targetClient = getClient().getClient(targetSlot); + } else if (isServer()) { + // It is possible for the client to send a level event to the server, in + // very specific and rare cases. Mostly in debugging cases + targetClient = getServer().getClient(targetSlot); + } + } + + @Override + public void init() { + super.init(); + // init() happens just after the event has been added to a level + + // If we have no target client, don't run this event + if (targetClient == null) { + over(); + return; + } + + // Server side: Just send the message and give the buff + if (isServer()) { + targetClient.getServerClient().sendChatMessage("This message was sent from the ExampleLevelEvent"); + + // 5 seconds movement speed burst buff + ActiveBuff activeBuff = new ActiveBuff(BuffRegistry.MOVE_SPEED_BURST, targetClient.playerMob, 5f, null); + + // We make sure to send the buff to other clients, since this is only ran on the server + targetClient.playerMob.buffManager.addBuff(activeBuff, true); + + // End the event on the server + over(); + } + } + + @Override + public void clientTick() { + super.clientTick(); + // Runs every game tick, only on the client + + // Reduce the ticks left and call over when complete + ticksLeft--; + if (ticksLeft <= 0) { + over(); + return; + } + + Color particleColor = new Color(120, 200, 255); + + // Spawn 4 particles every game tick (20 ticks per second) + for (int i = 0; i < 4; i++) { + // Find the top-left level position of the tile + int levelX = GameMath.getLevelCoordinate(tileX); + int levelY = GameMath.getLevelCoordinate(tileY); + + // Make the position random within the tile + levelX += GameRandom.globalRandom.nextInt(32); + levelY += GameRandom.globalRandom.nextInt(32); + + // Add the particle to the level + // Here we use our particle type switcher defined above. You can read more about it up there + // A lot of things can be defined when spawning a particle. Here we use a pretty simple color, + // size, alpha, height and lifetime + level.entityManager + .addParticle(levelX, levelY, particleTypeSwitcher.next()) + .color(particleColor) + .sizeFades(20, 30) + .heightMoves(0, 20) + .fadesAlphaTime(250, 150) + .lifeTime(400); + } + } + + public Point getSaveToRegionPos() { + // Since this event is sent over the network, we need to define which regions it is part of + // This is used to determine which clients should receive the event spawn packet + // If the event is part of multiple regions, we can override getRegionPositions() + + // We convert the tile position to a region position using the levels region manager + return new Point( + level.regionManager.getRegionCoordByTile(tileX), + level.regionManager.getRegionCoordByTile(tileY) + ); + } + +} diff --git a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java b/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java deleted file mode 100644 index 8c15c37..0000000 --- a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java +++ /dev/null @@ -1,11 +0,0 @@ -package examplemod.examples.items; - -import necesse.inventory.item.matItem.MatItem; - -public class ExampleHuntIncursionMaterialItem extends MatItem { - - public ExampleHuntIncursionMaterialItem() { - super(100, Rarity.RARE); - } - -} diff --git a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/ExampleMaterialItem.java deleted file mode 100644 index a20ddb6..0000000 --- a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java +++ /dev/null @@ -1,11 +0,0 @@ -package examplemod.examples.items; - -import necesse.inventory.item.matItem.MatItem; - -public class ExampleMaterialItem extends MatItem { - - public ExampleMaterialItem() { - super(100, Rarity.UNCOMMON); - } - -} diff --git a/src/main/java/examplemod/examples/items/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/ExamplePotionItem.java deleted file mode 100644 index 17bdcdc..0000000 --- a/src/main/java/examplemod/examples/items/ExamplePotionItem.java +++ /dev/null @@ -1,11 +0,0 @@ -package examplemod.examples.items; - -import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; - -public class ExamplePotionItem extends SimplePotionItem { - - public ExamplePotionItem() { - super(100,Rarity.COMMON,"examplebuff",100, "examplepotionitemtip"); - } - -} \ No newline at end of file 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..e5b775d --- /dev/null +++ b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java @@ -0,0 +1,33 @@ +package examplemod.examples.items.ammo; + +import necesse.engine.registries.ProjectileRegistry; +import necesse.entity.mobs.GameDamage; +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 + + damage = 8; // Adds +8 damage to the bows base damage + armorPen = 2; // Adds +2 armor pen + critChance = 0.05f; // +5% crit chance + 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) { + return ProjectileRegistry.getProjectile( + "examplearrowprojectile", // Projectile stringID that the arrow shoots + owner.getLevel(), + x, y, targetX, targetY, + velocity, range, + damage, knockback, + 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..5a710bc --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java @@ -0,0 +1,20 @@ +package examplemod.examples.items.armor; + +import necesse.engine.registries.ItemRegistry; +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, // Armor value + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Enchant cost. See explanation in ExampleSwordMeleeWeaponItem + Item.Rarity.UNCOMMON, // Rarity + "exampleboots", // Texture name (loaded from resources/player/armor/...) + FeetArmorLootTable.feetArmor // Loot table category + ); + } + +} 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..5626f8a --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java @@ -0,0 +1,40 @@ +package examplemod.examples.items.armor; + +import necesse.engine.modifiers.ModifierValue; +import necesse.engine.registries.ItemRegistry; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.buffs.BuffModifiers; +import necesse.inventory.InventoryItem; +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.ArmorModifiers; +import necesse.inventory.item.armorItem.ChestArmorItem; +import necesse.inventory.item.upgradeUtils.FloatUpgradeValue; +import necesse.inventory.lootTable.presets.BodyArmorLootTable; + +public class ExampleChestArmorItem extends ChestArmorItem { + + // Additional stats besides armor value the chestpiece gives (used in getter below) + public FloatUpgradeValue healthRegen = new FloatUpgradeValue() + .setBaseValue(1f) // Base: 1 health per second + .setUpgradedValue(1, 4f); // Tier 1: 4 health per second + + public ExampleChestArmorItem() { + super( + 4, // Armor value + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Enchant cost. See explanation in ExampleSwordMeleeWeaponItem + Item.Rarity.UNCOMMON, // Rarity + "examplechest", // Body texture name (loaded from resources/player/armor/...) + "examplearms", // Arms texture name (loaded from resources/player/armor/...) + BodyArmorLootTable.bodyArmor // Loot table category + ); + } + + // Here we can return what stats other than armor the piece gives when equipped + @Override + public ArmorModifiers getArmorModifiers(InventoryItem item, Mob mob) { + return new ArmorModifiers( + new ModifierValue<>(BuffModifiers.COMBAT_HEALTH_REGEN_FLAT, healthRegen.getValue(getUpgradeTier(item))) + ); + } + +} 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..05c3d2c --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java @@ -0,0 +1,27 @@ +package examplemod.examples.items.armor; + +import necesse.engine.registries.DamageTypeRegistry; +import necesse.engine.registries.ItemRegistry; +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 + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Enchant cost. See explanation in ExampleSwordMeleeWeaponItem + HeadArmorLootTable.headArmor, // Head armor loot category + ArmorSetsLootTable.armorSets, // Armor sets loot category + Item.Rarity.UNCOMMON, // Rarity + "examplehelmet", // Helmet texture name (loaded from resources/player/armor/...) + "examplechestplate", // Chest item stringID. Used for set bonus, etc. + "exampleboots", // Boots item stringID. Used for set bonus, etc. + "examplearmorsetbonusbuff" // Set bonus buff stringID as defined in ExampleModBuffs + ); + } + +} diff --git a/src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java b/src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java new file mode 100644 index 0000000..100426e --- /dev/null +++ b/src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java @@ -0,0 +1,174 @@ +package examplemod.examples.items.consumable; + +import examplemod.examples.maps.biomes.ExampleBiome; +import necesse.engine.localization.Localization; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.network.gameNetworkData.GNDItemMap; +import necesse.engine.network.packet.PacketChatMessage; +import necesse.engine.registries.MobRegistry; +import necesse.engine.util.GameBlackboard; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.gameTooltips.ListGameTooltips; +import necesse.inventory.InventoryItem; +import necesse.inventory.item.Item; +import necesse.inventory.item.ItemCategory; +import necesse.inventory.item.placeableItem.consumableItem.ConsumableItem; +import necesse.level.maps.IncursionLevel; +import necesse.level.maps.Level; + +import java.awt.geom.Line2D; + +/** + * A consumable item that summons our boss mob. + */ +public class ExampleBossSummonItem extends ConsumableItem { + + public ExampleBossSummonItem() { + // Stack size 1, is "single use" consumable behavior + super(1, true); + + // Cooldown (ms) before you can use it again + itemCooldownTime.setBaseValue(2000); + + // If the player dies, drop this like a material (depending on death penalty rules) + dropsAsMatDeathPenalty = true; + + // Search keywords (helps with the in-game search) + keyWords.add("boss"); + + // Item rarity / color + rarity = Item.Rarity.LEGENDARY; + + // How big the item sprite is when dropped in the world + worldDrawSize = 32; + + // How long it takes the incinerator to destroy this item + incinerationTimeMillis = 30_000; + + // Often when you extend an existing object (like ChairObject in this case), it will have the categories + // defined in that parent class. But in this case we want to use our custom example category, which + // we have defined in ExampleModCategories + setItemCategory("examplemod", "sub"); + // If we want to change where it is displayed in workstations, we set the crafting category: + setItemCategory(ItemCategory.craftingManager, "examplemod", "sub"); + } + + /** + * Checks if the item is allowed to be used here. + */ + public String canPlace(Level level, int x, int y, PlayerMob player, + Line2D playerPositionLine, InventoryItem item, GNDItemMap mapContent) { + // Don't allow boss summoning inside an incursion (special dungeon-like levels) + if (level instanceof IncursionLevel) { + return "inincursion"; + } + + // Only allow use in normal caves (not surface or deep caves) + if (!level.isBasicCaveLevel()) { + return "notcave"; + } + + // Figure out which tile we should check. + // If we have a player, use the player's tile. + // If not (rare cases), convert the clicked pixel coords into tile coords. + int tileX, tileY; + if (player == null) { + tileX = GameMath.getTileCoordinate(x); + tileY = GameMath.getTileCoordinate(y); + } else { + tileX = player.getTileX(); + tileY = player.getTileY(); + } + + // Only allow our ExampleBiome + if (level.getBiome(tileX, tileY) instanceof ExampleBiome) { + return "notexamplebiome"; + } + + // No errors to return, allow the placement + return null; + } + + /** + * Runs when the player tries to use the item but canPlace(...) returned an error. + * This is where we can send a nicer message to the player. + */ + public InventoryItem onAttemptPlace(Level level, int x, int y, PlayerMob player, + InventoryItem item, GNDItemMap mapContent, String error) { + + // Only do chat messages on the server, and only if the error was because it's in an incursion + if (level.isServer() && player != null && error.equals("inincursion")) { + player.getServerClient().sendChatMessage(new LocalMessage("misc", "cannotsummoninincursion")); + } + + // Let base game handle the rest + return super.onAttemptPlace(level, x, y, player, item, mapContent, error); + } + + /** + * Runs when the item is successfully used. + * This is where we actually spawn the boss. + */ + public InventoryItem onPlace(Level level, int x, int y, PlayerMob player, + int seed, InventoryItem item, GNDItemMap mapContent) { + + // Only spawn mobs on the server (clients are just visuals) + if (level.isServer()) { + // Simple debug log + System.out.println("Example Boss Mob has been summoned at " + level.getIdentifier() + "."); + + // Pick a random direction (angle 0-359 degrees) + int angle = GameRandom.globalRandom.nextInt(360); + + // Turn that angle into a unit direction vector (nx, ny) + float nx = GameMath.cos(angle); + float ny = GameMath.sin(angle); + + // How far away from the player the boss should appear (in pixels) + float distance = 16 * 32; // 16 tiles + + // Create the boss mob instance + Mob mob = MobRegistry.getMob("exampleboss", level); + + // Spawn it near the player, at a random offset + level.entityManager.addMob( + mob, + (player.getX() + (int) (nx * distance)), + (player.getY() + (int) (ny * distance)) + ); + + // Tell nearby clients (chat message) that the boss was summoned + level.getServer().network.sendToClientsWithEntity( + new PacketChatMessage(new LocalMessage("misc", "bosssummon", "name", mob.getLocalization())), + mob + ); + } + + // If this item is single-use, consume 1 from the stack + if (isSingleUse(player)) { + item.setAmount(item.getAmount() - 1); + } + + return item; + } + + /** + * Extra tooltip line shown on the item. + */ + public ListGameTooltips getTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { + ListGameTooltips tooltips = super.getTooltips(item, perspective, blackboard); + tooltips.add(Localization.translate("itemtooltip", "examplebosssummontip")); + return tooltips; + } + + /** + * The "type name" shown in the journal, etc. (e.g. Relic). + */ + public String getTranslatedTypeName() { + return Localization.translate("item", "relic"); + } + +} \ No newline at end of file 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/consumable/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java new file mode 100644 index 0000000..1f87164 --- /dev/null +++ b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java @@ -0,0 +1,17 @@ +package examplemod.examples.items.consumable; + +import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; + +public class ExamplePotionItem extends SimplePotionItem { + + public ExamplePotionItem() { + super( + 100, // Max stack size + Rarity.COMMON, // Item rarity + "examplebuff", // Buff stringID to apply + 120, // Buff duration in seconds + "examplepotionitemtip" // Localization key for tooltip (under itemtooltip category) + ); + } + +} \ No newline at end of file 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..11f8b0b --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java @@ -0,0 +1,15 @@ +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, // Max stack size + Item.Rarity.UNCOMMON // Rarity + ); + + } +} 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..f930abf --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -0,0 +1,20 @@ +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. + * 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 + super("examplegrasstile"); + } + +} 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..c5ca616 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java @@ -0,0 +1,17 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.matItem.MatItem; + +public class ExampleLogItem extends MatItem { + + public ExampleLogItem() { + super( + 500, // Max stack size + Rarity.UNCOMMON, // Rarity + new String[]{ "anylog" } // Global ingredient stringIDs + ); + + // Adjust the item category to logs + setItemCategory("materials", "logs"); + } +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java new file mode 100644 index 0000000..a8403e8 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java @@ -0,0 +1,14 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.matItem.MatItem; + +public class ExampleMaterialItem extends MatItem { + + public ExampleMaterialItem() { + super( + 100, // Max stack size + Rarity.UNCOMMON // Rarity + ); + } + +} 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..ff85dac --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java @@ -0,0 +1,15 @@ +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, // Max stack size + Item.Rarity.UNCOMMON // Rarity + ); + + } +} 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..523cc72 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java @@ -0,0 +1,11 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.StonePlaceableItem; + +public class ExampleStoneItem extends StonePlaceableItem { + + public ExampleStoneItem(){ + super(100); // Max stack size + } + +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeaponItem.java b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeaponItem.java new file mode 100644 index 0000000..e2a7104 --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeaponItem.java @@ -0,0 +1,36 @@ +package examplemod.examples.items.tools; + +import necesse.engine.registries.ItemRegistry; +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.projectileToolItem.bowProjectileToolItem.BowProjectileToolItem; +import necesse.inventory.lootTable.presets.BowWeaponsLootTable; + +public class ExampleBowRangedWeaponItem extends BowProjectileToolItem { + + public ExampleBowRangedWeaponItem() { + super( + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Enchant Cost + BowWeaponsLootTable.bowWeapons // Loot table category + ); + rarity = Item.Rarity.NORMAL; + + // Core stats + attackAnimTime.setBaseValue(800); // Attack animation time in milliseconds + attackDamage.setBaseValue(16) // Base damage + .setUpgradedValue(1, 120); // Upgraded tier 1 damage + attackRange.setBaseValue(600); // Attack range + velocity.setBaseValue(100); // Projectile velocity + knockback.setBaseValue(25); // Knockback + + // Offsets of the attack item sprite relative to the player arm + attackXOffset = 8; + attackYOffset = 20; + + // How much the bow sprite “stretches” while charging + attackSpriteStretch = 4; + + // Optional + canBeUsedForRaids = true; + } + +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeaponItem.java b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeaponItem.java new file mode 100644 index 0000000..3e013dc --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeaponItem.java @@ -0,0 +1,30 @@ +package examplemod.examples.items.tools; + +import necesse.engine.registries.ItemRegistry; +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 ExampleOrbSummonWeaponItem extends SummonToolItem { + + public ExampleOrbSummonWeaponItem() { + super( + "examplesummon", // Mob stringID + FollowPosition.PYRAMID, // Follow position + 1, // Summon space taken per mob spawned (1 slot) + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Weapon enchant cost + SummonWeaponsLootTable.summonWeapons // Loot table category (used for incursion drop, etc.) + ); + + rarity = Item.Rarity.UNCOMMON; + + // Base damage: 15, and a tier 1 damage: 35 + attackDamage.setBaseValue(15).setUpgradedValue(1, 35); + + // Offsets of the attack item sprite relative to the player arm + attackXOffset = 15; + attackYOffset = 10; + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeaponItem.java similarity index 89% rename from src/main/java/examplemod/examples/ExampleProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeaponItem.java index 31aff93..cdf8ca9 100644 --- a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeaponItem.java @@ -1,7 +1,9 @@ -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.registries.ItemRegistry; import necesse.engine.sound.SoundEffect; import necesse.engine.sound.SoundManager; import necesse.engine.util.GameBlackboard; @@ -14,17 +16,18 @@ import necesse.gfx.gameTooltips.ListGameTooltips; import necesse.inventory.InventoryItem; import necesse.inventory.item.toolItem.projectileToolItem.magicProjectileToolItem.MagicProjectileToolItem; +import necesse.inventory.lootTable.presets.MagicWeaponsLootTable; import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleProjectileWeapon extends MagicProjectileToolItem { +public class ExampleStaffMagicWeaponItem 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() { - super(400, null); + public ExampleStaffMagicWeaponItem() { + super(ItemRegistry.EQUIPMENT_VALUE_GOLD, MagicWeaponsLootTable.magicWeapons); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); attackDamage.setBaseValue(20) // Base sword damage @@ -41,7 +44,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; } @@ -57,7 +60,7 @@ public void showAttack(Level level, int x, int y, ItemAttackerMob attackerMob, i @Override public InventoryItem onAttack(Level level, int x, int y, ItemAttackerMob attackerMob, int attackHeight, InventoryItem item, ItemAttackSlot slot, int animAttack, int seed, GNDItemMap mapContent) { - // This method is ran on the attacking client and on the server. + // This method is run on the attacking client and on the server. // This means we need to tell other clients that a projectile is being added. // Every projectile weapon is set to include an integer seed used to make sure that the attacking client // and the server gives the projectiles added the same uniqueID. diff --git a/src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeaponItem.java b/src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeaponItem.java new file mode 100644 index 0000000..8f18e2a --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeaponItem.java @@ -0,0 +1,30 @@ +package examplemod.examples.items.tools; + +import necesse.engine.registries.ItemRegistry; +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; +import necesse.inventory.lootTable.presets.CloseRangeWeaponsLootTable; + +// Extends SwordToolItem +public class ExampleSwordMeleeWeaponItem extends SwordToolItem { + + // Weapon attack textures are loaded from resources/player/weapons/ + + public ExampleSwordMeleeWeaponItem() { + super( + ItemRegistry.EQUIPMENT_VALUE_GOLD, // Enchant cost + CloseRangeWeaponsLootTable.closeRangeWeapons // Loot table category + ); + // Enchant cost also defines the general "value" of the equipment. This is used for how much it's + // prioritized by settlers for changing their weapon, and when determining which raid should spawn and + // with what gear they spawn + + rarity = Item.Rarity.UNCOMMON; // Rarity + attackAnimTime.setBaseValue(300); // 300 ms attack time + attackDamage.setBaseValue(20) // Base Sword damage + .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/trinkets/ExampleTrinketItem.java b/src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java new file mode 100644 index 0000000..71b8556 --- /dev/null +++ b/src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java @@ -0,0 +1,32 @@ +package examplemod.examples.items.trinkets; + +import necesse.engine.localization.Localization; +import necesse.engine.util.GameBlackboard; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.gameTooltips.ListGameTooltips; +import necesse.inventory.InventoryItem; +import necesse.inventory.item.trinketItem.SimpleTrinketItem; +import necesse.inventory.lootTable.presets.TrinketsLootTable; + +// Extends SimpleTrinketItem +public class ExampleTrinketItem extends SimpleTrinketItem { + + public ExampleTrinketItem() { + super( + Rarity.UNCOMMON, // Rarity + "exampletrinketbuff", // The buffs stringID that it gives + 400, // Enchant cost + TrinketsLootTable.trinkets // Loot table category + ); + } + + @Override + public ListGameTooltips getPreEnchantmentTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { + ListGameTooltips tooltips = super.getPreEnchantmentTooltips(item, perspective, blackboard); + + // Add our custom tooltip + tooltips.add(Localization.translate("itemtooltip", "exampletrinkettip")); + + return tooltips; + } +} \ 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..e81447a --- /dev/null +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -0,0 +1,221 @@ +package examplemod.examples.maps.biomes; + +import examplemod.Loaders.ExampleModObjects; +import examplemod.Loaders.ExampleModTiles; +import examplemod.examples.ExampleLootTable; +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.util.LevelIdentifier; +import necesse.engine.world.biomeGenerator.BiomeGeneratorStack; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.PlayerMob; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.maps.Level; +import necesse.level.maps.biomes.Biome; +import necesse.level.maps.biomes.MobSpawnTable; +import necesse.level.maps.presets.RandomCaveChestRoom; +import necesse.level.maps.presets.caveRooms.CaveRuins; +import necesse.level.maps.regionSystem.Region; + +import java.awt.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A biome controls stuff like: + * - World generation features of this biome + * - Mob spawns and biome specific mob drops + * - What music is playing in the biome + */ +public class ExampleBiome extends Biome { + + // Here we construct the mob spawn table for later use + public static MobSpawnTable mobSpawnTable = new MobSpawnTable() + .add(100, "examplemob"); + + + // Set up the loot interface for our boss summon extra drop + public static LootTable randomExampleBossSummonDrop = new LootTable( + // 10% chance to drop + new ChanceLootItem(0.1f, "examplebosssummonitem") + ); + + public ExampleBiome() { + super(); + // Setting the generation weight makes this biome spawn in the world + setGenerationWeight(1); + } + + // ========================================================================= + // In generation, it uses these next getters as base to figure out what tiles/objects to spawn + // Since these getters are used very often during generation, we want it to be very optimized + // Because of this, we have stored the ID of the tile we want to use in the registry + + @Override + public int getGenerationTerrainTileID() { + return ExampleModTiles.EXAMPLE_GRASS_TILE_ID; + } + + @Override + public int getGenerationCaveTileID() { + return ExampleModTiles.EXAMPLE_TILE_ID; + } + + @Override + public int getGenerationCaveRockObjectID() { + return ExampleModObjects.EXAMPLE_BASE_ROCK_ID; + } + + @Override + public int getGenerationDeepCaveTileID() { + // If we ever add a separate deep version, change it here + return ExampleModTiles.EXAMPLE_TILE_ID; + } + + @Override + public int getGenerationDeepCaveRockObjectID() { + // If we ever add a separate deep version, change it here + return ExampleModObjects.EXAMPLE_BASE_ROCK_ID; + } + + // ========================================================================= + // The way generator stacks works, is that we first set up the branches/veins in the initialize method + // We then later use these branches in the generate methods below + + @Override + public void initializeGeneratorStack(BiomeGeneratorStack stack) { + super.initializeGeneratorStack(stack); + + // Trees on the surface + stack.addRandomSimplexVeinsBranch("exampleTrees", 2f, 0.2f, 1f, 0); + + // Ore veins underground + stack.addRandomVeinsBranch("exampleCaveOre", 0.6f, 3, 6, 0.4f, 2, false); + stack.addRandomVeinsBranch("exampleDeepCaveOre", 0.6f, 3, 6, 0.4f, 2, false); + } + + @Override + public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionSurfaceTerrain(region, stack, random); + + // On the surface, we use our exampleTrees vein we initialized above + // The stack has a factory-style place system like seen below + // This can also be used to place tiles, mobs, etc. + // Or you can use the customPlace(..) in the end to iterate through the valid tiles + + int grassTile = getGenerationTerrainTileID(); + + stack.startPlaceOnVein(this, region, random, "exampleTrees") + .onlyOnTile(grassTile) + .chance(0.1f) // 10% chance for each valid spot + .placeObject("exampletree"); + + stack.startPlace(this, region, random) + .chance(0.4f) // 40% chance for each valid spot + .onlyOnTile(grassTile) + .placeObject("examplegrass"); + } + + @Override + public void generateRegionCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionCaveTerrain(region, stack, random); + + // In the cave, we use our exampleCaveOre vein we initialized above + stack.startPlaceOnVein(this, region, random, "exampleCaveOre") + .onlyOnObject(getGenerationCaveRockObjectID()) + .placeObjectForced("exampleorerock"); + + // If you want crates / small rocks etc, add them here. + + // If you place stuff relating to liquid, like only on shores, only certain distance from + // the shore, etc. You can call this update to actually calculate that data before placing: + // region.updateLiquidManager(); + } + + @Override + public void generateRegionDeepCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionDeepCaveTerrain(region, stack, random); + + // In the deep cave, we use our exampleDeepCaveOre vein we initialized above + stack.startPlaceOnVein(this, region, random, "exampleDeepCaveOre") + .onlyOnObject(getGenerationDeepCaveRockObjectID()) + .placeObjectForced("exampleorerock"); + + } + + @Override + public Color getDebugBiomeColor() { + // Debug color is only used for debug tools. Specifically in the F10 menu -> Dev tools -> One World tests + return new Color(128, 0, 128); + } + + @Override + public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { + // This biome only plays Forest Path. Even in caves, etc. + // Here you can do checks if level is a cave, etc. Like: level.isCave / level.isDeepCaveLevel() + return new MusicList(MusicRegistry.ForestPath); + } + + @Override + public LootTable getExtraBiomeMobDrops(LevelIdentifier levelIdentifier) { + // This is currently only used for showing in the journal + if (levelIdentifier.isCave()) { + return randomExampleBossSummonDrop; + } + return new LootTable(); + } + + // Add Example Boss Summon Item + @Override + public LootTable getExtraMobDrops(Mob mob) { + LevelIdentifier levelIdentifier = mob.getLevel().getIdentifier(); + // When in regular cave, hostile mobs that are not summoned have a random + // chance to drop the boss summon item + if (levelIdentifier.isCave() && mob.isHostile && !mob.isSummoned) { + return randomExampleBossSummonDrop; + } + return super.getExtraMobDrops(mob); + } + + @Override + public MobSpawnTable getMobSpawnTable(Level level) { + // We use the same spawn table for all levels in this biome. + // Here you can do checks if level is a cave, etc. Like: level.isCave / level.isDeepCaveLevel() + return mobSpawnTable; + } + + // ========================================================================= + // Structures / presets + + public RandomCaveChestRoom getNewCaveChestRoomPreset(GameRandom random, AtomicInteger lootRotation) { + // Here we generate a chest room based on our example loot table and chest room set + RandomCaveChestRoom preset = new RandomCaveChestRoom( + random, + ExampleLootTable.exampleLootTable, + lootRotation, + ExampleModObjects.EXAMPLE_CHEST_ROOM_SET + ); + // Because of a bug in the base game, we have to replace the floor manually + preset.replaceTile(TileRegistry.stoneFloorID, ExampleModObjects.EXAMPLE_CHEST_ROOM_SET.floor); + return preset; + } + + public RandomCaveChestRoom getNewDeepCaveChestRoomPreset(GameRandom random, AtomicInteger lootRotation) { + // This example biome does not spawn deep cave chest rooms + return null; + } + + public CaveRuins getNewCaveRuinsPreset(GameRandom random, AtomicInteger lootRotation) { + // This example biome does not spawn cave ruins + return null; + } + + public CaveRuins getNewDeepCaveRuinsPreset(GameRandom random, AtomicInteger lootRotation) { + // This example biome does not spawn cave ruins + return null; + } + +} diff --git a/src/main/java/examplemod/examples/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java similarity index 90% rename from src/main/java/examplemod/examples/ExampleIncursionBiome.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java index 6671f98..83a1c59 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,23 +26,23 @@ public class ExampleIncursionBiome extends IncursionBiome { public ExampleIncursionBiome() { - super("reaper"); // The boss mob string ID for this incursion + super("exampleboss"); // 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")); } /** * Loot dropped from mobs during hunt-type incursion objectives. - * This example returns a custom item to demonstrate adding new drops. + * This example just returns the example item. */ @Override public LootTable getHuntDrop(IncursionData incursionData) { return new LootTable( - new ChanceLootItem(0.66F, "examplehuntincursionitem") + new ChanceLootItem(0.66F, "exampleitem") ); } @@ -83,7 +83,7 @@ public IncursionLevel getNewIncursionLevel(FallenAltarObjectEntity altar, LevelI @Override public ArrayList getFallenAltarGatewayColorsForBiome() { ArrayList colors = new ArrayList<>(); - // Repeat colors to satisfy the altar rendering requirements + // Repeat colours to satisfy the altar rendering requirements colors.add(new Color(181, 80, 120)); colors.add(new Color(215, 42, 52)); colors.add(new Color(181, 92, 59)); @@ -92,4 +92,5 @@ public ArrayList getFallenAltarGatewayColorsForBiome() { colors.add(new Color(181, 92, 59)); return colors; } + } diff --git a/src/main/java/examplemod/examples/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java similarity index 68% rename from src/main/java/examplemod/examples/ExampleIncursionLevel.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java index e332674..1502c40 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.presets.ExamplePreset; import necesse.engine.GameEvents; import necesse.engine.events.worldGeneration.GenerateCaveLayoutEvent; import necesse.engine.events.worldGeneration.GeneratedCaveOresEvent; @@ -16,6 +17,7 @@ import necesse.level.maps.incursion.BiomeExtractionIncursionData; import necesse.level.maps.incursion.BiomeMissionIncursionData; import necesse.level.maps.incursion.IncursionBiome; +import necesse.level.maps.presets.Preset; /** * Example incursion level. @@ -45,32 +47,25 @@ 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 - PresetGeneration entranceAndPerkPresets = new PresetGeneration(this); + // Keeps track of occupied space when trying to place presets + PresetGeneration presets = new PresetGeneration(this); - // Generate an incursion entrance that clears terrain, - // blends edges, reserves space, and places the return portal + // Generate entrance (this reserves space inside presets) + int spawnSize = 32; boolean hasBiggerArenaPerk = altarData.hasPerk(IncursionPerksRegistry.BIGGER_ARENA); IncursionBiome.generateEntrance( this, - entranceAndPerkPresets, + presets, cg.random, - 32, + spawnSize, cg.rockTile, "exampletile", "exampletile", @@ -78,15 +73,30 @@ public void generateLevel(BiomeMissionIncursionData incursionData, AltarData alt hasBiggerArenaPerk ); - // Now call incursion perks to generate their presets - generatePresetsBasedOnPerks(altarData, incursionData, entranceAndPerkPresets, cg.random, baseBiome); + // Perk presets avoid the entrance preset, since we pass presets as presetGeneration parameter + generatePresetsBasedOnPerks(altarData, incursionData, presets, cg.random, baseBiome); + + // We add an example preset to the level. We can either decide to do this before or after perk + // presets. Depending on how important we think it is as part of generation. If not important, + // then add it after the perks like this + Preset examplePreset = new ExamplePreset(cg.random); + presets.findRandomValidPositionAndApply( + cg.random, + 250, // It tries to place randomly anywhere with this many attempts + examplePreset, + 8, // How many tiles around the edge of the level it should be within + true, // randomizeMirrorX + true, // randomizeMirrorY + true, // randomizeRotation + false // overrideCanPlace (false = respect canApply rules) + ); // 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..dcd029d --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -0,0 +1,202 @@ +package examplemod.examples.mobs; + +import examplemod.ExampleMod; +import examplemod.examples.ai.ExampleBossAI; +import necesse.engine.eventStatusBars.EventStatusBarManager; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.network.server.ServerClient; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.sound.PositionSoundEffect; +import necesse.engine.sound.SoundEffect; +import necesse.engine.sound.SoundManager; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ability.EmptyMobAbility; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.hostile.bosses.FlyingBossMob; +import necesse.entity.particle.FleshParticle; +import necesse.entity.particle.Particle; +import necesse.gfx.GameResources; +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.LootItem; +import necesse.inventory.lootTable.lootItem.RotationLootItem; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.*; +import java.util.List; + +// Extends FlyingBossMob, which makes the boss not collide with any objects, etc. +public class ExampleBossMob extends FlyingBossMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + // Items this boss drops on rotation for each player + public static RotationLootItem uniqueDrops = RotationLootItem.privateLootRotation( + new LootItem("examplemeleesword"), + new LootItem("examplemagicstaff"), + new LootItem("examplesummonorb"), + new LootItem("examplerangedbow")); + + // The loot table that is private for each individual player + public static LootTable privateLootTable = new LootTable(uniqueDrops); + + // Deals 40 collision damage. We use this in a getter later + public static GameDamage collisionDamage = new GameDamage(40); + + // Similar to ExampleMob, we define an ability that can be run from the server + public EmptyMobAbility chargeSoundAbility; + + // MUST HAVE an empty constructor + public ExampleBossMob() { + super(2000); + setSpeed(50); + setFriction(3); + // Bosses don't save by default, but we could define that here if we want to +// this.shouldSave = 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); + // Swim mask values + swimMaskMove = 16; + swimMaskOffset = -2; + swimSinkOffset = -4; + + // Register our charge sound ability. It will be used in our boss AI + chargeSoundAbility = registerAbility(new EmptyMobAbility() { + @Override + protected void run() { + // Play a sound on the client when this ability is run from the server + if (isClient()) { + // Choose one of the ascended wizard sounds + GameSound sound = GameRandom.globalRandom.getOneOf( + GameResources.ascendedWizardHurt1, + GameResources.ascendedWizardHurt2, + GameResources.ascendedWizardHurt3 + ); + + // Define the effect to come from the mob itself + PositionSoundEffect effect = SoundEffect.effect(ExampleBossMob.this) + .volume(2f) // Let's make it loud + .falloffDistance(2000); + + // Play the sound + SoundManager.playSound(sound, effect); + } + } + }); + } + + // Init happens after the boss was added to a level + @Override + public void init() { + super.init(); + // Setup AI + ai = new BehaviourTreeAI<>(this, new ExampleBossAI<>()); + + // We want to play a spawn sound here. Only do so on the client + if (isClient()) { + // When passing a sound to somewhere for playing, you can use the SoundSettings class to + // specify stuff like pitch, falloff distance, volume, etc. + SoundSettings soundSettings = new SoundSettings(ExampleMod.EXAMPLE_SOUND) + .volume(0.8f) + .basePitch(1.0f) + .pitchVariance(0.08f) + .fallOffDistance(1500); // Large falloff distance since this is a boss spawned + + // Finally, play the sound with this mob as the emitter + soundSettings.play(this); + } + } + + // Client tick happens only on the clients game ticks (20 times a second) + @Override + public void clientTick() { + super.clientTick(); + + // Only show boss bar when the client player is close enough + if (isClientPlayerNearby()) { + EventStatusBarManager.registerMobHealthStatusBar(this); + } + + // Make sure the boss music is playing + SoundManager.setMusic(MusicRegistry.AscendedReturn, SoundManager.MusicPriority.EVENT, 1.5F); + } + + // Return the defined collision damage in this override method + @Override + public GameDamage getCollisionDamage(Mob target, boolean fromPacket, ServerClient packetSubmitter) { + return collisionDamage; + } + + // The private loot table, unique to each player + @Override + public LootTable getPrivateLootTable() { + return privateLootTable; + } + + // Called only on the client, when it should spawn death particles + @Override + public void spawnDeathParticles(float knockbackX, float knockbackY) { + // Spawn 4 flesh 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, // Sprite y coordinate + 32, // Sprite resolution + 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()); + // We always draw mobs so that their "feet" at the center of their collision/hotbox + 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() { + // Defines the speed at which this mobs animation plays (used in getAnimSprite(...)) + return 20; + } + +} diff --git a/src/main/java/examplemod/examples/mobs/ExampleHumanMob.java b/src/main/java/examplemod/examples/mobs/ExampleHumanMob.java new file mode 100644 index 0000000..0930a86 --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleHumanMob.java @@ -0,0 +1,36 @@ +package examplemod.examples.mobs; + +import necesse.engine.network.server.ServerClient; +import necesse.entity.mobs.friendly.human.humanShop.HumanShop; +import necesse.inventory.InventoryItem; + +import java.util.Collections; +import java.util.List; + +public class ExampleHumanMob extends HumanShop { + + // MUST HAVE an empty constructor + public ExampleHumanMob() { + super( + 500, // Max health when not part of a player settlement + 200, // Max health when part of a player settlement + "examplesettler" // The settler stringID registered in ExampleModSettlers + ); + + // Unlock the job type for THIS settler only + this.jobTypeHandler.getPriority("examplejobtype").disabledBySettler = false; + } + + // Cost to recruit this as a settler + @Override + public List getRecruitItems(ServerClient client) { + // If you return null, it means you cannot recruit them + + // If trapped, it's free to recruit (returns empty list) + if (isTrapped()) return Collections.emptyList(); + + // Simple recruit cost (you can make this random like vanilla does) + return Collections.singletonList(new InventoryItem("exampleitem", 10)); + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleMob.java b/src/main/java/examplemod/examples/mobs/ExampleMob.java similarity index 58% rename from src/main/java/examplemod/examples/ExampleMob.java rename to src/main/java/examplemod/examples/mobs/ExampleMob.java index c537747..1d4a626 100644 --- a/src/main/java/examplemod/examples/ExampleMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleMob.java @@ -1,15 +1,17 @@ -package examplemod.examples; +package examplemod.examples.mobs; +import examplemod.examples.ai.ExampleAI; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.engine.util.GameRandom; import necesse.entity.mobs.GameDamage; import necesse.entity.mobs.MobDrawable; import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ability.CoordinateMobAbility; import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; -import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserWandererAI; import necesse.entity.mobs.hostile.HostileMob; import necesse.entity.particle.FleshParticle; import necesse.entity.particle.Particle; +import necesse.entity.particle.SmokePuffParticle; import necesse.gfx.camera.GameCamera; import necesse.gfx.drawOptions.DrawOptions; import necesse.gfx.drawables.OrderableDrawables; @@ -28,9 +30,15 @@ public class ExampleMob extends HostileMob { 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 + // 50% chance to drop between 1-3 example items + ChanceLootItem.between(0.5f, "exampleitem", 1, 3) ); + // Here we define a mob ability. Mob abilities are an easy way for the server to run some logic and + // send it to the client over the network. + // In this case, we use a CoordinateMobAbility which allows us to send a coordinate. + public final CoordinateMobAbility teleportAbility; + // MUST HAVE an empty constructor public ExampleMob() { super(200); @@ -41,29 +49,59 @@ public ExampleMob() { collision = new Rectangle(-10, -7, 20, 14); hitBox = new Rectangle(-14, -12, 28, 24); selectBox = new Rectangle(-14, -7 - 34, 28, 48); + // Swim mask values + swimMaskMove = 16; + swimMaskOffset = -2; + swimSinkOffset = -4; + + // We construct and register our teleport ability in the constructor. It will be used in our AI. + teleportAbility = registerAbility(new CoordinateMobAbility() { + @Override + protected void run(int x, int y) { + if (isClient()) { + // If this is run from a client, spawn particles where we were and where we're teleporting + getLevel().entityManager.addParticle(new SmokePuffParticle(getLevel(), ExampleMob.this.x, ExampleMob.this.y, new Color(30, 165, 161)), Particle.GType.CRITICAL); + getLevel().entityManager.addParticle(new SmokePuffParticle(getLevel(), x, y, new Color(30, 165, 161)), Particle.GType.CRITICAL); + } + // Teleport to the position + setPos(x, y, true); + } + }); } + // Init happens after the mob was added to a level @Override public void init() { super.init(); // Setup AI - ai = new BehaviourTreeAI<>(this, new CollisionPlayerChaserWandererAI<>(null, 12 * 32, new GameDamage(25), 25, 40000)); + ai = new BehaviourTreeAI<>(this, new ExampleAI(12 * 32, new GameDamage(25), 25, 40_000) { + @Override + public boolean teleport(ExampleMob mob, int x, int y) { + // Use the teleport ability + mob.teleportAbility.runAndSend(x, y); + // And make sure we stop moving when teleported + getBlackboard().mover.stopMoving(mob); + return true; + } + }); } + // The regular loot table, shared between all players @Override public LootTable getLootTable() { return lootTable; } + // Called only on the client, when it should spawn death particles @Override public void spawnDeathParticles(float knockbackX, float knockbackY) { - // Spawn flesh debris particles + // Spawn 4 flesh 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, + 8, // Sprite y coordinate + 32, // Sprite resolution x, y, 20f, // Position knockbackX, knockbackY // Basically start speed of the particles ), Particle.GType.IMPORTANT_COSMETIC); @@ -75,6 +113,7 @@ protected void addDrawables(List list, OrderableDrawables tileList, 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()); + // We always draw mobs so that their "feet" at the center of their collision/hotbox int drawX = camera.getDrawX(x) - 32; int drawY = camera.getDrawY(y) - 51; @@ -105,5 +144,4 @@ public int getRockSpeed() { return 20; } - } 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..d218182 --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java @@ -0,0 +1,87 @@ +package examplemod.examples.mobs; + +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; + +import java.awt.*; +import java.util.List; + +public class ExampleSummonWeaponMob extends AttackingFollowingMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + public ExampleSummonWeaponMob() { + // Max health doesn't really matter in this case because the mob is not killable + super(20); + setSpeed(60); + setFriction(2); + attackCooldown = 500; + + collision = new Rectangle(-10, -7, 20, 14); + hitBox = new Rectangle(-12, -14, 24, 24); + selectBox = new Rectangle(-13, -14, 26, 24); + } + + @Override + public void init() { + super.init(); + + ai = new BehaviourTreeAI<>(this, + new PlayerFollowerCollisionChaserAI<>( + 18 * 32, // Enemy targeting range (18 tiles) + summonDamage, // This damage is set from the summon weapon before being spawned + 50, // Knockback + 500, // Cooldown between in milliseconds + 20 * 32, // When more than 20 tiles away, teleport to the player + 64 // Stop following when within 64 pixels of the player (if it cannot reach the exact follow position) + ) + ); + } + + @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/objectentity/ExampleJobObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java new file mode 100644 index 0000000..8dca4df --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java @@ -0,0 +1,127 @@ +package examplemod.examples.objectentity; + +import examplemod.examples.settlement.jobs.ExampleLevelJob; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.save.LoadData; +import necesse.engine.save.SaveData; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.level.gameObject.GameObject; +import necesse.level.maps.Level; +import necesse.level.maps.LevelObject; + +public class ExampleJobObjectEntity extends ObjectEntity { + + // Config variables which are global for all entities + // We define these as public static so that others can change them if they want to + public static int TILE_RADIUS = 15; + public static int TILES_PER_SECOND = 100; + + // The current state of the scan + // Since these values are only used by the server, we don't need to sync them with clients + // See in serverTick() how these are used + protected float tilesToScanBuffer; + protected int currentDeltaX; + protected int currentDeltaY; + + public ExampleJobObjectEntity(Level level, int tileX, int tileY) { + // The type we define here is used to verify corruption on level loading, etc. + // Make sure it's always the same for the same object + super(level, "examplejobobjectentity", tileX, tileY); + // By default, object entities will be saved onto a level. So we don't need to say this: + // shouldSave = true; + + // We initialize the current scan delta tiles to be the beginning of the radius + currentDeltaX = -TILE_RADIUS; + currentDeltaY = -TILE_RADIUS; + } + + @Override + public void addSaveData(SaveData save) { + super.addSaveData(save); + // Here we add the data we want to be persistent between loads, etc. + // Save data is a string key-value system, with the possibility of adding more + // save data branches as the value. SaveData have adders for all the basic stuff, + // but if you need to add something custom, you can create your own string to data parser + + // Here we want to save the buffer as well as the current scan tiles + save.addFloat("tilesToScanBuffer", tilesToScanBuffer); + save.addInt("currentDeltaX", currentDeltaX); + save.addInt("currentDeltaY", currentDeltaY); + } + + @Override + public void applyLoadData(LoadData save) { + super.applyLoadData(save); + // Here we load the data that was saved in addSaveData + // Something to keep in mind is that we should never trust the load data. Which means we should always + // expect data to be corrupt or missing. LoadData getters have default ways to handle this + + // This is how we load it: + tilesToScanBuffer = save.getFloat( + "tilesToScanBuffer", // The identifier we assigned in addSaveData + tilesToScanBuffer, // If the data is corrupted or missing, we will just keep the current value + false // We don't want to print a warning if the data is corrupted or missing + ); + // If we simply used save.getFloat("tilesToScanBuffer"), it would throw exceptions that we have to deal with + + // Same thing with current scan tiles + currentDeltaX = save.getInt("currentDeltaX", currentDeltaX, false); + currentDeltaY = save.getInt("currentDeltaY", currentDeltaY, false); + } + + @Override + public void serverTick() { + super.serverTick(); + + // We advance the scan evenly across game ticks. This makes sure that we have a smooth framerate and + // no stutters from high compute ticks + + // First we found out how many tiles per tick that is + float tilesPerTick = (float) TILES_PER_SECOND / TickManager.ticksPerSec; + // Next we add that number to our buffer + tilesToScanBuffer += tilesPerTick; + + // And for each tile we have left to tick, we do that + while (tilesToScanBuffer >= 1) { + tilesToScanBuffer -= 1; // Reduce the buffer as we're handling the ticks + + Level level = getLevel(); + // Calculate the current tile we want to process + int currentTileX = tileX + currentDeltaX; + int currentTileY = tileY + currentDeltaY; + + // Advance scan cursor (square area) + currentDeltaX++; + if (currentDeltaX > TILE_RADIUS) { + currentDeltaX = -TILE_RADIUS; + currentDeltaY++; + if (currentDeltaY > TILE_RADIUS) { + currentDeltaY = -TILE_RADIUS; + } + } + + // Handle the current tile + if (!level.isTileWithinBounds(currentTileX, currentTileY)) continue; + + GameObject object = level.getObject(currentTileX, currentTileY); + boolean isPlayerPlaced = level.objectLayer.isPlayerPlaced(currentTileX, currentTileY); + if (!isValidObject(object, isPlayerPlaced)) continue; + + // Add your example job + level.jobsLayer.addJob(new ExampleLevelJob(currentTileX, currentTileY, this)); + } + } + + public boolean isValidObject(GameObject object, boolean isPlayerPlaced) { + // Don’t clear decorative / player-placed grass + if (isPlayerPlaced) return false; + // Only clear grass objects + return object.isGrass; + } + + // Helper method used in ExampleLevelJob + public boolean isValidLevelObject(LevelObject levelObject) { + return isValidObject(levelObject.object, levelObject.isPlayerPlaced); + } + +} diff --git a/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java new file mode 100644 index 0000000..c6068bd --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java @@ -0,0 +1,76 @@ +package examplemod.examples.objectentity; + +import examplemod.examples.events.ExampleLevelEvent; +import necesse.entity.mobs.PlayerMob; +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) + protected boolean isPressed = false; + + // Small cooldown to avoid rapid re-triggers if the player jitters on the edge + protected long nextTriggerTime = 0L; + + public ExampleObjectEntity(Level level, int tileX, int tileY) { + // The type we define here is used to verify corruption on level loading, etc. + // Make sure it's always the same for the same object + super(level, "exampleeventtrigger", tileX, tileY); + + // If the cooldown is significant, it may be worth to save it using addSaveData and applyLoadData + // But in this case there's really no need for it + shouldSave = false; + } + + @Override + public void serverTick() { + super.serverTick(); + // serverTick runs on the server and main menu at 20 ticks per second (TickManager.ticksPerSec) + + // Get the level + Level level = getLevel(); + + // Get the current time (used later for cooldown management) + long currentTime = level.getTime(); + + // The hitbox covering the full tile under this object + // Level positions are different from tile positions + // Each tile is 32x32 in size, so here we convert from tile to level position + Rectangle hitbox = new Rectangle( + tileX * 32, + tileY * 32, + 32, + 32 + ); + + // Here we iterate through all the regions which the hitbox overlaps with + // We add 1 extra region range to avoid edge cases of players being on the edge of regions, etc. + // We then check if any of the players collision intersects with the hitbox + PlayerMob target = level.entityManager.players.streamInRegionsShape(hitbox, 1) + .filter(player -> player.getCollision().intersects(hitbox)) + .findFirst() + .orElse(null); + + // If a target was found, and it is not currently pressed or on cooldown + if (target != null && !isPressed && currentTime >= nextTriggerTime) { + isPressed = true; + nextTriggerTime = currentTime + 300; // 300 milliseconds cooldown + + /* + * This is an example of triggering a level event (in this case ExampleLevelEvent) + * Using events.add(...) will add it to the servers level and send it over to other clients + * Using events.addHidden(...) will just add it to the servers level without sending it + */ + level.entityManager.events.add(new ExampleLevelEvent(target.getServerClient(), tileX, tileY)); + } + // Reset when nobody is standing on it + if (target == null) { + isPressed = false; + } + } + +} + diff --git a/src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java new file mode 100644 index 0000000..2317fc1 --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java @@ -0,0 +1,77 @@ +package examplemod.examples.objectentity; + +import java.awt.Point; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.objectEntity.TrapObjectEntity; +import necesse.entity.projectile.TrapArrowProjectile; +import necesse.level.maps.Level; + +/* + * Arrow trap logic. + * When this trap is triggered by a wire, it shoots an arrow in the direction it faces. + */ +public class ExampleTrapObjectEntity extends TrapObjectEntity { + + // The damage the arrow will deal when it hits something. + public static final GameDamage DAMAGE = new GameDamage(40.0F, 100.0F, 0.0F, 2.0F, 1.0F); + + public ExampleTrapObjectEntity(Level level, int x, int y) { + // Cooldown in milliseconds (1000ms = 1 second). + super(level, x, y, 1000L); + + // This object entity is meant to be recreated, not saved. + this.shouldSave = false; + } + + @Override + public void triggerTrap(int wireID, int dir) { + // Only the server should spawn projectiles. + // Also, don't fire again while we're still on cooldown. + if (isClient() || onCooldown()) return; + + // If a different wire is active at the same time, ignore this trigger. + if (otherWireActive(wireID)) return; + + // Find the tile position the trap should fire from. + Point tilePos = getPos(this.tileX, this.tileY, dir); + + // Turn the direction number (0..3) into a simple (x,y) direction. + Point d = getDir(dir); + + // Convert tile coordinates into pixel coordinates (32 pixels per tile). + int xPos = tilePos.x * 32; + int yPos = tilePos.y * 32; + + // Shift the spawn position a bit so the arrow looks like it comes from the correct side. + if (d.x == 0) xPos += 16; // shooting up/down: centre of the tile + else if (d.x == -1) xPos += 30; // shooting left: near the left edge + else if (d.x == 1) xPos += 2; // shooting right: near the right edge + + if (d.y == 0) yPos += 16; // shooting left/right: centre of the tile + else if (d.y == -1) yPos += 30; // shooting up: near the top edge + else if (d.y == 1) yPos += 2; // shooting down: near the bottom edge + + // Create and spawn the projectile. + // The "target" is just one step in the direction we're firing. + getLevel().entityManager.projectiles.add(new TrapArrowProjectile( + xPos, yPos, + xPos + d.x, + yPos + d.y, + DAMAGE, + null + )); + + // Start the cooldown so it can't fire again instantly. + startCooldown(); + } + + // Converts 0..3 into up/right/down/left. + private Point getDir(int dir) { + if (dir == 0) return new Point(0, -1); // up + if (dir == 1) return new Point(1, 0); // right + if (dir == 2) return new Point(0, 1); // down + if (dir == 3) return new Point(-1, 0); // left + return new Point(0, 0); + } +} \ 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..6d175bf --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -0,0 +1,19 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.RockObject; + +import java.awt.*; + +public class ExampleBaseRockObject extends RockObject { + + public ExampleBaseRockObject() { + super( + "examplebaserock", // Texture for the base rock + new Color(92, 37, 23), // Minimap color + "examplestone", // Dropped stone stringID + "objects", "landscaping" // Item categories + ); + this.toolTier = 0; // Tier of pickaxe required to mine this rock + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExampleConfigObject.java b/src/main/java/examplemod/examples/objects/ExampleConfigObject.java new file mode 100644 index 0000000..67c6a37 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleConfigObject.java @@ -0,0 +1,100 @@ +package examplemod.examples.objects; + +import examplemod.ExampleMod; +import necesse.engine.Settings; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.network.server.ServerClient; +import necesse.entity.mobs.PlayerMob; +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.*; +import java.util.List; + +/** + * See ExampleObject for a simple object and ExampleWorkstationObject for a more complex object with + * explanations for code without comments here + * This object is pretty basic: + * - Draws a 32x32 sprite in the world + * - You can interact with this object to change the example settings + */ +public class ExampleConfigObject extends GameObject { + + private GameTexture texture; + + public ExampleConfigObject() { + super(new Rectangle(32, 32)); + this.isSolid = true; + } + + @Override + public void loadTextures() { + super.loadTextures(); + texture = GameTexture.fromFile("objects/exampleconfigobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + TextureDrawOptionsEnd opts = texture.initDraw() + .light(light) + .pos(drawX, drawY); + + // We add it to the tile list instead of the LevelSortedDrawable list + // This makes it draw right after all the tiles have been drawn, but before any other objects + tileList.add(tm -> opts.draw()); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + texture.initDraw() + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public boolean canInteract(Level level, int x, int y, PlayerMob player) { + return true; + } + + @Override + public void interact(Level level, int x, int y, PlayerMob player) { + // This interact method will run both on the server and the client. In this case we only want something + // to happen when runs on the server, so we do this if check below. + + // In general, when the player does an action you want to verify that it's a valid action on the server. + // The base game already do this in the case of the interact method. All the client does is send a packet + // that they want to interact with this object. Then the server checks if the object actually exists, + // if they're in range etc. You can see this happens in the PacketObjectInteract class. + // And the server then runs the effect that happens when a client interacted with this object + if (player.isServerClient()) { + // Increment server settings value + ExampleMod.SETTINGS.exampleInt += 1; + + // Save server settings back to disk (server.cfg + cfg/mods/.cfg) + Settings.saveServerSettings(); + + // Send the message to the client that sent the packet about the settings + ServerClient client = player.getServerClient(); + client.sendChatMessage("[ExampleMod] Server config updated (saved):"); + client.sendChatMessage("exampleBoolean: " + ExampleMod.SETTINGS.exampleBoolean); + client.sendChatMessage("exampleInt: " + ExampleMod.SETTINGS.exampleInt); + client.sendChatMessage("exampleString: " + ExampleMod.SETTINGS.exampleString); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleEventTriggerObject.java b/src/main/java/examplemod/examples/objects/ExampleEventTriggerObject.java new file mode 100644 index 0000000..d6224ef --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleEventTriggerObject.java @@ -0,0 +1,77 @@ +package examplemod.examples.objects; + +import examplemod.examples.objectentity.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.*; +import java.util.List; + +/** + * See ExampleObject for a simple object and ExampleWorkstationObject for a more complex object with + * explanations for code without comments here + * This object is pretty basic: + * - Draws a 32x32 sprite in the world + * - Uses ExampleObjectEntity to work as a "pressureplate" which triggers an ExampleLevelEvent + */ +public class ExampleEventTriggerObject extends GameObject { + + private GameTexture texture; + + public ExampleEventTriggerObject() { + super(new Rectangle()); // No collision + } + + @Override + public void loadTextures() { + super.loadTextures(); + texture = GameTexture.fromFile("objects/exampleeventtriggerobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + TextureDrawOptionsEnd opts = texture.initDraw() + .light(light) + .pos(drawX, drawY); + + // We add it to the tile list instead of the LevelSortedDrawable list + // This makes it draw right after all the tiles have been drawn, but before any other objects + tileList.add(tm -> opts.draw()); + } + + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + texture.initDraw() + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + // GameObject are static objects, sharing data between all other objects of that same type in the world + // If we want custom data for a specific object, we have to assign it an ObjectEntity + // Each ObjectEntity is unique to the specific tile and will allow us to define data like items in + // a chest, cooldown for a trigger, etc. + return new ExampleObjectEntity(level, x, y); + } + +} 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..b9c9b53 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java @@ -0,0 +1,32 @@ +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; + +import java.awt.*; + +public class ExampleGrassObject extends GrassObject { + + public ExampleGrassObject() { + // "examplegrass" is the texture name + // 2 = max density (how many adjacent grass objects can be next to each other before they stop growing) + super("examplegrass", 2); + mapColor = new Color(112, 0, 109); // Minimap color + } + + @Override + public LootTable getLootTable(Level level, int layerID, int tileX, int tileY) { + if (level.objectLayer.isPlayerPlaced(tileX, tileY)) { + // If the grass is player placed (like from the landscaping station), it should just drop the grass itself + return super.getLootTable(level, layerID, tileX, tileY); + } else { + // Else 4% chance to drop an example grass seed + return new LootTable( + new ChanceLootItem(0.04f, "examplegrassseed") + ); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleJobObject.java b/src/main/java/examplemod/examples/objects/ExampleJobObject.java new file mode 100644 index 0000000..637efef --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleJobObject.java @@ -0,0 +1,77 @@ +package examplemod.examples.objects; + +import examplemod.examples.objectentity.ExampleJobObjectEntity; +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.*; +import java.util.List; + +/** + * See ExampleObject for a simple object and ExampleWorkstationObject for a more complex object with + * explanations for code without comments here + * This object is pretty basic: + * - Draws a 32x32 sprite in the world + * - Uses ExampleJobObjectEntity to add ExampleLevelJob to grass around itself + */ +public class ExampleJobObject extends GameObject { + + private GameTexture texture; + + public ExampleJobObject() { + super(new Rectangle(32, 32)); + mapColor = new Color(120, 170, 120); + } + + @Override + public void loadTextures() { + super.loadTextures(); + texture = GameTexture.fromFile("objects/examplejobobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + TextureDrawOptionsEnd opts = texture.initDraw() + .light(light) + .pos(drawX, drawY); + + // We add it to the tile list instead of the LevelSortedDrawable list + // This makes it draw right after all the tiles have been drawn, but before any other objects + tileList.add(tm -> opts.draw()); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + texture.initDraw() + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + // GameObject are static objects, sharing data between all other objects of that same type in the world + // If we want custom data for a specific object, we have to assign it an ObjectEntity + // Each ObjectEntity is unique to the specific tile and will allow us to define data like items in + // a chest, cooldown for a trigger, etc. + return new ExampleJobObjectEntity(level, x, y); + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExampleObject.java b/src/main/java/examplemod/examples/objects/ExampleObject.java new file mode 100644 index 0000000..d384558 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleObject.java @@ -0,0 +1,117 @@ +package examplemod.examples.objects; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.texture.TextureDrawOptions; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.item.toolItem.ToolType; +import necesse.level.gameObject.GameObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.*; +import java.util.List; + +public class ExampleObject extends GameObject { + + // This is just a simple object cosmetic object, explaining a bit of how objects work + + // All objects run the loadTextures() method when the game loads all of its resources + // Dedicated servers does not load or draw any textures (this will always be null) + // Here we just declare the variable that will be loaded later + protected GameTexture texture; + + public ExampleObject() { + // In the super, we define the collision relative to the tile it's placed on + // Tiles are 32x32 pixels in size, so defining an area outside that bounds does not work + // In this case, collision is in the center of the tile, but does not cover it completely (24x24, centered) + super(new Rectangle(4, 4, 24, 24)); + + // By default, you can target objects only by hovering over their tile location (32x32) + // Here we change that, so that the hover hitbox also covers the tile above it + hoverHitbox = new Rectangle(0, -32, 32, 64); + + // It can be broken by all tools + toolType = ToolType.ALL; + + // It lets light pass through it + isLightTransparent = true; + + // Defines what color it has on the minimap, etc. + // It also defines which color particles come out when we break it + // This can be overridden by setting debrisColor field though + mapColor = new Color(31, 150, 148); // Also applies as debris color if not set + + // We set the category that this object should be part of + // You can see the registered category stringIDs in the ItemCategory class. Link: + /// {@link necesse.inventory.item.ItemCategory} + setItemCategory("objects", "columns"); + // Same with crafting category (where they are displayed in the workstation) + setCraftingCategory("objects", "columns"); + } + + @Override + public void loadTextures() { + super.loadTextures(); + // As explained above, here we load the texture from the objects folder in resources + texture = GameTexture.fromFile("objects/exampleobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, Level level, int tileX, int tileY, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + // Necesse has an asynchronous level rendering pipeline. This means that we calculate and setup as + // much as possible for the next frame, at the same time we are rendering the previous frame + // This also means that everything happening in here will be subject to concurrency, so anything + // that is reused has to be considered for that + + // First we collect the variables we need for setup + // The screen coordinates, relative the to camera we should draw it at + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // The current lighting of the tile + GameLight light = level.getLightLevel(tileX, tileY); + + // The rotation of the object (not used in this example) + // int rotation = level.getObjectRotation(tileX, tileY); + + // The most simple form of texture drawables setup + // We initialize the texture for drawing, set the lighting and the position on the screen + // We could select a specific part of the texture by calling texture.initDraw().sprite(...) + // See ExampleWorkstationObject for a more complex example of this + TextureDrawOptions options = texture.initDraw() + .light(light) + .pos(drawX, drawY - texture.getHeight() + 32); + + // Necesse draws objects using LevelSortedDrawable so they sort correctly in front or behind other things + // We add the drawable entry for this tile, and inside it, we draw what we have set up + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + // Basically where this will be sorted on the Y axis (when it will be behind the player etc.) + // Should be in [0 - 32] range + return 16; // 16 is the center of the tile + } + + @Override + public void draw(TickManager tickManager) { + options.draw(); + } + }); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, PlayerMob player, GameCamera camera) { + // Drawing preview is very similar to addDrawables, however this time we don't add + // the drawables to a list, we just draw them directly with an alpha and no lighting + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + texture.initDraw() + .alpha(alpha) + .draw(drawX, drawY - texture.getHeight() + 32); + } + +} 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..cf3b2cb --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -0,0 +1,28 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.RockObject; +import necesse.level.gameObject.RockOreObject; + +import java.awt.*; + +/** + * Example ore rock that uses our ExampleIncursionDeepRockObject as its parent rock. + */ +public class ExampleOreRockObject extends RockOreObject { + + public ExampleOreRockObject(RockObject parentRock) { + super( + parentRock, + "oremask", // Ore mask image + "exampleore", // Ore texture name + new Color(90, 40, 160), // Minimap Color + "exampleore", // Dropped ore stringID + 1, // Min ores dropped + 3, // Max ores dropped + 2, // Placed dropped ore - not actually ued right now + true, // Is incursion extraction mission object + "objects", "landscaping" // Item categories + ); + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java new file mode 100644 index 0000000..8e5dcdd --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java @@ -0,0 +1,20 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.MaskedPressurePlateObject; + +import java.awt.*; + +public class ExamplePressurePlateObject extends MaskedPressurePlateObject { + + public ExamplePressurePlateObject() { + super( + "pressureplatemask", // Texture mask name + "exampletile", // Tile texture name + new Color(120, 80, 200) // Minimap color + ); + + // MaskedPressurePlateObject sets the important flags internally (including isPressurePlate) + // and uses a default trigger hitbox through its object entity. + } + +} \ No newline at end of file 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..7349023 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java @@ -0,0 +1,31 @@ +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", // Texture name + "examplelog", // Log item stringID + "examplesapling", // Sapling stringID + new Color(87, 6, 86), // Minimap color + 45, // Width of the tree's crown texture (where dropped leaves will spawn from) + 60, // Min height that leaves will be dropped from + 110, // Max height that leaves will be dropped from + "exampleleaves" // Leaves texture name + ); + } + + // Optional: override drops if you want something different than the base TreeObject default + // Base TreeObject drops 1-2 saplings + 4-5 logs + @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..b69a11e --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java @@ -0,0 +1,21 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.TreeSaplingObject; + +import java.awt.*; + +public class ExampleTreeSaplingObject extends TreeSaplingObject { + + public ExampleTreeSaplingObject(){ + super( + "examplesapling", // Texture name, + new Color(122, 0, 121), // The map and debris color + "exampletree", // Grown object stringID + 30 * 60, // Min grow time in seconds - 30 minutes + 45 * 60, // Max grow time in seconds - 45 minutes + true, // Can be used as "Any sapling" ingredient + "examplegrasstile" + ); + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java b/src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java new file mode 100644 index 0000000..288c045 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java @@ -0,0 +1,26 @@ +package examplemod.examples.objects; + +import examplemod.examples.objectentity.ExampleTrapObjectEntity; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.level.gameObject.WallObject; +import necesse.level.gameObject.WallTrapObject; +import necesse.level.maps.Level; + +/* + * A wall trap you can place in the world. + * It uses "examplearrowtrap" as its texture name. + */ +public class ExampleWallTrapObject extends WallTrapObject { + + public ExampleWallTrapObject(WallObject wallObject) { + // Tells the game which texture to use (objects/examplearrowtrap.png) + super(wallObject, "examplewalltrap"); + } + + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + // Creates the object entity that handles the trap behavior. + return new ExampleTrapObjectEntity(level, x, y); + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstation2Object.java b/src/main/java/examplemod/examples/objects/ExampleWorkstation2Object.java new file mode 100644 index 0000000..af9f0de --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstation2Object.java @@ -0,0 +1,143 @@ +package examplemod.examples.objects; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptionsList; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.gameObject.container.CraftingStationObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; +import necesse.level.maps.multiTile.MultiTile; +import necesse.level.maps.multiTile.SidedRotationMultiTile; + +import java.awt.*; +import java.util.List; + +public class ExampleWorkstation2Object extends CraftingStationObject { + + // This is the secondary object to ExampleWorkstationObject + // See that class for explanations + // Although, check out the alternated version of getMultiTile below + + public GameTexture texture; + protected int counterID; + + public ExampleWorkstation2Object() { + super(new Rectangle(32, 32)); + mapColor = new Color(87, 22, 76); + isLightTransparent = true; + hoverHitbox = new Rectangle(0, -16, 32, 48); + } + + @Override + public void loadTextures() { + texture = GameTexture.fromFile("objects/exampleworkstation"); + } + + @Override + public MultiTile getMultiTile(int rotation) { + return new SidedRotationMultiTile( + 1, 0, // The position of this object is different from the master object + 2, 1, // Same size + rotation, + false, // Not the master this time + counterID, getID() // objectIDs are the same final IDs assigned (swapped variables) + ); + } + + @Override + public Rectangle getCollision(Level level, int x, int y, int rotation) { + // Basically reverse of the master object + if (rotation == 0) { // Facing north + return new Rectangle(x * 32, y * 32 + 6, 26, 20); + } else if (rotation == 1) { // Facing east + return new Rectangle(x * 32 + 4, y * 32, 24, 26); + } else if (rotation == 2) { // Facing south + return new Rectangle(x * 32 + 6, y * 32 + 6, 26, 20); + } else { // Facing west + return new Rectangle(x * 32 + 4, y * 32 + 4, 24, 28); + } + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, + TickManager tickManager, GameCamera camera, PlayerMob perspective) { + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + GameLight light = level.getLightLevel(tileX, tileY); + int rotation = level.getObjectRotation(tileX, tileY); + + DrawOptionsList options = new DrawOptionsList(); + if (rotation == 0) { // Facing north + options.add(texture.initDraw() + .section(32, 2 * 32, 3 * 32, 5 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } else if (rotation == 1) { // Facing east + options.add(texture.initDraw() + .section(0, 32, 2 * 32, 3 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); + } else if (rotation == 2) { // Facing south + options.add(texture.initDraw() + .section(0, 32, 5 * 32, 7 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } else { // Facing west + options.add(texture.initDraw() + .section(32, 2 * 32, 0, 2 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } + + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + return 16; + } + + @Override + public void draw(TickManager tickManager) { + options.draw(); + } + }); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, + float alpha, PlayerMob player, GameCamera camera) { + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + if (rotation == 0) { // Facing north + texture.initDraw() + .section(32, 2 * 32, 3 * 32, 5 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); + } else if (rotation == 1) { // Facing east + texture.initDraw() + .section(0, 32, 2 * 32, 3 * 32) + .alpha(alpha) + .draw(drawX, drawY); + } else if (rotation == 2) { // Facing south + texture.initDraw() + .section(0, 32, 5 * 32, 7 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); + } else { // Facing west + texture.initDraw() + .section(32, 2 * 32, 0, 2 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstationObject.java b/src/main/java/examplemod/examples/objects/ExampleWorkstationObject.java new file mode 100644 index 0000000..d35124b --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstationObject.java @@ -0,0 +1,228 @@ +package examplemod.examples.objects; + +import examplemod.Loaders.ExampleModTech; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.registries.ObjectRegistry; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptionsList; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.recipe.Tech; +import necesse.level.gameObject.container.CraftingStationObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; +import necesse.level.maps.multiTile.MultiTile; +import necesse.level.maps.multiTile.SidedRotationMultiTile; + +import java.awt.*; +import java.util.List; + +public class ExampleWorkstationObject extends CraftingStationObject { + + // This class is also an example of a multi tile object. Since the workstation takes up 2x1 tiles. + // The other object is stored in ExampleWorkstation2Object + + // First we assign our variables we will be using later + + // All objects run the loadTextures() method when the game loads all of its resources + // Dedicated servers does not load or draw any textures (this will always be null) + // Here we just declare the variable that will be loaded later + public GameTexture texture; + + // Here we declare the objectID for the other object (ExampleWorkstation2Object), which will be assigned later + protected int counterID; + + public ExampleWorkstationObject() { + super(new Rectangle(32, 32)); + mapColor = new Color(87, 22, 76); + isLightTransparent = true; + + // Here we change the hover hitbox to be 16 tiles higher than the tile it is at + hoverHitbox = new Rectangle(0, -16, 32, 48); + } + + @Override + public void loadTextures() { + // As explained above, here we load the texture from the objects folder + texture = GameTexture.fromFile("objects/exampleworkstation"); + } + + @Override + public Tech[] getCraftingTechs() { + // Here we define which crafting techs we want this crafting station to be able to craft + // In this case, we use our own registered example tech + return new Tech[] { ExampleModTech.EXAMPLE_TECH }; + } + + @Override + public MultiTile getMultiTile(int rotation) { + // Since this is a multi tile, we here have to define how that multi tile behaves + // Both in terms of how big it is, and also what happens when it is rotated/mirrored in presets, etc. + + // We use SidedRotationMultiTile, because that fixes offset position when mirrored in presets + return new SidedRotationMultiTile( + 0, 0, // This object is placed at (0,0) in the multi tile + 2, 1, // The entire multi tile is 2 tiles wide and 1 tile high + rotation, + true, // This object is the master object (the one that's placed, etc.) + // Lastly, we define the object IDs that is part of this multi tile. The total parameters + // we give here have to match the total size of the multi tile. In this case, + // that is 2*1 = 2. If the multi tile was 2x2 tiles, it would be 4, etc. + // The other which we add them matter as well. It has to first be the top left one, then + // the one to the right of that, wrapping around and starting on the next row. If this was + // a 2x2 multi tile, it would look like this: + // topLeftID, topRightID, bottomLeftID, bottomRightID + + // It's here we use the counterID assigned when we register the objects + getID(), counterID + ); + } + + @Override + public Rectangle getCollision(Level level, int x, int y, int rotation) { + // Since we want the collision to be different based on rotation, we override it here and + // return the desired collision + // Remember that the rectangle we return should always be within the 32x32 tile size + + if (rotation == 0) { // Facing north + // A shorter/wider box, shifted in from left and down. + // Starts 6px in and 6px down, 26px wide, 20px tall. + return new Rectangle(x * 32 + 6, y * 32 + 6, 26, 20); + } else if (rotation == 1) { // Facing east + // The tallest version (almost fills the tile vertically). + // Starts 4px in and 4px down, 24px wide, 28px tall. + return new Rectangle(x * 32 + 4, y * 32 + 4, 24, 28); + } else if (rotation == 2) { // Facing south + // Similar size to rotation 1 but shifted left a bit. + // Starts at the left edge, 6px down, 26px wide, 20px tall. + return new Rectangle(x * 32, y * 32 + 6, 26, 20); + } else { // Facing west + // A taller box that starts at the top of the tile. + // 4px inset from the left, 24px wide, 26px tall. + return new Rectangle(x * 32 + 4, y * 32, 24, 26); + } + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, + TickManager tickManager, GameCamera camera, PlayerMob perspective) { + // This is where we setup the drawables and add them to the drawables list for the next frame + + // First we collect the variables we need for setup + // The screen coordinates, relative the to camera we should draw it at + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // The current lighting of the tile + GameLight light = level.getLightLevel(tileX, tileY); + + // The rotation of the object + int rotation = level.getObjectRotation(tileX, tileY); + + // Now we setup our draw options list and iterate through our rotations to add + // the correct draw options for each one + DrawOptionsList options = new DrawOptionsList(); + if (rotation == 0) { // Facing north + // Here's what's going on: + // First we initialize a draw of the loaded texture + // Next, we assign which pixels from that texture should be drawn + // Next, we add the damage overlay (when you mine it) + // Next, we assign lighting to the drawn texture + // And lastly, we position the drawn texture on the screen + // That is then added to the draw options list, which is used later for actually drawing it + + // The values passed into sections and pos is just me looking at the raw texture, and figuring + // out which pixels should be drawn and which offset it should be drawn with + options.add(texture.initDraw() + .section(0, 32, 3 * 32, 5 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } else if (rotation == 1) { // Facing east + options.add(texture.initDraw() + .section(0, 32, 0, 2 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } else if (rotation == 2) { // Facing south + options.add(texture.initDraw() + .section(32, 2 * 32, 5 * 32, 7 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + } else { // Facing west + options.add(texture.initDraw() + .section(32, 2 * 32, 2 * 32, 3 * 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); + } + + // Necesse draws objects using LevelSortedDrawable so they sort correctly in front or behind other things + // We add the drawable entry for this tile, and inside it, we draw our options list + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + // Draw order within the tile + // 16 = middle of tile because 1 tile = 32 + return 16; + } + + @Override + public void draw(TickManager tickManager) { + // Actually draw everything we queued up above + options.draw(); + } + }); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, + float alpha, PlayerMob player, GameCamera camera) { + // Drawing preview is very similar to addDrawables, however this time we don't add + // the drawables to a list, we just draw them directly with an alpha and no lighting + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + if (rotation == 0) { // Facing north + texture.initDraw() + .section(0, 32, 3 * 32, 5 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); // Instead of assigning a screen position, we draw it directly + } else if (rotation == 1) { // Facing east + texture.initDraw() + .section(0, 32, 0, 2 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); + } else if (rotation == 2) { // Facing south + texture.initDraw() + .section(32, 2 * 32, 5 * 32, 7 * 32) + .alpha(alpha) + .draw(drawX, drawY - 32); + } else { // Facing west + texture.initDraw() + .section(32, 2 * 32, 2 * 32, 3 * 32) + .alpha(alpha) + .draw(drawX, drawY); + } + } + + // Call this from your mod init to register BOTH pieces + public static int[] register() { + ExampleWorkstationObject main = new ExampleWorkstationObject(); + ExampleWorkstation2Object part = new ExampleWorkstation2Object(); + + int mainID = ObjectRegistry.registerObject("exampleworkstation", main, 10f, true); + int partID = ObjectRegistry.registerObject("exampleworkstation2", part, 0f, false); + + // Link them together (this is the key) + main.counterID = partID; + part.counterID = mainID; + + return new int[] { mainID, partID }; + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExamplePacket.java b/src/main/java/examplemod/examples/packets/ExamplePacket.java similarity index 76% rename from src/main/java/examplemod/examples/ExamplePacket.java rename to src/main/java/examplemod/examples/packets/ExamplePacket.java index f0255c8..44c0f29 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; @@ -27,6 +27,7 @@ public ExamplePacket(byte[] data) { someContent = reader.getNextContentPacket(); } + // Used to construct the packet to be sent from the server public ExamplePacket(ServerClient client, int someInteger, boolean someBoolean, String someString, Packet someContent) { this.playerSlot = client.slot; this.someInteger = someInteger; @@ -44,6 +45,7 @@ public ExamplePacket(ServerClient client, int someInteger, boolean someBoolean, // Examples how to send packets: // client.sendPacket(this); // To a single client +// server.network.sendToClientsWithEntity(packet, mob/player); // To all clients that have the specific entity loaded // server.network.sendToAllClients(packet); // To all clients } @@ -51,6 +53,14 @@ public ExamplePacket(ServerClient client, int someInteger, boolean someBoolean, public void processClient(NetworkPacket packet, Client client) { // Do some stuff with the packet } + + // You can also override processServer if you want the packet to be processed on the server when received + // from a client, but for this example we'll just have it processed on the client +// @Override +// public void processServer(NetworkPacket packet, Server server, ServerClient client) { +// super.processServer(packet, server, client); +// } + } diff --git a/src/main/java/examplemod/examples/ExampleConstructorPatch.java b/src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleConstructorPatch.java rename to src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java index 7b48c45..eb20d2f 100644 --- a/src/main/java/examplemod/examples/ExampleConstructorPatch.java +++ b/src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.patches; import necesse.engine.modLoader.annotations.ModConstructorPatch; import necesse.entity.mobs.friendly.critters.RabbitMob; @@ -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/ExampleMethodPatch.java b/src/main/java/examplemod/examples/patches/ExampleMethodPatch.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleMethodPatch.java rename to src/main/java/examplemod/examples/patches/ExampleMethodPatch.java index 00c5082..6bc6809 100644 --- a/src/main/java/examplemod/examples/ExampleMethodPatch.java +++ b/src/main/java/examplemod/examples/patches/ExampleMethodPatch.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.patches; import necesse.engine.modLoader.annotations.ModMethodPatch; import necesse.inventory.lootTable.LootTable; diff --git a/src/main/java/examplemod/examples/presets/ExampleCodePreset.java b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java new file mode 100644 index 0000000..c2d1d43 --- /dev/null +++ b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java @@ -0,0 +1,128 @@ +package examplemod.examples.presets; + +import examplemod.examples.ExampleLootTable; +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 ExampleCodePreset 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 ExampleCodePreset(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 centre. + * + * 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): + * + * "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 + * - If the tile is NOT a floor, then this returns true + * + * 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/presets/ExamplePreset.java b/src/main/java/examplemod/examples/presets/ExamplePreset.java new file mode 100644 index 0000000..6b3da8b --- /dev/null +++ b/src/main/java/examplemod/examples/presets/ExamplePreset.java @@ -0,0 +1,114 @@ +package examplemod.examples.presets; + +import examplemod.examples.ExampleLootTable; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +/** + * ExamplePreset (Script-based) + * This preset is the same idea as the code-built room, but it is created using a big text string + * in Necesse's "PRESET script" format. + */ +public class ExamplePreset extends Preset { + + /** + * 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 + * + * width / height + * - Size of the structure. + * + * tileIDs + tiles + * - "tileIDs" is a list 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 + * - "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, 1436, examplewall, 298, walltorch]," + + "objects=[1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 298, 0, 0, 0, 0, 0, 0, 0, 298, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 290, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 298, 0, 0, 0, 0, 0, 0, 0, 298, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436]," + + "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. + * + * The idea here is: + * 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 then placement fails + * - if isFloor is false, !isFloor is true then 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/projectiles/ExampleArrowProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java new file mode 100644 index 0000000..a9f3418 --- /dev/null +++ b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java @@ -0,0 +1,149 @@ +package examplemod.examples.projectiles; + +import examplemod.Loaders.ExampleModBuffs; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.util.GameRandom; +import necesse.entity.levelEvent.mobAbilityLevelEvent.AmethystGlyphEvent; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.MobBeforeHitCalculatedEvent; +import necesse.entity.mobs.MobBeforeHitEvent; +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; + +import java.awt.*; +import java.util.List; +import java.util.stream.Stream; + +public class ExampleArrowProjectile extends Projectile { + + // Must have an empty constructor for the registry to accept it + public ExampleArrowProjectile() { + } + + @Override + public void init() { + super.init(); + // Projectile starts at height 18 (roughly where the players bow is) + height = 18; + + // Height reduces over time as the projectile travels + heightBasedOnDistance = true; + + // Has 8 pixels of width for collision with terrain and targets + setWidth(8); + + // This arrow does not do damage to targets. Instead, we apply a buff in doHitLogic + doesImpactDamage = false; + } + + @Override + protected Stream streamTargets(Mob owner, Shape hitBounds) { + // If owner is null, use default targeting + if (owner == null) { + return super.streamTargets(owner, hitBounds); + } + + // This arrow is supposed to hit friendly targets, similar to gem staves/glyphs + // The Amethyst Glyph event has a static method we can use to stream targets + return AmethystGlyphEvent.streamBuffableTargets(getLevel(), hitBounds, owner) + .filter(mob -> mob != owner); // Don't hit the owner + } + + @Override + public boolean canHit(Mob mob) { + // Since we can hit all our targets from streamTargets(..), we just return true here + return true; + } + + @Override + public void doHitLogic(Mob mob, LevelObjectHit object, float x, float y) { + super.doHitLogic(mob, object, x, y); + + // doHitLogic happens both on server and client. + // If mob is null, it means it hit an object or traveled max distance + // If object is null, it means it hit a mob or traveled max distance + if (!isServer() || mob == null) return; + + // We want this arrow to heal the mob it hits + // Heal amount will be the damage the arrow does + // And the health will be given over a duration + // Game design wise, this might be a bit OP with something like greatbows. But just as an example :D + + // We get the owner of the projectile for later use + Mob owner = getOwner(); + + // To calculate the actual damage, we use the hit events similar to how mobs does it + // This ensures that we calculate crit chance and crit damage correctly + MobBeforeHitEvent hitEvent = new MobBeforeHitEvent(mob, owner, getDamage(), 0f, 0f, 0f); + MobBeforeHitCalculatedEvent calculatedEvent = new MobBeforeHitCalculatedEvent(hitEvent); + + // Now we can get the final damage this hit should do as a number + int totalDamage = calculatedEvent.damage; + + // And we want to calculate how much healing that is per game tick + float duration = 4; // Heal duration in seconds + float healthPerSecond = totalDamage / duration; + float healthPerGameTick = healthPerSecond / TickManager.ticksPerSec; + + // We then use our registered arrow buff to actually apply the healing + // To give a buff to a player, we have to create an ActiveBuff that contains the duration and data we need + ActiveBuff activeBuff = new ActiveBuff(ExampleModBuffs.EXAMPLE_ARROW_BUFF, mob, duration, owner); + + // Active buffs can have extra data in the form of GND data (Game Network Data) + // In this case, we store the healthPerGameTick on it + activeBuff.getGndData().setFloat("healthPerGameTick", healthPerGameTick); + + // Lastly we add the buff to the target + // The buff class is what actually handles the healing, etc. + mob.buffManager.addBuff( + activeBuff, + true, // Since this logic only happening on the server, we make sure to send it to clients + true // We force override the previous buff if it existed + ); + } + + @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(), x, y)); + } + } + + @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(x) - texture.getWidth() / 2; + int drawY = camera.getDrawY(y); + + TextureDrawOptionsEnd options = texture.initDraw() + .light(light) + .rotate(getAngle(), 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); + } + +} diff --git a/src/main/java/examplemod/examples/ExampleProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java similarity index 96% rename from src/main/java/examplemod/examples/ExampleProjectile.java rename to src/main/java/examplemod/examples/projectiles/ExampleProjectile.java index 8e46ce8..6ce8da7 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; @@ -65,7 +65,7 @@ public Trail getTrail() { @Override public void updateTarget() { - // When we have traveled longer than 20 distance, start to find and update the target + // When we have travelled longer than 20 distance, start to find and update the target if (traveledDistance > 20) { findTarget( m -> m.isHostile, // Filter all non hostile diff --git a/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java new file mode 100644 index 0000000..b2ef77e --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java @@ -0,0 +1,154 @@ +package examplemod.examples.settlement.jobs; + +import examplemod.examples.objectentity.ExampleJobObjectEntity; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.save.LoadData; +import necesse.entity.ObjectDamageResult; +import necesse.entity.mobs.friendly.human.HumanMob; +import necesse.entity.mobs.job.*; +import necesse.entity.mobs.job.activeJob.MineObjectActiveJob; +import necesse.level.maps.LevelObject; +import necesse.level.maps.levelData.jobs.MineObjectLevelJob; +import necesse.level.maps.levelData.jobs.TileLevelJob; + +/** + * A simple settlement job: + * "Go to this tile and clear the grass object there." + * We extend MineObjectLevelJob because Necesse already has a job type for + * destroying an object at a tile. + */ +public class ExampleLevelJob extends MineObjectLevelJob { + + // Since this job is never saved to the level, we can easily have variables like entities, etc. + // We can use this to determine if the job is still valid + public ExampleJobObjectEntity jobObjectEntity; + + // Create a new job at a tile position + public ExampleLevelJob(int tileX, int tileY, ExampleJobObjectEntity jobObjectEntity) { + super(tileX, tileY); + this.jobObjectEntity = jobObjectEntity; + } + + // Create a job from saved data (not used if shouldSave() returns false) + public ExampleLevelJob(LoadData save) { + super(save); + } + + @Override + public boolean isValidObject(LevelObject object) { + // This is called by our extending "MineObjectLevelJob" class. Which in turn calls this in + // it's isValid() method. It is used to determine if this job should be removed from the + // level or not. In this case, we check if the job entity that created this is still valid, + // and if the object we are targeting is still valid. This means if the object has changed between + // the job was added and now, we won't destroy the new object. And if we have cleared the job entity, + // it will also clear all jobs that it created. + return !jobObjectEntity.removed() && jobObjectEntity.isValidLevelObject(object); + } + + @Override + public boolean isSameJob(TileLevelJob other) { + // When adding a job to a tile, the game checks this to see if another job already exists that is the same. + // The super method checks for job ID/type and the tile. If you had anything custom to check for, + // we should do it here. + + // In this case, we have nothing else to check for. We could check if it's the same jobObjectEntity, but + // that could lead to 2 ExampleLevelJobs being at the same tile, not sharing the same "reservable". + // Which means 2 settlers will try to go for the same job + return super.isSameJob(other); + } + + @Override + public boolean shouldSave() { + // Don't save this job. The ExampleJobObjectEntity will recreate it if needed. + return false; + } + + /** + * This builds the actual steps the settler will do. + * Here we only add one step: mine/destroy the grass object. + * Once that is complete, we add the pickup dropped items steps + */ + public static JobSequence getJobSequence( + EntityJobWorker worker, FoundJob foundJob + ) { + // Get the current object at the job tile (might be null if it changed) + LevelObject target = foundJob.job.getObject(); + + // Message shown for the job (in settlement UI) + LocalMessage msg = new LocalMessage( + "activities", + "examplejob", + "target", + target.object.getLocalization() + ); + + // A list of work steps + GameLinkedListJobSequence sequence = new GameLinkedListJobSequence(msg, false); + + // Add the work step: go to tile + hit the object until it breaks + sequence.add(new MineObjectActiveJob( + worker, + foundJob.priority, + foundJob.job.tileX, + foundJob.job.tileY, + // Keep working only while the job still exists AND the object is still valid grass + lo -> (!foundJob.job.isRemoved() && foundJob.job.isValidObject(lo)), + foundJob.job.reservable, // Reservation (stops 2 settlers trying to do the same tile) + "sickle", // Item used for the "swing" animation (visual only) + 5, // Damage per hit to the object + 250, // Time per swing (ms) + 0 // Extra delay between swings (ms) + ) { + @Override + public void onObjectDestroyed(ObjectDamageResult result) { + // Once done with destroying the object, add pickup jobs for any drops that happened + addItemPickupJobs(foundJob.priority, result, sequence); + + // Remove the job so it doesn't stay posted + foundJob.job.remove(); + } + }); + + return sequence; + } + + // The default handler for the job, used when registering the job + public static JobTypeHandler.SubHandler handler(EntityJobWorker worker, JobTypeHandler handler) { + if (worker instanceof HumanMob) { + HumanMob humanMob = (HumanMob) worker; + + // We register a job type sub handler for the ExampleLevelJob. And use our sequence getter from above + JobTypeHandler.SubHandler subHandler = handler.setJobHandler( + ExampleLevelJob.class, + (foundJob) -> getJobSequence(humanMob, foundJob) + ); + + // Here we define when the worker should be able to do this job + subHandler.setPredicate(() -> { + // Don't do the job if they are on strike. There are some jobs (like eating), they would + // still do while they're on strike + if (humanMob.isOnStrike()) return false; + + // Don't do the job if they have currently completed a mission as are + // waiting for player pickup (like a Miners mining trip) + if (humanMob.hasCompletedMission()) return false; + + // Don't do the job if they are a settler and not within their settlement. Like on adventure party, etc. + if (humanMob.isSettler() && !humanMob.isSettlerWithinSettlement()) return false; + + // Don't do the job if they have a full inventory. The parameter is if the settler inventory full + // notification should be sent to the players + if (humanMob.isInventoryFull(true)) return false; + + // No other checks, return true if successful + return true; + }); + + return subHandler; + } else { + // If the worker is not a human, we don't do the job. + return null; + } + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java b/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java new file mode 100644 index 0000000..202ee77 --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java @@ -0,0 +1,36 @@ +package examplemod.examples.settlement.settlers; + +import necesse.engine.localization.message.GameMessage; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.util.TicketSystemList; +import necesse.entity.mobs.friendly.human.HumanMob; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.maps.levelData.settlementData.ServerSettlementData; +import necesse.level.maps.levelData.settlementData.settler.Settler; + +import java.util.function.Supplier; + +public class ExampleSettler extends Settler { + + public ExampleSettler() { + super("examplehuman"); // Must match the human mob registered stringID in ExampleModMobs + } + + @Override + public void loadTextures() { + // Use an existing icon for now, or add your own under mobs/icons/ + this.texture = GameTexture.fromFile("mobs/icons/human"); + } + + @Override + public GameMessage getAcquireTip() { + return new LocalMessage("settlement", "foundinvillagetip"); + } + + @Override + public void addNewRecruitSettler(ServerSettlementData data, boolean isRandomEvent, + TicketSystemList> ticketSystem) { + // Weight controls how often they appear as recruits + ticketSystem.addObject(isRandomEvent ? 50 : 25, getNewRecruitMob(data)); + } +} 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..1d9e383 --- /dev/null +++ b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java @@ -0,0 +1,119 @@ +package examplemod.examples.tiles; + +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.gfx.gameTexture.GameTextureSection; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GameObject; +import necesse.level.gameTile.GrassTile; +import necesse.level.gameTile.TerrainSplatterTile; +import necesse.level.maps.Level; +import necesse.level.maps.regionSystem.SimulatePriorityList; + +import java.awt.*; + +/** + * ExampleGrassTile which extends TerrainSplatterTile + * This is a ground tile. + * It does 3 main things: + * 1) Sometimes drops a seed sometimes when mined. + * 2) Can grow a grass object on top of it ("examplegrass"). + * 3) Can spread onto nearby dirt tiles. + */ +public class ExampleGrassTile extends TerrainSplatterTile { + + // How often the grass OBJECT should grow on this tile + // Takes an average of 7000 seconds to grow a grass + public static double growChance = GameMath.getAverageSuccessRuns(7000); + + // How often this TILE should spread onto dirt next to it + // Takes an average of 850 seconds for this to spread to dirt + public static double spreadChance = GameMath.getAverageSuccessRuns(850); + + // Used only for picking a random sprite row (visual variation) + private final GameRandom drawRandom = new GameRandom(); + + public ExampleGrassTile() { + // isFloor parameter defines how some other systems interact with it + // For example if settlers are happy with it in their rooms + // Texture file: resources/tiles/examplegrasstile_splat.png + // The texture is a very specific format. It consists of several rows that looks similar, + // but also a bit different. This gives some variance in which texture is drawn. + // TerrainSplatterTile asks which row/column to use in our getTerrainSprite method below. + // The texture name we give here does not include the "_splat" part for backwards compatibility reasons + super(false, "examplegrasstile"); + + mapColor = new Color(140, 0, 133); // Minimap color + canBeMined = true; // Player can mine/remove it + isOrganic = true; // Marks it as organic. Defines how some other systems interact with it + } + + @Override + public LootTable getLootTable(Level level, int tileX, int tileY) { + // 4% chance to drop a grass seed when mined + return new LootTable(new ChanceLootItem(0.04f, "examplegrassseed")); + } + + @Override + public void addSimulateLogic(Level level, int x, int y, long ticks, + SimulatePriorityList list, boolean sendChanges) { + // This happens when a chunk is loaded with this tile that has not been loaded in a while + // GrassTile has a helper function for this that we can use: + GrassTile.addSimulateGrow(level, x, y, growChance, ticks, "examplegrass", list, sendChanges); + } + + @Override + public double spreadToDirtChance() { + // Controls how fast dirt turns into this grass tile when nearby + // The actual logic is handled by the dirt tile itself + return spreadChance; + } + + @Override + public void tick(Level level, int x, int y) { + // This happens about once a second. Note: This is not time synced between server and client + // Mostly used for random events and visual stuff like particles, etc. + + // Only the server should change the world + if (!level.isServer()) return; + + // If there is no object on the tile and our random chance passes + if (level.getObjectID(x, y) == 0 && GameRandom.globalRandom.getChance(growChance)) { + + // Check if the grass object can be placed. If the canPlace returns null, it means there is no reason + // it cannot be placed. If it returns a string, that string is the reason it cannot be placed + GameObject grassObj = ObjectRegistry.getObject("examplegrass"); + if (grassObj.canPlace(level, x, y, 0, false) == null) { + // Place the object and send an update packet to clients about it + grassObj.placeObject(level, x, y, 0, false); + level.sendObjectUpdatePacket(x, y); + } + } + } + + @Override + public Point getTerrainSprite(GameTextureSection terrainTexture, Level level, int tileX, int tileY) { + // Pick a random row for the sprite, but keep it consistent per tile position + int row; + synchronized (drawRandom) { + // The reason this is handled in a synchronized segment, is because this method is run on + // different threads on the same time to faster setup rendering of the next frame + // See ExampleObject.addDrawables for a bit more explanation on this + + row = drawRandom.seeded(getTileSeed(tileX, tileY)) + .nextInt(terrainTexture.getHeight() / 32); + } + // We only have one column, so we return 0 for the column and the random row we picked + return new Point(0, row); + } + + @Override + public int getTerrainPriority() { + // TerrainPriority is used to determine which tiles should draw on top, + // when they're next to each other and overlapping + return TerrainSplatterTile.PRIORITY_TERRAIN; + } + +} \ 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/examplebuff.png b/src/main/resources/buffs/examplebuff.png index b4edeee..588757b 100644 Binary files a/src/main/resources/buffs/examplebuff.png and b/src/main/resources/buffs/examplebuff.png differ diff --git a/src/main/resources/buffs/negativebuff.png b/src/main/resources/buffs/negativebuff.png index 09e55ee..ed19641 100644 Binary files a/src/main/resources/buffs/negativebuff.png and b/src/main/resources/buffs/negativebuff.png differ diff --git a/src/main/resources/buffs/positivebuff.png b/src/main/resources/buffs/positivebuff.png index b520375..ac82b71 100644 Binary files a/src/main/resources/buffs/positivebuff.png and b/src/main/resources/buffs/positivebuff.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/examplebosssummonitem.png b/src/main/resources/items/examplebosssummonitem.png new file mode 100644 index 0000000..99c9dd6 Binary files /dev/null and b/src/main/resources/items/examplebosssummonitem.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/exampleconfigobject.png b/src/main/resources/items/exampleconfigobject.png new file mode 100644 index 0000000..2428534 Binary files /dev/null and b/src/main/resources/items/exampleconfigobject.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/exampleeventtriggerobject.png b/src/main/resources/items/exampleeventtriggerobject.png new file mode 100644 index 0000000..957d455 Binary files /dev/null and b/src/main/resources/items/exampleeventtriggerobject.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/examplejobobject.png b/src/main/resources/items/examplejobobject.png new file mode 100644 index 0000000..214f4fa Binary files /dev/null and b/src/main/resources/items/examplejobobject.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/exampletrinket.png b/src/main/resources/items/exampletrinket.png new file mode 100644 index 0000000..27a5131 Binary files /dev/null and b/src/main/resources/items/exampletrinket.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/items/exampleworkstation.png b/src/main/resources/items/exampleworkstation.png new file mode 100644 index 0000000..22a2c0f Binary files /dev/null and b/src/main/resources/items/exampleworkstation.png differ diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 3306996..1c8a6ce 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -1,29 +1,83 @@ [tile] exampletile=Example Tile +examplegrasstile=Example Grass Tile [object] exampleobject=Example Object +examplebaserock=Example Rock +exampleore=Example Ore +examplesapling=Example Sapling +exampletree=Example Tree +examplegrass=Example Grass +examplewall=Example Wall +exampledoor=Example Door +exampleeventtriggerobject=Example Event Trigger Object +examplejobobject=Example Job Object +exampleconfigobject=Example Config Object +examplepressureplate=Example Pressure Plate +examplewalltrap=Example Wall Trap +exampleworkstation=Example Workstation [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 +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 +examplebosssummonitem=Example Boss Summon Item +exampletrinket=Example Trinket + +[tech] +exampletech=Example Workstation [itemtooltip] -examplestafftip=Shoots a homing, piercing projectile -examplepotionitemtip= An example potion +examplemagicstafftip=Shoots a homing, piercing projectile +examplepotionitemtip=An example potion +examplebosssummontip=Use in the Example Biome Cave to summon the Example Boss Mob +exampletrinkettip=Example Trinket. Acts like a Spelunker Potion [mob] examplemob=Example Mob +exampleboss=Example Boss +examplesummon=Example Summon Mob +examplehuman=Example Settler +examplehumanname= The Example Settler [buff] examplebuff=Example Buff +examplearmorsetbonusbuff=Example Armor Set Bonus Buff [biome] +examplebiome=Example Biome exampleincursion=Example Incursion [incursion] -exampleincursion=Example Incursion \ No newline at end of file +exampleincursion=Example Incursion + +[itemcategory] +examplemod=ExampleMod +examplemodsub=Example subcategory + +[jobs] +examplejobname=Example Job +examplejobtip=Keeps grass cleared in assigned zones + +[activities] +examplejob=Doing Example Job on + +[journal] +examplebiomesurface=Example Biome Surface +examplebiomecave=Example Biome Cave +examplebiomedeepcave=Example Biome Deep Cave + 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/exampleconfigobject.png b/src/main/resources/objects/exampleconfigobject.png new file mode 100644 index 0000000..2428534 Binary files /dev/null and b/src/main/resources/objects/exampleconfigobject.png differ diff --git a/src/main/resources/objects/exampleeventtriggerobject.png b/src/main/resources/objects/exampleeventtriggerobject.png new file mode 100644 index 0000000..957d455 Binary files /dev/null and b/src/main/resources/objects/exampleeventtriggerobject.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/examplejobobject.png b/src/main/resources/objects/examplejobobject.png new file mode 100644 index 0000000..214f4fa Binary files /dev/null and b/src/main/resources/objects/examplejobobject.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/objects/examplewalltrap.png b/src/main/resources/objects/examplewalltrap.png new file mode 100644 index 0000000..c99f9b6 Binary files /dev/null and b/src/main/resources/objects/examplewalltrap.png differ diff --git a/src/main/resources/objects/exampleworkstation.png b/src/main/resources/objects/exampleworkstation.png new file mode 100644 index 0000000..fce0809 Binary files /dev/null and b/src/main/resources/objects/exampleworkstation.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