From 9a3ed83cf246972834bf86a9dec73b6ad46b6d51 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Tue, 20 Jan 2026 09:16:31 +0000 Subject: [PATCH 01/28] Add example boss, ore, and incursion content Introduces ExampleBossMob, ExampleOreItem, ExampleBaseRockObject, and ExampleOreRockObject, along with their textures and localization. Refactors example incursion classes into a dedicated incursion package, updates incursion logic to use new example objects and items, and registers new mobs, items, and objects in ExampleMod. Adds ExampleAI for mob behavior and updates localization for new content. --- src/main/java/examplemod/ExampleMod.java | 22 +++ .../java/examplemod/examples/ExampleAI.java | 68 +++++++++ .../{ => incursion}/ExampleBiome.java | 2 +- .../ExampleIncursionBiome.java | 6 +- .../ExampleIncursionLevel.java | 6 +- .../examples/items/ExampleOreItem.java | 14 ++ .../examples/mobs/ExampleBossMob.java | 134 ++++++++++++++++++ .../examples/{ => mobs}/ExampleMob.java | 2 +- .../objects/ExampleBaseRockObject.java | 14 ++ .../examples/{ => objects}/ExampleObject.java | 2 +- .../objects/ExampleOreRockObject.java | 27 ++++ src/main/resources/items/examplebaserock.png | Bin 0 -> 547 bytes src/main/resources/items/exampleore.png | Bin 0 -> 494 bytes src/main/resources/items/exampleorerock.png | Bin 0 -> 547 bytes src/main/resources/locale/en.lang | 4 + src/main/resources/mobs/examplebossmob.png | Bin 0 -> 4070 bytes .../resources/objects/examplebaserock.png | Bin 0 -> 1078 bytes src/main/resources/objects/exampleore.png | Bin 0 -> 730 bytes src/main/resources/objects/rock.png | Bin 0 -> 5853 bytes 19 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 src/main/java/examplemod/examples/ExampleAI.java rename src/main/java/examplemod/examples/{ => incursion}/ExampleBiome.java (96%) rename src/main/java/examplemod/examples/{ => incursion}/ExampleIncursionBiome.java (95%) rename src/main/java/examplemod/examples/{ => incursion}/ExampleIncursionLevel.java (97%) create mode 100644 src/main/java/examplemod/examples/items/ExampleOreItem.java create mode 100644 src/main/java/examplemod/examples/mobs/ExampleBossMob.java rename src/main/java/examplemod/examples/{ => mobs}/ExampleMob.java (99%) create mode 100644 src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java rename src/main/java/examplemod/examples/{ => objects}/ExampleObject.java (98%) create mode 100644 src/main/java/examplemod/examples/objects/ExampleOreRockObject.java create mode 100644 src/main/resources/items/examplebaserock.png create mode 100644 src/main/resources/items/exampleore.png create mode 100644 src/main/resources/items/exampleorerock.png create mode 100644 src/main/resources/mobs/examplebossmob.png create mode 100644 src/main/resources/objects/examplebaserock.png create mode 100644 src/main/resources/objects/exampleore.png create mode 100644 src/main/resources/objects/rock.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 6cc822c..68e084d 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,10 +1,19 @@ package examplemod; import examplemod.examples.*; +import examplemod.examples.incursion.ExampleBiome; +import examplemod.examples.incursion.ExampleIncursionBiome; +import examplemod.examples.incursion.ExampleIncursionLevel; import examplemod.examples.items.ExampleFoodItem; import examplemod.examples.items.ExampleHuntIncursionMaterialItem; import examplemod.examples.items.ExampleMaterialItem; import examplemod.examples.items.ExamplePotionItem; +import examplemod.examples.items.ExampleOreItem; +import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.objects.ExampleObject; +import examplemod.examples.objects.ExampleBaseRockObject; +import examplemod.examples.objects.ExampleOreRockObject; import necesse.engine.commands.CommandsManager; import necesse.engine.modLoader.annotations.ModEntry; import necesse.engine.registries.*; @@ -38,8 +47,16 @@ public void init() { // Register our objects ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + // Register a rock for the example incursion to use as cave walls + ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); + ObjectRegistry.registerObject(ExampleBaseRockObject.ID, exampleBaseRock, -1.0F, true); + + // Register an ore rock that overlays onto our incursion rock + ObjectRegistry.registerObject(ExampleOreRockObject.ID, new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + // Register our items ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); + ItemRegistry.registerItem(ExampleOreItem.ID, new ExampleOreItem(), 25, true); ItemRegistry.registerItem("examplehuntincursionitem", new ExampleHuntIncursionMaterialItem(), 50, true); ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); @@ -49,6 +66,9 @@ public void init() { // Register our mob MobRegistry.registerMob("examplemob", ExampleMob.class, true); + // Register boss mob + MobRegistry.registerMob("examplebossmob",ExampleBossMob.class,true,true); + // Register our projectile ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); @@ -66,6 +86,8 @@ public void initResources() { // 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"); } public void postInit() { diff --git a/src/main/java/examplemod/examples/ExampleAI.java b/src/main/java/examplemod/examples/ExampleAI.java new file mode 100644 index 0000000..f1a8094 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleAI.java @@ -0,0 +1,68 @@ +package examplemod.examples; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.composites.SelectorAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.CollisionChaserAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; +import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; + +public class ExampleAI extends SelectorAINode { + /* + This node handles "find player -> chase -> try to attack" + We keep a reference so we can use its damage/knockback settings later. + */ + public final CollisionPlayerChaserAI chaser; + + // This node handles "walk around randomly" when there's nothing to chase. + public final WandererAINode wanderer; + + public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { + /* + A SelectorAINode tries its children in order. + First one that can run/works is the behavior the mob uses. + + So: we add CHASING first, because we want chasing to "win" whenever possible. + */ + this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { + + // CollisionPlayerChaserAI has an attackTarget method it calls when in range. + // We override it so we can route the attack logic to OUR method below. + @Override + public boolean attackTarget(T mob, Mob target) { + // "ExampleAI.this" means "the outer ExampleAI instance" + // (because we're inside an anonymous inner class right now). + return ExampleAI.this.attackTarget(mob, target); + } + }; + addChild(this.chaser); + + /* + If the chaser doesn't have a target (or can't chase), + the selector will try the next child: wandering. + */ + this.wanderer = new WandererAINode<>(wanderFrequency); + addChild(this.wanderer); + } + + /* + This is the actual "do the hit" logic. + We keep it outside the chaser node so it's easy to change later. + */ + public boolean attackTarget(T mob, Mob target) { + /* + simpleAttack is a helper that does a basic melee attack: + - checks if the mob can attack right now + - applies damage + - applies knockback + - returns true if an attack happened + */ + return CollisionChaserAINode.simpleAttack( + mob, + target, + // Use the damage/knockback values that were passed into the chaser constructor. + this.chaser.damage, + this.chaser.knockback + ); + } +} diff --git a/src/main/java/examplemod/examples/ExampleBiome.java b/src/main/java/examplemod/examples/incursion/ExampleBiome.java similarity index 96% rename from src/main/java/examplemod/examples/ExampleBiome.java rename to src/main/java/examplemod/examples/incursion/ExampleBiome.java index a6ae51e..60d122b 100644 --- a/src/main/java/examplemod/examples/ExampleBiome.java +++ b/src/main/java/examplemod/examples/incursion/ExampleBiome.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.incursion; import necesse.engine.AbstractMusicList; import necesse.engine.MusicList; diff --git a/src/main/java/examplemod/examples/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleIncursionBiome.java rename to src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java index 6671f98..e9ffdde 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.incursion; import necesse.engine.network.server.Server; import necesse.engine.registries.ItemRegistry; @@ -26,13 +26,13 @@ public class ExampleIncursionBiome extends IncursionBiome { public ExampleIncursionBiome() { - super("reaper"); // The boss mob string ID for this incursion + super("examplebossmob"); // The boss mob string ID for this incursion } // Items required to be obtained when completing an extraction objective in this incursion @Override public Collection getExtractionItems(IncursionData data) { - return Collections.singleton(ItemRegistry.getItem("tungstenore")); + return Collections.singleton(ItemRegistry.getItem("exampleore")); } /** diff --git a/src/main/java/examplemod/examples/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java similarity index 97% rename from src/main/java/examplemod/examples/ExampleIncursionLevel.java rename to src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java index bfcecef..8e0b4d9 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.incursion; import examplemod.ExampleMod; import necesse.engine.GameEvents; @@ -45,7 +45,7 @@ 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"); + CaveGeneration cg = new CaveGeneration(this, "deeprocktile", "examplebaserock"); // Seed the generator so this incursion layout is deterministic per mission cg.random.setSeed(incursionData.getUniqueID()); @@ -83,7 +83,7 @@ public void generateLevel(BiomeMissionIncursionData incursionData, AltarData alt // For extraction incursions, guarantee tungsten 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/items/ExampleOreItem.java b/src/main/java/examplemod/examples/items/ExampleOreItem.java new file mode 100644 index 0000000..bc9cfd9 --- /dev/null +++ b/src/main/java/examplemod/examples/items/ExampleOreItem.java @@ -0,0 +1,14 @@ +package examplemod.examples.items; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleOreItem extends MatItem { + + public static final String ID = "exampleore"; + + public ExampleOreItem() { + super(500, Item.Rarity.UNCOMMON); + + } +} 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..d0c8944 --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -0,0 +1,134 @@ +package examplemod.examples.mobs; + +import examplemod.examples.ExampleAI; +import necesse.engine.eventStatusBars.EventStatusBarManager; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.sound.SoundManager; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.hostile.bosses.BossMob; +import necesse.entity.particle.FleshParticle; +import necesse.entity.particle.Particle; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptions; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.*; +import java.util.List; + +public class ExampleBossMob extends BossMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + public static LootTable lootTable = new LootTable( + ChanceLootItem.between(0.5f, "exampleitem", 1, 3) // 50% chance to drop between 1-3 example items + ); + + // MUST HAVE an empty constructor + public ExampleBossMob() { + super(200); + setSpeed(50); + setFriction(3); + this.shouldSave = false; + this.canDespawn = true; + this.isSummoned = true; + // Hitbox, collision box, and select box (for hovering) + collision = new Rectangle(-10, -7, 20, 14); + hitBox = new Rectangle(-14, -12, 28, 24); + selectBox = new Rectangle(-14, -7 - 34, 28, 48); + } + + @Override + public void init() { + super.init(); + // Setup AI + this.ai = new BehaviourTreeAI<>( + this, + new ExampleAI<>( + 1380, // search distance (in pixels) + new GameDamage(60), // collide damage + 150, // knockback + 12000 // wander frequency + ) + ); + } + + @Override + public LootTable getLootTable() { + return lootTable; + } + + @Override + public void spawnDeathParticles(float knockbackX, float knockbackY) { + // Spawn flesh debris particles + for (int i = 0; i < 4; i++) { + getLevel().entityManager.addParticle(new FleshParticle( + getLevel(), texture, + GameRandom.globalRandom.nextInt(5), // Randomize between the debris sprites + 8, + 32, + x, y, 20f, // Position + knockbackX, knockbackY // Basically start speed of the particles + ), Particle.GType.IMPORTANT_COSMETIC); + } + } + + @Override + protected void addDrawables(List list, OrderableDrawables tileList, OrderableDrawables topList, Level level, int x, int y, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + super.addDrawables(list, tileList, topList, level, x, y, tickManager, camera, perspective); + // Tile positions are basically level positions divided by 32. getTileX() does this for us etc. + GameLight light = level.getLightLevel(getTileX(), getTileY()); + int drawX = camera.getDrawX(x) - 32; + int drawY = camera.getDrawY(y) - 51; + + // A helper method to get the sprite of the current animation/direction of this mob + Point sprite = getAnimSprite(x, y, getDir()); + + drawY += getBobbing(x, y); + drawY += getLevel().getTile(getTileX(), getTileY()).getMobSinkingAmount(this); + + DrawOptions drawOptions = texture.initDraw() + .sprite(sprite.x, sprite.y, 64) + .light(light) + .pos(drawX, drawY); + + list.add(new MobDrawable() { + @Override + public void draw(TickManager tickManager) { + drawOptions.draw(); + } + }); + + addShadowDrawables(tileList, level, x, y, light, camera); + } + + @Override + public int getRockSpeed() { + // Change the speed at which this mobs animation plays + return 20; + } + + @Override + public void clientTick() { + super.clientTick(); + + // Only show boss bar when the client player is close enough + if (isClientPlayerNearby()) { + EventStatusBarManager.registerMobHealthStatusBar(this); + } + // Optional: set boss music here too if you want + SoundManager.setMusic(MusicRegistry.AscendedReturn, SoundManager.MusicPriority.EVENT, 1.5F); + } + + +} diff --git a/src/main/java/examplemod/examples/ExampleMob.java b/src/main/java/examplemod/examples/mobs/ExampleMob.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleMob.java rename to src/main/java/examplemod/examples/mobs/ExampleMob.java index c537747..c3151e4 100644 --- a/src/main/java/examplemod/examples/ExampleMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleMob.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.mobs; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.engine.util.GameRandom; diff --git a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java new file mode 100644 index 0000000..44ed536 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -0,0 +1,14 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; + +import java.awt.Color; + +public class ExampleBaseRockObject extends RockObject { + + public static final String ID = "examplebaserock"; + + public ExampleBaseRockObject() { + super("examplebaserock", new Color(92, 37, 23), "stone", "objects", "landscaping"); + this.toolTier = 5.0F; + } +} diff --git a/src/main/java/examplemod/examples/ExampleObject.java b/src/main/java/examplemod/examples/objects/ExampleObject.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleObject.java rename to src/main/java/examplemod/examples/objects/ExampleObject.java index 7ad35be..f611cfe 100644 --- a/src/main/java/examplemod/examples/ExampleObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleObject.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.objects; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; diff --git a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java new file mode 100644 index 0000000..f2e1e3d --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -0,0 +1,27 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; +import necesse.level.gameObject.RockOreObject; + +import java.awt.Color; + +/** + * Example ore rock that uses our ExampleIncursionDeepRockObject as its parent rock. + */ +public class ExampleOreRockObject extends RockOreObject { + + public static final String ID = "exampleorerock"; + + public ExampleOreRockObject(RockObject parentRock) { + super( + parentRock, + "oremask", + "exampleore", + new Color(90, 40, 160), + "exampleore", + 1, + 3, + 2, + true, + "objects", "landscaping"); + } +} diff --git a/src/main/resources/items/examplebaserock.png b/src/main/resources/items/examplebaserock.png new file mode 100644 index 0000000000000000000000000000000000000000..bfc82382d32caeed91cd11fe1256e27b535a4b64 GIT binary patch literal 547 zcmV+;0^I$HP)KNKUVla79%p@lS{y)5Z zlVR?z!#T(nCA-Y zlN%CvT}g`1$qfmtAwY@GNev0CAwY@GNeu~<5TK^di46&q5Fp0qNU0oed4r{#z?wTq zDhU~A07xzgarul;`GCvq10Dhd9gyT|!vMkzGy;P^4b0np1nhTMD8TsC3Xq|b3vl_I z+|&;X!J$_U5Go1LLj;y~l5r%Op%(%$j}VG`Vsp*V4FUADfF2^mmKVb}4G>#W4xcnY zZ0> z*QX4_rcFq#{0|EdaQ7V7*e?T(0L(tn*fR3?fGjcgkvl?yWH!v6#~?8#dix&PlOVf6 zApmoc1~DN(g%J`Idj`BDq`Cu09Z?y&tzum1h13z1p&tUcO2#46HynQ1kksmf5la~j zE2(x-xh`^pns)l8%PKx6rY3i!K-6fDA22BL=JUq^97G- l)DFRFE{X$a9TM2m8vu$V!b$EgfkOZQ002ovPDHLkV1n8)-2(ss literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampleore.png b/src/main/resources/items/exampleore.png new file mode 100644 index 0000000000000000000000000000000000000000..62c0e6de60bc79adc39782324f14fcbbc4959a50 GIT binary patch literal 494 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$);LoovvyXX~Dpa^GyM`SSr1Gg{;GcwGYBLNg-FY)wsWq-ve!oa7!T7Uf~pbLyV zT^vIq4!@nYKkHC{$g%o=ku|MOinD*PG+j_%As%yty&&Ypli-q96E`}T~P;)j}&mU*EfGVEf~g_l)%WP3CWnXI=MO zkQ?>V*q`A`So4))>#b8$S9mXqjAUE#Nm7`hhL53QvBK{6mrw3jvAem%DA1PeTqo&DkpcmYQ80|fT&+s{-H2K1FfzZ6iDz&E_H6F~(?zP!r!hBNYlb4p?$CYZ#t^YaGdz@>T iPCLyD_g@gUR+!^gtm_QUoBx0T%HZkh=d#Wzp$Pz43&E8D literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampleorerock.png b/src/main/resources/items/exampleorerock.png new file mode 100644 index 0000000000000000000000000000000000000000..bfc82382d32caeed91cd11fe1256e27b535a4b64 GIT binary patch literal 547 zcmV+;0^I$HP)KNKUVla79%p@lS{y)5Z zlVR?z!#T(nCA-Y zlN%CvT}g`1$qfmtAwY@GNev0CAwY@GNeu~<5TK^di46&q5Fp0qNU0oed4r{#z?wTq zDhU~A07xzgarul;`GCvq10Dhd9gyT|!vMkzGy;P^4b0np1nhTMD8TsC3Xq|b3vl_I z+|&;X!J$_U5Go1LLj;y~l5r%Op%(%$j}VG`Vsp*V4FUADfF2^mmKVb}4G>#W4xcnY zZ0> z*QX4_rcFq#{0|EdaQ7V7*e?T(0L(tn*fR3?fGjcgkvl?yWH!v6#~?8#dix&PlOVf6 zApmoc1~DN(g%J`Idj`BDq`Cu09Z?y&tzum1h13z1p&tUcO2#46HynQ1kksmf5la~j zE2(x-xh`^pns)l8%PKx6rY3i!K-6fDA22BL=JUq^97G- l)DFRFE{X$a9TM2m8vu$V!b$EgfkOZQ002ovPDHLkV1n8)-2(ss literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 3306996..55de7a6 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -3,9 +3,12 @@ exampletile=Example Tile [object] exampleobject=Example Object +examplebaserock=Example Rock +exampleore=Example Ore [item] exampleitem=Example Item +exampleore=Example Ore examplehuntincursionitem=Example Hunt Incursion Item examplepotionitem=Example Potion examplesword=Example Sword @@ -18,6 +21,7 @@ examplepotionitemtip= An example potion [mob] examplemob=Example Mob +examplebossmob=Example Boss [buff] examplebuff=Example Buff diff --git a/src/main/resources/mobs/examplebossmob.png b/src/main/resources/mobs/examplebossmob.png new file mode 100644 index 0000000000000000000000000000000000000000..a740a8f6992bd008acea61e4faf13cf37575efce GIT binary patch literal 4070 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/examplebaserock.png b/src/main/resources/objects/examplebaserock.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ddf9211df20af773b34bea5e628b36aa17e8f3 GIT binary patch literal 1078 zcmeAS@N?(olHy`uVBq!ia0vp^4M2Q>gAGWY-;ikoq!^2X+?^P2p46!aa#+$GeH|GX zHuiJ>Nn{1`6_P!Id>I(3)PNdW7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXICJ|AhslL zcNc~a48;sw?4nmpfg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHic?2q%-whHrIs3=GUw zo-U3d6}R5r^_?`^K*Tk1VuiBp>)4GVkC$#(J+D$#(}+?0Ez6nt6DEJ3+Zmi1?O!6h zu4RI!7tk<e9x@4IGO2ydR(=)Bwb zYqtWUio+XY1uLNj1_cHV2Brg63|vL8*H2n!x$c;oc7r)*!+tdfLFSJOUdFFizdDiO z$2*(VJni zg8&0qUVz~mBbVUcyd|=Z8nYS7SZ6aU6fv3nT^)N+h(S>Jz&r;|1}3N@;M9-*70drJ zxW0b=Z@!fZ!#z93S^u_PscTsDwY{Zd8jFwvlg$SC1v>Ni8#Zw^Y%=*f^Zic-15Or3 zfn2@^+Z!c6-gb~+a$q=fhH=$LiHU`(Sy^w^o|E9d`adq7iDmh$nWkmz>IZH`mS6t< zc-Q84xp(e$MHXLtY`=OR3n>qvy*`0K z=)rw2h8kb(yYG+b~~7y$x~BY5x-;5C>(+4QCP>)v^_`~PN$*XF+}cK@23`~Td# z=hwC!zon^e$zwL9n0XwQA9ZVQsRqy>d@3)Ni?a=>s zegT8Mfq~<6s*T%{E%o|$M4Wf;eEmjJSy^sBivddmg8{n%+i{yZ&IAs)F#=uob&^wF zSCwS$zW93n-g63!O~sP)YznUktA|Of>JUxloWXOGumBfyuGf0H53%7p00i_>zopr0M-|bP5=M^ literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/exampleore.png b/src/main/resources/objects/exampleore.png new file mode 100644 index 0000000000000000000000000000000000000000..9308a723fcd3a0ea8534a69aa2cb296db995b1dd GIT binary patch literal 730 zcmV<00ww*4P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;&yra{vkl71T8V000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHS61{F7tLVPR$00JIKL_t(&-tAbiZi7G!eMC%Y2Y!+)7}X2&o#pBnOA_`MvY`k?pZN0zi#z24=5#J_5ia9-!T)=f3dGFXIjG z>O3+z-|}z0NBWoZEI*dN74F3j>B`Iu)tZ+A>wmSQwDXfX`}u-lIk6GCn?p4W6nGSoe$2O~$CP47B{naV{UA zMe)THq04yjYQxLffj0lz)(4Utw*e@t1|?3{+7M9#_=5VKpAw-ZfD-0ad3k!o_8n!_ zfX7Jx_t+izJ#9_g@|cYC2ix!h3g9mp0YeDW*kfj{XSh+F7op4K!nOG^dpTB1c=IJN zHMUtDF1L#1FSng-)xvfN8_45~&%c!sFm>o(_{AwOwDU_vI`(?11{kXr@4gSnr_$q= z(Q^`kSmeFhAkX(yO|o=?v3qZ94}Ysp&{K&E1E$*ZQs;Z$5~o6T=&7$*s(%G z(*|?Z2>ITWxVe~HSuLehgVvZjL%#SqOnYx%ob)T>%r(SFI%-0Ppv7QinOYE ziS^Nn^AI*?e=^R0qXvh@nfsksW2!-(eJ?md%tBapbTQux`j{FEP;O;%52$)t)7Nu; z4YJ-VV;}}_`K-m6O;w$*b-qv4A6T_&QG;`JU}L`l4!!SnqIb`LABuGEqbrCwc>n+a M07*qoM6N<$g4Jv{i2wiq literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/rock.png b/src/main/resources/objects/rock.png new file mode 100644 index 0000000000000000000000000000000000000000..aa51ed0427a5c368ea03de6a8132ead9438ed708 GIT binary patch literal 5853 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 Date: Tue, 20 Jan 2026 22:02:43 +0000 Subject: [PATCH 02/28] Add ExampleAI and ExampleAILeaf for mob behavior Introduces ExampleAI and ExampleAILeaf classes to provide custom AI behavior for mobs, including teleporting to incursion entrance and handling chasing and wandering logic. Updates ExampleBossMob to use the new ExampleAI implementation. --- .../examplemod/examples/ai/ExampleAI.java | 77 ++++++++++ .../examplemod/examples/ai/ExampleAILeaf.java | 143 ++++++++++++++++++ .../examples/mobs/ExampleBossMob.java | 2 +- 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/main/java/examplemod/examples/ai/ExampleAI.java create mode 100644 src/main/java/examplemod/examples/ai/ExampleAILeaf.java 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..8ba9ec6 --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -0,0 +1,77 @@ +package examplemod.examples.ai; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.composites.SelectorAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.CollisionChaserAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; +import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; + +public class ExampleAI extends SelectorAINode { + public final ExampleAILeaf teleporter; + + /* + This node handles "find player -> chase -> try to attack" + We keep a reference so we can use its damage/knockback settings later. + */ + public final CollisionPlayerChaserAI chaser; + + // This node handles "walk around randomly" when there's nothing to chase. + public final WandererAINode wanderer; + + public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { + /* + A SelectorAINode tries its children in order. + First one that can run/works is the behaviour the mob uses. + + So: we add CHASING first, because we want chasing to "win" whenever possible. + */ + + + this.teleporter = new ExampleAILeaf<>(8, 10); + { + // 8 tiles = 256px open-space check, search within 10 tiles if it needs to move. + } + addChild(this.teleporter); + this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { + + // CollisionPlayerChaserAI has an attackTarget method it calls when in range. + // We override it so we can route the attack logic to OUR method below. + @Override + public boolean attackTarget(T mob, Mob target) { + // "ExampleAI.this" means "the outer ExampleAI instance" + // (because we're inside an anonymous inner class right now). + return ExampleAI.this.attackTarget(mob, target); + } + }; + addChild(this.chaser); + + /* + If the chaser doesn't have a target (or can't chase), + the selector will try the next child: wandering. + */ + this.wanderer = new WandererAINode<>(wanderFrequency); + addChild(this.wanderer); + } + + /* + attack logic. + We keep it outside the chaser node so it's easy to change later. + */ + public boolean attackTarget(T mob, Mob target) { + /* + simpleAttack is a helper that does a basic melee attack: + - checks if the mob can attack right now + - applies damage + - applies knockback + - returns true if an attack happened + */ + return CollisionChaserAINode.simpleAttack( + mob, + target, + // Use the damage/knockback values that were passed into the chaser constructor. + this.chaser.damage, + this.chaser.knockback + ); + } +} diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java new file mode 100644 index 0000000..fae5455 --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -0,0 +1,143 @@ +package examplemod.examples.ai; + +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.AINode; +import necesse.entity.mobs.ai.behaviourTree.AINodeResult; +import necesse.entity.mobs.ai.behaviourTree.Blackboard; +import necesse.level.maps.IncursionLevel; +import necesse.level.maps.Level; + +/** + Runs once after spawn: + If we are in an IncursionLevel and the mob is NOT inside the entrance open-space, + teleport it to a valid tile near the incursion return portal (entrance centre). + + "Entrance centre" = return portal position: + IncursionBiome.generateEntrance(...) clears an open area and calls addReturnPortalOnTile(...) + which ends up setting IncursionLevel returnPortalPosition/ID. + */ +public class ExampleAILeaf extends AINode { + + private boolean didCheck = false; + + // How close the mob must be to the entrance centre to count as "in the entrance area" + private final int openRadiusTiles; + + // How far around the portal we will search to find a valid teleport tile + private final int searchRadiusTiles; + + public ExampleAILeaf(int openRadiusTiles, int searchRadiusTiles) { + this.openRadiusTiles = Math.max(1, openRadiusTiles); + this.searchRadiusTiles = Math.max(this.openRadiusTiles, searchRadiusTiles); + } + + @Override + protected void onRootSet(AINode aiNode, T t, Blackboard blackboard) { + + } + + @Override + public void init(T mob, Blackboard blackboard) { + // Nothing to init; we just run once in tick(). + } + + @Override + public AINodeResult tick(T mob, Blackboard blackboard) { + // Only do this once per mob instance. + if (didCheck) return AINodeResult.FAILURE; + didCheck = true; + + // Only do this on the server (positions are authoritative there). + if (!mob.isServer()) return AINodeResult.FAILURE; + + Level level = mob.getLevel(); + if (!(level instanceof IncursionLevel)) return AINodeResult.FAILURE; + + IncursionLevel incursion = (IncursionLevel) level; + + // Entrance centre is the return portal position. + Point portalPos = incursion.getReturnPortalPosition(); + if (portalPos == null) return AINodeResult.FAILURE; + + float centerX = portalPos.x; + float centerY = portalPos.y; + + // If already inside the entrance open space, do nothing. + float openRadiusPx = this.openRadiusTiles * 32.0f; + if (mob.getDistance(centerX, centerY) <= openRadiusPx) { + return AINodeResult.FAILURE; + } + + // Otherwise teleport to a nearby valid spot close to the portal. + Point2D.Float dest = findValidTeleportPos(incursion, mob, centerX, centerY, this.searchRadiusTiles); + + if (dest != null) { + // "Direct" position set (sends movement packet if needed). + mob.stopMoving(); + mob.setPos(dest.x, dest.y, true); + } + + // Return FAILURE so parent Selector continues to chase/wander this same tick. + return AINodeResult.FAILURE; + } + + private static Point2D.Float findValidTeleportPos(IncursionLevel level, Mob mob, float centerX, float centerY, int searchRadiusTiles) { + int centerTileX = GameMath.getTileCoordinate(centerX); + int centerTileY = GameMath.getTileCoordinate(centerY); + + // Try rings from 0 outward (0 = centre) + for (int r = 0; r <= searchRadiusTiles; r++) { + ArrayList ring = buildRing(centerTileX, centerTileY, r); + + // Randomise the order a bit so we don't always pick the same spot + while (!ring.isEmpty()) { + Point p = ring.remove(GameRandom.globalRandom.nextInt(ring.size())); + + // Avoid liquid/shore like vanilla spawn logic + if (level.isLiquidTile(p.x, p.y)) continue; + if (level.isShore(p.x, p.y)) continue; + + int px = p.x * 32 + 16; + int py = p.y * 32 + 16; + + // Must not collide with the map, objects, etc. + if (mob.collidesWith(level, px, py)) continue; + + // Also avoid landing inside another mob/player + if (mob.collidesWithAnyMob(level, px, py)) continue; + + return new Point2D.Float(px, py); + } + } + + // No valid tile found + return null; + } + + private static ArrayList buildRing(int cx, int cy, int r) { + ArrayList points = new ArrayList<>(); + if (r == 0) { + points.add(new Point(cx, cy)); + return points; + } + + // Top and bottom edges + for (int dx = -r; dx <= r; dx++) { + points.add(new Point(cx + dx, cy - r)); + points.add(new Point(cx + dx, cy + r)); + } + // Left and right edges (excluding corners already added) + for (int dy = -r + 1; dy <= r - 1; dy++) { + points.add(new Point(cx - r, cy + dy)); + points.add(new Point(cx + r, cy + dy)); + } + + return points; + } +} diff --git a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java index d0c8944..cab411e 100644 --- a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -1,6 +1,6 @@ package examplemod.examples.mobs; -import examplemod.examples.ExampleAI; +import examplemod.examples.ai.ExampleAI; import necesse.engine.eventStatusBars.EventStatusBarManager; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.engine.registries.MusicRegistry; From 3eca28db652aead335fe5b50e6963a5f7fae1a59 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Wed, 21 Jan 2026 00:36:25 +0000 Subject: [PATCH 03/28] Refactor item structure and add Example Bar item Moved sword and projectile weapon classes to a new tools subpackage. Added ExampleBarItem and its associated recipe and icon. Updated item and recipe registration to use new item IDs and structure. Renamed exampleore.png to exampleoreitem.png and updated references. Updated localization and added mob icons. --- src/main/java/examplemod/ExampleMod.java | 61 ++----------- .../java/examplemod/examples/ExampleAI.java | 68 -------------- .../examplemod/examples/ExampleRecipes.java | 83 ++++++++++++++++++ .../examples/items/ExampleBarItem.java | 12 +++ .../examples/items/ExampleOreItem.java | 2 - .../tools}/ExampleProjectileWeapon.java | 3 +- .../{ => items/tools}/ExampleSwordItem.java | 3 +- src/main/resources/items/examplebaritem.png | Bin 0 -> 657 bytes .../{exampleore.png => exampleoreitem.png} | Bin src/main/resources/items/examplestaff.png | Bin 444 -> 395 bytes src/main/resources/items/examplesword.png | Bin 446 -> 389 bytes src/main/resources/locale/en.lang | 3 +- .../resources/mobs/icons/examplebossmob.png | Bin 0 -> 402 bytes src/main/resources/mobs/icons/examplemob.png | Bin 0 -> 398 bytes src/main/resources/objects/rock.png | Bin 5853 -> 0 bytes 15 files changed, 109 insertions(+), 126 deletions(-) delete mode 100644 src/main/java/examplemod/examples/ExampleAI.java create mode 100644 src/main/java/examplemod/examples/ExampleRecipes.java create mode 100644 src/main/java/examplemod/examples/items/ExampleBarItem.java rename src/main/java/examplemod/examples/{ => items/tools}/ExampleProjectileWeapon.java (98%) rename src/main/java/examplemod/examples/{ => items/tools}/ExampleSwordItem.java (94%) create mode 100644 src/main/resources/items/examplebaritem.png rename src/main/resources/items/{exampleore.png => exampleoreitem.png} (100%) create mode 100644 src/main/resources/mobs/icons/examplebossmob.png create mode 100644 src/main/resources/mobs/icons/examplemob.png delete mode 100644 src/main/resources/objects/rock.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 68e084d..4f77f57 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -4,23 +4,19 @@ import examplemod.examples.incursion.ExampleBiome; import examplemod.examples.incursion.ExampleIncursionBiome; import examplemod.examples.incursion.ExampleIncursionLevel; -import examplemod.examples.items.ExampleFoodItem; -import examplemod.examples.items.ExampleHuntIncursionMaterialItem; -import examplemod.examples.items.ExampleMaterialItem; -import examplemod.examples.items.ExamplePotionItem; -import examplemod.examples.items.ExampleOreItem; +import examplemod.examples.items.*; +import examplemod.examples.items.tools.ExampleProjectileWeapon; +import examplemod.examples.items.tools.ExampleSwordItem; import examplemod.examples.mobs.ExampleMob; import examplemod.examples.mobs.ExampleBossMob; import examplemod.examples.objects.ExampleObject; import examplemod.examples.objects.ExampleBaseRockObject; import examplemod.examples.objects.ExampleOreRockObject; +import examplemod.examples.ExampleRecipes; import necesse.engine.commands.CommandsManager; 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; @ModEntry @@ -56,7 +52,8 @@ public void init() { // Register our items ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); - ItemRegistry.registerItem(ExampleOreItem.ID, new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("exampleoreitem", new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("examplebaritem", new ExampleBarItem(),50,true); ItemRegistry.registerItem("examplehuntincursionitem", new ExampleHuntIncursionMaterialItem(), 50, true); ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); @@ -91,50 +88,8 @@ public void initResources() { } 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) - } - )); + // load our recipes from the ExampleRecipes class + ExampleRecipes.registerRecipes(); // Add our example mob to default cave mobs. // Spawn tables use a ticket/weight system. In general, common mobs have about 100 tickets. diff --git a/src/main/java/examplemod/examples/ExampleAI.java b/src/main/java/examplemod/examples/ExampleAI.java deleted file mode 100644 index f1a8094..0000000 --- a/src/main/java/examplemod/examples/ExampleAI.java +++ /dev/null @@ -1,68 +0,0 @@ -package examplemod.examples; - -import necesse.entity.mobs.GameDamage; -import necesse.entity.mobs.Mob; -import necesse.entity.mobs.ai.behaviourTree.composites.SelectorAINode; -import necesse.entity.mobs.ai.behaviourTree.leaves.CollisionChaserAINode; -import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; -import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; - -public class ExampleAI extends SelectorAINode { - /* - This node handles "find player -> chase -> try to attack" - We keep a reference so we can use its damage/knockback settings later. - */ - public final CollisionPlayerChaserAI chaser; - - // This node handles "walk around randomly" when there's nothing to chase. - public final WandererAINode wanderer; - - public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { - /* - A SelectorAINode tries its children in order. - First one that can run/works is the behavior the mob uses. - - So: we add CHASING first, because we want chasing to "win" whenever possible. - */ - this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { - - // CollisionPlayerChaserAI has an attackTarget method it calls when in range. - // We override it so we can route the attack logic to OUR method below. - @Override - public boolean attackTarget(T mob, Mob target) { - // "ExampleAI.this" means "the outer ExampleAI instance" - // (because we're inside an anonymous inner class right now). - return ExampleAI.this.attackTarget(mob, target); - } - }; - addChild(this.chaser); - - /* - If the chaser doesn't have a target (or can't chase), - the selector will try the next child: wandering. - */ - this.wanderer = new WandererAINode<>(wanderFrequency); - addChild(this.wanderer); - } - - /* - This is the actual "do the hit" logic. - We keep it outside the chaser node so it's easy to change later. - */ - public boolean attackTarget(T mob, Mob target) { - /* - simpleAttack is a helper that does a basic melee attack: - - checks if the mob can attack right now - - applies damage - - applies knockback - - returns true if an attack happened - */ - return CollisionChaserAINode.simpleAttack( - mob, - target, - // Use the damage/knockback values that were passed into the chaser constructor. - this.chaser.damage, - this.chaser.knockback - ); - } -} diff --git a/src/main/java/examplemod/examples/ExampleRecipes.java b/src/main/java/examplemod/examples/ExampleRecipes.java new file mode 100644 index 0000000..174e8bf --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleRecipes.java @@ -0,0 +1,83 @@ +package examplemod.examples; + +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 ExampleRecipes { + + //Put your recipe registrations in here + public static void registerRecipes(){ + + // Example Bar item smelted in the forge + Recipes.registerModRecipe(new Recipe( + "examplebaritem", + 1, + RecipeTechRegistry.FORGE, + new Ingredient[]{ + new Ingredient("exampleoreitem",2) + }) + ); + + + // Example item recipe, crafted in inventory for 2 iron bars + Recipes.registerModRecipe(new Recipe( + "exampleitem", + 1, + RecipeTechRegistry.NONE, + new Ingredient[]{ + new Ingredient("examplebaritem", 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("examplebaritem", 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("examplebaritem", 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) + } + )); + + // Example potion item recipe + Recipes.registerModRecipe(new Recipe( + "examplepotionitem", + 1, + RecipeTechRegistry.ALCHEMY, + new Ingredient[]{ + new Ingredient("speedpotion", 1), + } + )); + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleBarItem.java b/src/main/java/examplemod/examples/items/ExampleBarItem.java new file mode 100644 index 0000000..0e5160f --- /dev/null +++ b/src/main/java/examplemod/examples/items/ExampleBarItem.java @@ -0,0 +1,12 @@ +package examplemod.examples.items; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleBarItem extends MatItem { + + public ExampleBarItem() { + super(500, Item.Rarity.UNCOMMON); + + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleOreItem.java b/src/main/java/examplemod/examples/items/ExampleOreItem.java index bc9cfd9..8e3ed98 100644 --- a/src/main/java/examplemod/examples/items/ExampleOreItem.java +++ b/src/main/java/examplemod/examples/items/ExampleOreItem.java @@ -5,8 +5,6 @@ public class ExampleOreItem extends MatItem { - public static final String ID = "exampleore"; - public ExampleOreItem() { super(500, Item.Rarity.UNCOMMON); diff --git a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java index 31aff93..fe57eb1 100644 --- a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java @@ -1,5 +1,6 @@ -package examplemod.examples; +package examplemod.examples.items.tools; +import examplemod.examples.ExampleProjectile; import necesse.engine.localization.Localization; import necesse.engine.network.gameNetworkData.GNDItemMap; import necesse.engine.sound.SoundEffect; diff --git a/src/main/java/examplemod/examples/ExampleSwordItem.java b/src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java similarity index 94% rename from src/main/java/examplemod/examples/ExampleSwordItem.java rename to src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java index 68fc649..43e856f 100644 --- a/src/main/java/examplemod/examples/ExampleSwordItem.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.items.tools; import necesse.inventory.item.Item; import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; @@ -16,6 +16,7 @@ public ExampleSwordItem() { .setUpgradedValue(1, 95); // Upgraded tier 1 damage attackRange.setBaseValue(120); // 120 range knockback.setBaseValue(100); // 100 knockback + } } diff --git a/src/main/resources/items/examplebaritem.png b/src/main/resources/items/examplebaritem.png new file mode 100644 index 0000000000000000000000000000000000000000..92d03dff9b0e1f276bbb9d2de2188d6dffcf84eb GIT binary patch literal 657 zcmV;C0&e|@P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;&yra{vkl71T8V000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHS777sKhmrR5J00GrWL_t(o!|j&8OB_KIfWJ2$iW)^QqJ+mPlXR%LRcs9y@^35x(CmJ+`lpUQ=ky$(qpK6 zc(I8n|GZ{T;T63n?`kwhWKfU98gjpp&?Y5DN9#meU8%WZt0XE#VzyUW-4n3&@>qoSZ zq!)#HLO#z@;66~5f${7hSAQ<;t+k+{<$xjo=^g;^-PsQuFyy9?J%J(|H)Q$HQ3t>$ zx)3d_Uu}baAfLmztAdQYcn+Mk=b*jx{DX4@Xgw1oUqu!8$&Wd;UvNNAcqF98YA#X) z4gGYNHl#A-RT&uG>b@^C6DH|Yh3DI|3-FWs>)3p7nsKW?Ecxp_4NWzeSjG)yrc(0w r+y}}TiAo;dJE7l5RPtA2;645U_TrY%FCNJL00000NkvXXu0mjfX(kKr literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampleore.png b/src/main/resources/items/exampleoreitem.png similarity index 100% rename from src/main/resources/items/exampleore.png rename to src/main/resources/items/exampleoreitem.png diff --git a/src/main/resources/items/examplestaff.png b/src/main/resources/items/examplestaff.png index 40200b40a213fe3a77c6f60364f6c1c1bf3c70f0..1be7fd84548bc4d92b44468565e89015781e5ea5 100644 GIT binary patch delta 369 zcmdnP+|4{ext@WsILO_J@#aaLdIkmtmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6VX;z);M<#V&fq6ez-3;1OBO zz`!jG!i)^F=12eq*-JcqUD;nTiilftY!awnc^D{m-qXb~B;s&#f&}a0hKdjJT`Rd} zu-yvparrgpq0V7JHV{Z=lMedA_fH@}G3=pi+iso=#gFr)6b)B6GpHWDpSt7x0U$`& zv4}}Yz|g0Nky~&6`jm){N5!X4US4qN%nqln!W&bWH8KVOAC^%DVy2YvC72l4nPLm#96lj`?*43N5FjEE6#z-Q52;GsujiBimVLegak@nh9y2w6aYg3B@N82 zgoKEa6l$PJi$bDHE#f&@EVE<%D z2}y29V9kc;E`O(Gxxhj~e#0p7)MQy09CKfRLk1QSq!tfEIRIB&;tLsK(*g^%i`iQ% zB*8`ka|Kc51I?RcplHHZK2TJr!=eCW$*sqKu%-oI8o^sWP!t8!C^x8?1{jfZ1+3&G zl*Xvy07^oFsy!oEn!r(VVzrB^*^r!&pn5rg6z4>>6&y!9rvu(O1pq6kf&>e(<&^*c N002ovPDHLkV1mc&u$TY< diff --git a/src/main/resources/items/examplesword.png b/src/main/resources/items/examplesword.png index 57758c18ab539cd31d4e1408283dfebf51ec79f6..5aeae299e0a0ce1d833a519d0ffb95b299f8530e 100644 GIT binary patch delta 363 zcmdnT+{!#bxt@WsILO_J@#aaLdIkmtmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6VX;z);M<#V&fq6ez-3;1OBO zz`!jG!i)^F=12eq*-JcqUD;nTiileYE9TV)<^jb{db&7x{|kYH7oHAoWt?cWsD z#dvuEZ{t)RZb=Cs2$7fqrh6oFGK7Sf<(PDKsJt_}@1-LP_!xJ!9GSquxJOa&oPd3i6GL_^qqgD3mLv5^Z=@zEHl%ES*U)-o zt>`2k5Xcfd-(JtLpykNFlSvv)eLL5)m2+$eIQ_g>yA|XJiFdZY4<7)61u934YECmI s-JZ~Ls`KG=?hf9C2Su3{Cx|dGOiI+`yLPg~2k0vXPgg&ebxsLQ0M$-&ZU6uP delta 421 zcmV;W0b2fr1HJ>0BYy#fNklWXE*2~{ z;z|^Zw1_RDqkvzHg#i>oghGi_N3!73U_Vy~EXAt?QL@GH6)a?^8WJQYLTZNu&Jg%~ z{|`#w`0N)0^Qj#YWM@Nag#=1T2uomG5=;!=Ui<^cxls)tB!4X^vY@94SO`&6nxaGj zET5B-(qSP1at%4<1XkD6GbFGkLUKZaflRO| z{0sIedPv+_A%6*WfLg?Jc*tPOrYxkSC3FL*Q8ThoAu%(8LIRiuz#*}JGNjIU`uZ2oPfvteikkHx$)mP{Z(cEU=Xm z$ccHto8{C90h}>_TuvY2f(()NHEwg> P00000NkvXXu0mjf8`h~K diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 55de7a6..75d4784 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -8,7 +8,8 @@ exampleore=Example Ore [item] exampleitem=Example Item -exampleore=Example Ore +exampleoreitem=Example Ore +examplebaritem=Example Bar examplehuntincursionitem=Example Hunt Incursion Item examplepotionitem=Example Potion examplesword=Example Sword diff --git a/src/main/resources/mobs/icons/examplebossmob.png b/src/main/resources/mobs/icons/examplebossmob.png new file mode 100644 index 0000000000000000000000000000000000000000..471e69df3ef160406c29442b51d0cb79c29b023d GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gz!3HE#_5EXk6k~CayA$KhlREW44okYDuOkD) z#(wTUiL5}rLb6AYF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=Ijcz0ZOnX zdAqwXd|)VM;9?iOVhR-DEbxddW?FiQ&TU$@#)ZgF!ztBrxlqvDJ z=;eR&{14e;f{qW5=ile!FaFhHxKYnFy!~C#uccf`v0gp=FCRa;xjlce$Ffzc9$oEO zH}9XG-LHqtt8}iozR+vmDdKE=-VluZBt literal 0 HcmV?d00001 diff --git a/src/main/resources/mobs/icons/examplemob.png b/src/main/resources/mobs/icons/examplemob.png new file mode 100644 index 0000000000000000000000000000000000000000..59e94f3d76950e12a1c2cc896467b1abacc8c1f5 GIT binary patch literal 398 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gz!3HE#_5EXk6k~CayA$KhlREW44okYDuOkD) z#(wTUiL5}rLb6AYF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=Ijcz0ZOnX zdAqwXd|)VM;9?iOVhR-DEbxddW?Pw~@Uo`0{ z-1$ZEu=$=@PbGH*#LCU)@(XXXkNwHo%{4XCrGQJ$X$YWMY+vBS} z>*oE_+gJ2PB+T`NUi(gw=~cgWO2{6FX?d4aqbD07uCDX$=&a2NkGPY%*>4=R3QLPP zEE3+=vQx%eYhmHbZv`j$X0ZN0akA22x?RksW>KxL^OxzRxvXE?>ABqE%HM_lbK5uj lDPP!k;M%+eJG(R47>W#oj;-iT=L7nV!PC{xWt~$(697o{jpYCU literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/rock.png b/src/main/resources/objects/rock.png deleted file mode 100644 index aa51ed0427a5c368ea03de6a8132ead9438ed708..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5853 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 Date: Wed, 21 Jan 2026 19:51:04 +0000 Subject: [PATCH 04/28] Add example sound and play packet for boss mob Introduces a new sound file and registers it with custom settings. Adds ExamplePlaySoundPacket to trigger sound playback on clients, and updates ExampleAILeaf to send this packet when teleporting mobs. Also fixes extraction item ID in ExampleIncursionBiome and moves ExamplePacket to a dedicated packets package. --- src/main/java/examplemod/ExampleMod.java | 21 +++- .../examplemod/examples/ai/ExampleAI.java | 62 ++++------ .../examplemod/examples/ai/ExampleAILeaf.java | 108 +++++++++++++----- .../incursion/ExampleIncursionBiome.java | 2 +- .../examples/{ => packets}/ExamplePacket.java | 2 +- .../packets/ExamplePlaySoundPacket.java | 45 ++++++++ src/main/resources/sound/examplesound.ogg | Bin 0 -> 16094 bytes 7 files changed, 172 insertions(+), 68 deletions(-) rename src/main/java/examplemod/examples/{ => packets}/ExamplePacket.java (98%) create mode 100644 src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java create mode 100644 src/main/resources/sound/examplesound.ogg diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 4f77f57..d2fdcbf 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -13,9 +13,13 @@ import examplemod.examples.objects.ExampleBaseRockObject; import examplemod.examples.objects.ExampleOreRockObject; import examplemod.examples.ExampleRecipes; +import examplemod.examples.packets.ExamplePacket; +import examplemod.examples.packets.ExamplePlaySoundPacket; import necesse.engine.commands.CommandsManager; import necesse.engine.modLoader.annotations.ModEntry; import necesse.engine.registries.*; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; import necesse.gfx.gameTexture.GameTexture; import necesse.level.maps.biomes.Biome; @@ -24,6 +28,8 @@ public class ExampleMod { // We define our static registered objects here, so they can be referenced elsewhere public static ExampleBiome EXAMPLE_BIOME; + public static GameSound EXAMPLESOUND; + public static SoundSettings EXAMPLESOUNDSETTINGS; public void init() { System.out.println("Hello world from my example mod!"); @@ -72,8 +78,10 @@ public void init() { // Register our buff BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); - // Register our packet + // Register our packets PacketRegistry.registerPacket(ExamplePacket.class); + + PacketRegistry.registerPacket(ExamplePlaySoundPacket.class); } public void initResources() { @@ -83,8 +91,17 @@ public void initResources() { // 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"); + + //initialising the sound to be used by our boss mob + EXAMPLESOUND = GameSound.fromFile("examplesound"); + + // Optional settings (volume/pitch/falloff) – used when playing via SoundSettings + EXAMPLESOUNDSETTINGS = new SoundSettings(EXAMPLESOUND) + .volume(0.8f) + .basePitch(1.0f) + .pitchVariance(0.08f) + .fallOffDistance(900); } public void postInit() { diff --git a/src/main/java/examplemod/examples/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java index 8ba9ec6..22e4b2b 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAI.java +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -8,70 +8,58 @@ import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; public class ExampleAI extends SelectorAINode { - public final ExampleAILeaf teleporter; - /* - This node handles "find player -> chase -> try to attack" - We keep a reference so we can use its damage/knockback settings later. - */ + // My custom “fix spawn position” leaf. + // This runs first so the mob gets put somewhere sensible before doing anything else. + public final ExampleAILeaf teleporter; + + // Vanilla AI that does: find player -> chase -> when close enough, call attackTarget(). + // We keep it as a field so we can reuse the damage/knockback values from it. public final CollisionPlayerChaserAI chaser; - // This node handles "walk around randomly" when there's nothing to chase. + // Vanilla “walk around randomly” node. This is what happens when there’s no player to chase. public final WandererAINode wanderer; public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { - /* - A SelectorAINode tries its children in order. - First one that can run/works is the behaviour the mob uses. - - So: we add CHASING first, because we want chasing to "win" whenever possible. - */ + // A Selector is basically: "try child #1, if it can run then use it, + // otherwise try child #2, otherwise child #3..." + // + // So the ORDER we add children is the ORDER of priority. + // 1) Teleport / reposition leaf (highest priority). + // (In my leaf: 8 tiles = how far to check for open space, 10 tiles = how far to search for a valid spot) this.teleporter = new ExampleAILeaf<>(8, 10); - { - // 8 tiles = 256px open-space check, search within 10 tiles if it needs to move. - } addChild(this.teleporter); + + // 2) Chase + attack (second priority). this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { - // CollisionPlayerChaserAI has an attackTarget method it calls when in range. - // We override it so we can route the attack logic to OUR method below. + // The chaser decides WHEN it should attack, but it asks us HOW to attack. + // So we override this and forward it to our own method below. @Override public boolean attackTarget(T mob, Mob target) { - // "ExampleAI.this" means "the outer ExampleAI instance" - // (because we're inside an anonymous inner class right now). return ExampleAI.this.attackTarget(mob, target); } }; addChild(this.chaser); - /* - If the chaser doesn't have a target (or can't chase), - the selector will try the next child: wandering. - */ + // 3) Wander around if we aren’t teleporting, and we aren’t chasing anyone. this.wanderer = new WandererAINode<>(wanderFrequency); addChild(this.wanderer); } - /* - attack logic. - We keep it outside the chaser node so it's easy to change later. - */ + // This is the “how to attack” part used by the chaser above. + // Keeping it here makes it easy to change later (special attacks, effects, etc). public boolean attackTarget(T mob, Mob target) { - /* - simpleAttack is a helper that does a basic melee attack: - - checks if the mob can attack right now - - applies damage - - applies knockback - - returns true if an attack happened - */ + + // simpleAttack is the vanilla helper for a basic melee hit. + // It handles cooldown/range stuff internally and returns true if an attack happened. return CollisionChaserAINode.simpleAttack( mob, target, - // Use the damage/knockback values that were passed into the chaser constructor. - this.chaser.damage, - this.chaser.knockback + this.chaser.damage, // use the damage we configured in the chaser + this.chaser.knockback // use the knockback we configured in the chaser ); } } diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java index fae5455..ecfe7cf 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -4,6 +4,7 @@ import java.awt.geom.Point2D; import java.util.ArrayList; +import examplemod.examples.packets.ExamplePlaySoundPacket; import necesse.engine.util.GameMath; import necesse.engine.util.GameRandom; import necesse.entity.mobs.Mob; @@ -13,126 +14,179 @@ import necesse.level.maps.IncursionLevel; import necesse.level.maps.Level; -/** - Runs once after spawn: - If we are in an IncursionLevel and the mob is NOT inside the entrance open-space, - teleport it to a valid tile near the incursion return portal (entrance centre). +/* + This is a "run once after spawn" AI leaf. - "Entrance centre" = return portal position: - IncursionBiome.generateEntrance(...) clears an open area and calls addReturnPortalOnTile(...) - which ends up setting IncursionLevel returnPortalPosition/ID. - */ + The problem it solves: + • Sometimes a mob spawns in a bad spot in an Incursion (not in the cleared entrance area) + • If that happens, we move it into the entrance area so the fight / behaviour starts properly + + What we consider the "entrance area": + • The centre is the Incursion return portal position + • The biome generator clears open space around that portal +*/ public class ExampleAILeaf extends AINode { + // We only want to do the check once per mob. private boolean didCheck = false; - // How close the mob must be to the entrance centre to count as "in the entrance area" + // How close you need to be to the portal to count as "in the entrance area" + // (in tiles, converted to pixels later). private final int openRadiusTiles; - // How far around the portal we will search to find a valid teleport tile + // How far we search around the portal to find a safe landing spot. private final int searchRadiusTiles; public ExampleAILeaf(int openRadiusTiles, int searchRadiusTiles) { + // Just making sure we don't get silly values. this.openRadiusTiles = Math.max(1, openRadiusTiles); this.searchRadiusTiles = Math.max(this.openRadiusTiles, searchRadiusTiles); } @Override protected void onRootSet(AINode aiNode, T t, Blackboard blackboard) { - + // Nothing needed here for this leaf. } @Override public void init(T mob, Blackboard blackboard) { - // Nothing to init; we just run once in tick(). + // Nothing to init. We just do everything in tick() one time. } @Override public AINodeResult tick(T mob, Blackboard blackboard) { - // Only do this once per mob instance. + + // Don’t keep doing this every tick. We only run it once. if (didCheck) return AINodeResult.FAILURE; didCheck = true; - // Only do this on the server (positions are authoritative there). + // Only do this on the server. + // The server is the “real” source of truth for mob position. if (!mob.isServer()) return AINodeResult.FAILURE; Level level = mob.getLevel(); + + // This only applies to Incursion levels. if (!(level instanceof IncursionLevel)) return AINodeResult.FAILURE; IncursionLevel incursion = (IncursionLevel) level; - // Entrance centre is the return portal position. + // The "entrance centre" is the return portal position. + // If this is null, something is wrong / not generated yet, so bail. Point portalPos = incursion.getReturnPortalPosition(); if (portalPos == null) return AINodeResult.FAILURE; float centerX = portalPos.x; float centerY = portalPos.y; - // If already inside the entrance open space, do nothing. + // If we're already close enough to the portal, we count as “in the entrance area”. + // (Tiles -> pixels. Tiles are 32x32 in Necesse.) float openRadiusPx = this.openRadiusTiles * 32.0f; if (mob.getDistance(centerX, centerY) <= openRadiusPx) { + // Already fine, do nothing. return AINodeResult.FAILURE; } - // Otherwise teleport to a nearby valid spot close to the portal. + // We’re NOT in the entrance area, so we need to move the mob. + // Look for a valid spot near the portal. Point2D.Float dest = findValidTeleportPos(incursion, mob, centerX, centerY, this.searchRadiusTiles); + // If we found somewhere safe, teleport the mob there. if (dest != null) { - // "Direct" position set (sends movement packet if needed). + + // Stop any current movement so we don't fight with pathing. mob.stopMoving(); + + // Move instantly. + // The "true" here is important: it makes sure the move is synced properly. mob.setPos(dest.x, dest.y, true); + + // Sounds must be played on clients, not on the server. + // So we send a packet to nearby clients telling them to play the sound here. + mob.getLevel().getServer().network.sendToClientsWithEntity( + new ExamplePlaySoundPacket(mob.x, mob.y), + mob + ); } - // Return FAILURE so parent Selector continues to chase/wander this same tick. + // We always return FAILURE here because this leaf isn't meant to "take over" the AI. + // It's just a one-time fix, then the parent Selector can move on to chase/wander normally. return AINodeResult.FAILURE; } + /* + Find a safe tile near the portal to teleport to. + + We search in "rings" around the centre: + • r = 0 is the centre tile + • r = 1 is the tiles touching it + • r = 2 is the next ring out, etc + + For each tile we check: + • Not liquid + • Not shore + • Mob doesn't collide with the map + • Mob doesn't collide with another mob/player + */ private static Point2D.Float findValidTeleportPos(IncursionLevel level, Mob mob, float centerX, float centerY, int searchRadiusTiles) { int centerTileX = GameMath.getTileCoordinate(centerX); int centerTileY = GameMath.getTileCoordinate(centerY); - // Try rings from 0 outward (0 = centre) + // Try rings from centre outward until we find something. for (int r = 0; r <= searchRadiusTiles; r++) { ArrayList ring = buildRing(centerTileX, centerTileY, r); - // Randomise the order a bit so we don't always pick the same spot + // Shuffle-ish: pick random points so we don't always choose the same spot every time. while (!ring.isEmpty()) { Point p = ring.remove(GameRandom.globalRandom.nextInt(ring.size())); - // Avoid liquid/shore like vanilla spawn logic + // Don't teleport into water/shore. if (level.isLiquidTile(p.x, p.y)) continue; if (level.isShore(p.x, p.y)) continue; + // Convert tile coords to world pixel coords. + // +16 puts us in the centre of the tile. int px = p.x * 32 + 16; int py = p.y * 32 + 16; - // Must not collide with the map, objects, etc. + // Make sure the mob can actually stand there. if (mob.collidesWith(level, px, py)) continue; - // Also avoid landing inside another mob/player + // Also don't land inside another mob/player. if (mob.collidesWithAnyMob(level, px, py)) continue; + // This spot looks good. return new Point2D.Float(px, py); } } - // No valid tile found + // Couldn't find a valid spot. return null; } + /* + Build a list of tile points that make up a square "ring" around (cx, cy). + + r = 0: just the centre + r = 1: the outer edge of a 3x3 square + r = 2: the outer edge of a 5x5 square + etc + */ private static ArrayList buildRing(int cx, int cy, int r) { ArrayList points = new ArrayList<>(); + if (r == 0) { points.add(new Point(cx, cy)); return points; } - // Top and bottom edges + // Top + bottom edges for (int dx = -r; dx <= r; dx++) { points.add(new Point(cx + dx, cy - r)); points.add(new Point(cx + dx, cy + r)); } - // Left and right edges (excluding corners already added) + + // Left + right edges (skip corners because top/bottom already added them) for (int dy = -r + 1; dy <= r - 1; dy++) { points.add(new Point(cx - r, cy + dy)); points.add(new Point(cx + r, cy + dy)); diff --git a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java index e9ffdde..e7981c3 100644 --- a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java @@ -32,7 +32,7 @@ public ExampleIncursionBiome() { // Items required to be obtained when completing an extraction objective in this incursion @Override public Collection getExtractionItems(IncursionData data) { - return Collections.singleton(ItemRegistry.getItem("exampleore")); + return Collections.singleton(ItemRegistry.getItem("exampleoreitem")); } /** diff --git a/src/main/java/examplemod/examples/ExamplePacket.java b/src/main/java/examplemod/examples/packets/ExamplePacket.java similarity index 98% rename from src/main/java/examplemod/examples/ExamplePacket.java rename to src/main/java/examplemod/examples/packets/ExamplePacket.java index f0255c8..6264da2 100644 --- a/src/main/java/examplemod/examples/ExamplePacket.java +++ b/src/main/java/examplemod/examples/packets/ExamplePacket.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.packets; import necesse.engine.network.NetworkPacket; import necesse.engine.network.Packet; diff --git a/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java new file mode 100644 index 0000000..fe80682 --- /dev/null +++ b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java @@ -0,0 +1,45 @@ +package examplemod.examples.packets; + +import examplemod.ExampleMod; +import necesse.engine.network.NetworkPacket; +import necesse.engine.network.Packet; +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.client.Client; +import necesse.engine.sound.SoundEffect; + +/** + * SERVER -> CLIENT packet: + * Tells the receiving client to play the example sound at a specific world position (x, y). + */ + +public class ExamplePlaySoundPacket extends Packet { + public final float x; + public final float y; + + // Decode (CLIENT receiving) + public ExamplePlaySoundPacket(byte[] data) { + super(data); + PacketReader r = new PacketReader(this); + x = r.getNextFloat(); + y = r.getNextFloat(); + } + + // Encode (SERVER sending) + public ExamplePlaySoundPacket(float x, float y) { + this.x = x; + this.y = y; + + PacketWriter w = new PacketWriter(this); + w.putNextFloat(x); + w.putNextFloat(y); + } + + // Runs ONLY on client + @Override + public void processClient(NetworkPacket packet, Client client) { + if (ExampleMod.EXAMPLESOUNDSETTINGS != null) { + ExampleMod.EXAMPLESOUNDSETTINGS.play(SoundEffect.effect(x, y)); + } + } +} diff --git a/src/main/resources/sound/examplesound.ogg b/src/main/resources/sound/examplesound.ogg new file mode 100644 index 0000000000000000000000000000000000000000..40c9c2073366ddebe3c9b5e88342db158104326a GIT binary patch literal 16094 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 literal 0 HcmV?d00001 From 978f2a7de659e804d4fbbfd946192127f3c6fdb7 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Thu, 22 Jan 2026 01:14:22 +0000 Subject: [PATCH 05/28] Refactor item/object IDs and add ExampleStoneItem Standardized item and object IDs by removing 'item' suffixes and updating all references accordingly. Added a new ExampleStoneItem class and registered it. Updated resource file names and localization keys to match new IDs. --- src/main/java/examplemod/ExampleMod.java | 15 ++++++++------- .../java/examplemod/examples/ExampleRecipes.java | 14 +++++++------- .../incursion/ExampleIncursionBiome.java | 4 ++-- .../examples/items/ExampleStoneItem.java | 9 +++++++++ .../examples/objects/ExampleBaseRockObject.java | 4 +--- .../examples/objects/ExampleOreRockObject.java | 2 -- .../items/{examplebaritem.png => examplebar.png} | Bin .../{examplefooditem.png => examplefood.png} | Bin ...item.png => examplehuntincursionmaterial.png} | Bin .../items/{exampleoreitem.png => exampleore.png} | Bin .../{examplepotionitem.png => examplepotion.png} | Bin src/main/resources/locale/en.lang | 11 ++++++----- 12 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 src/main/java/examplemod/examples/items/ExampleStoneItem.java rename src/main/resources/items/{examplebaritem.png => examplebar.png} (100%) rename src/main/resources/items/{examplefooditem.png => examplefood.png} (100%) rename src/main/resources/items/{examplehuntincursionitem.png => examplehuntincursionmaterial.png} (100%) rename src/main/resources/items/{exampleoreitem.png => exampleore.png} (100%) rename src/main/resources/items/{examplepotionitem.png => examplepotion.png} (100%) diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index d2fdcbf..5b0f488 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -51,20 +51,21 @@ public void init() { // Register a rock for the example incursion to use as cave walls ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); - ObjectRegistry.registerObject(ExampleBaseRockObject.ID, exampleBaseRock, -1.0F, true); + ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); // Register an ore rock that overlays onto our incursion rock - ObjectRegistry.registerObject(ExampleOreRockObject.ID, new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); // Register our items ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); - ItemRegistry.registerItem("exampleoreitem", new ExampleOreItem(), 25, true); - ItemRegistry.registerItem("examplebaritem", new ExampleBarItem(),50,true); - ItemRegistry.registerItem("examplehuntincursionitem", new ExampleHuntIncursionMaterialItem(), 50, true); + ItemRegistry.registerItem("examplestone", new ExampleStoneItem(),15,true); + ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("examplebar", new ExampleBarItem(),50,true); + ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); ItemRegistry.registerItem("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); + ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); + ItemRegistry.registerItem("examplefood", new ExampleFoodItem(),15, true); // Register our mob MobRegistry.registerMob("examplemob", ExampleMob.class, true); diff --git a/src/main/java/examplemod/examples/ExampleRecipes.java b/src/main/java/examplemod/examples/ExampleRecipes.java index 174e8bf..40c542a 100644 --- a/src/main/java/examplemod/examples/ExampleRecipes.java +++ b/src/main/java/examplemod/examples/ExampleRecipes.java @@ -16,11 +16,11 @@ public static void registerRecipes(){ // Example Bar item smelted in the forge Recipes.registerModRecipe(new Recipe( - "examplebaritem", + "examplebar", 1, RecipeTechRegistry.FORGE, new Ingredient[]{ - new Ingredient("exampleoreitem",2) + new Ingredient("exampleore",2) }) ); @@ -31,7 +31,7 @@ public static void registerRecipes(){ 1, RecipeTechRegistry.NONE, new Ingredient[]{ - new Ingredient("examplebaritem", 2) + new Ingredient("examplebar", 2) } ).showAfter("woodboat")); // Show recipe after wood boat recipe @@ -43,7 +43,7 @@ public static void registerRecipes(){ RecipeTechRegistry.IRON_ANVIL, new Ingredient[]{ new Ingredient("exampleitem", 4), - new Ingredient("examplebaritem", 5) + new Ingredient("examplebar", 5) } )); @@ -54,13 +54,13 @@ public static void registerRecipes(){ RecipeTechRegistry.WORKSTATION, new Ingredient[]{ new Ingredient("exampleitem", 4), - new Ingredient("examplebaritem", 10) + new Ingredient("examplebar", 10) } ).showAfter("exampleitem")); // Show the recipe after example item recipe // Example food item recipe Recipes.registerModRecipe(new Recipe( - "examplefooditem", + "examplefood", 1, RecipeTechRegistry.COOKING_POT, new Ingredient[]{ @@ -72,7 +72,7 @@ public static void registerRecipes(){ // Example potion item recipe Recipes.registerModRecipe(new Recipe( - "examplepotionitem", + "examplepotion", 1, RecipeTechRegistry.ALCHEMY, new Ingredient[]{ diff --git a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java index e7981c3..379782a 100644 --- a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java @@ -32,7 +32,7 @@ public ExampleIncursionBiome() { // Items required to be obtained when completing an extraction objective in this incursion @Override public Collection getExtractionItems(IncursionData data) { - return Collections.singleton(ItemRegistry.getItem("exampleoreitem")); + return Collections.singleton(ItemRegistry.getItem("exampleore")); } /** @@ -42,7 +42,7 @@ public Collection getExtractionItems(IncursionData data) { @Override public LootTable getHuntDrop(IncursionData incursionData) { return new LootTable( - new ChanceLootItem(0.66F, "examplehuntincursionitem") + new ChanceLootItem(0.66F, "examplehuntincursionmaterial") ); } diff --git a/src/main/java/examplemod/examples/items/ExampleStoneItem.java b/src/main/java/examplemod/examples/items/ExampleStoneItem.java new file mode 100644 index 0000000..f217e3b --- /dev/null +++ b/src/main/java/examplemod/examples/items/ExampleStoneItem.java @@ -0,0 +1,9 @@ +package examplemod.examples.items; + +import necesse.inventory.item.placeableItem.StonePlaceableItem; + +public class ExampleStoneItem extends StonePlaceableItem { + public ExampleStoneItem(){ + super(100); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java index 44ed536..9379e67 100644 --- a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -5,10 +5,8 @@ public class ExampleBaseRockObject extends RockObject { - public static final String ID = "examplebaserock"; - public ExampleBaseRockObject() { - super("examplebaserock", new Color(92, 37, 23), "stone", "objects", "landscaping"); + super("examplebaserock", new Color(92, 37, 23), "examplestone", "objects", "landscaping"); this.toolTier = 5.0F; } } diff --git a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java index f2e1e3d..6379944 100644 --- a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -9,8 +9,6 @@ */ public class ExampleOreRockObject extends RockOreObject { - public static final String ID = "exampleorerock"; - public ExampleOreRockObject(RockObject parentRock) { super( parentRock, diff --git a/src/main/resources/items/examplebaritem.png b/src/main/resources/items/examplebar.png similarity index 100% rename from src/main/resources/items/examplebaritem.png rename to src/main/resources/items/examplebar.png diff --git a/src/main/resources/items/examplefooditem.png b/src/main/resources/items/examplefood.png similarity index 100% rename from src/main/resources/items/examplefooditem.png rename to src/main/resources/items/examplefood.png diff --git a/src/main/resources/items/examplehuntincursionitem.png b/src/main/resources/items/examplehuntincursionmaterial.png similarity index 100% rename from src/main/resources/items/examplehuntincursionitem.png rename to src/main/resources/items/examplehuntincursionmaterial.png diff --git a/src/main/resources/items/exampleoreitem.png b/src/main/resources/items/exampleore.png similarity index 100% rename from src/main/resources/items/exampleoreitem.png rename to src/main/resources/items/exampleore.png diff --git a/src/main/resources/items/examplepotionitem.png b/src/main/resources/items/examplepotion.png similarity index 100% rename from src/main/resources/items/examplepotionitem.png rename to src/main/resources/items/examplepotion.png diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 75d4784..f5d37dc 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -8,13 +8,14 @@ exampleore=Example Ore [item] exampleitem=Example Item -exampleoreitem=Example Ore -examplebaritem=Example Bar -examplehuntincursionitem=Example Hunt Incursion Item -examplepotionitem=Example Potion +examplestone=Example Stone +exampleore=Example Ore +examplebar=Example Bar +examplehuntincursionmaterial=Example Hunt Incursion Material +examplepotion=Example Potion examplesword=Example Sword examplestaff=Example Staff -examplefooditem=Example Food +examplefood=Example Food [itemtooltip] examplestafftip=Shoots a homing, piercing projectile From 17d29f0f2557c11b767d86ef465f6e584974ee4c Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Thu, 22 Jan 2026 01:58:24 +0000 Subject: [PATCH 06/28] Play sound for AI leaf on both teleport and failure Added logic to send a sound packet to clients when the AI leaf fails due to proximity, ensuring sounds are played for both teleport and non-teleport cases. Also added examplestone.png to resources. --- .../examplemod/examples/ai/ExampleAILeaf.java | 9 ++++++++- src/main/resources/items/examplestone.png | Bin 0 -> 467 bytes 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/items/examplestone.png diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java index ecfe7cf..30d128a 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -83,6 +83,13 @@ public AINodeResult tick(T mob, Blackboard blackboard) { // (Tiles -> pixels. Tiles are 32x32 in Necesse.) float openRadiusPx = this.openRadiusTiles * 32.0f; if (mob.getDistance(centerX, centerY) <= openRadiusPx) { + + // Sounds must be played on clients, not on the server. + // So we send a packet to nearby clients telling them to play the sound here if teleport doesnt happen . + mob.getLevel().getServer().network.sendToClientsWithEntity( + new ExamplePlaySoundPacket(mob.x, mob.y), + mob + ); // Already fine, do nothing. return AINodeResult.FAILURE; } @@ -102,7 +109,7 @@ public AINodeResult tick(T mob, Blackboard blackboard) { mob.setPos(dest.x, dest.y, true); // Sounds must be played on clients, not on the server. - // So we send a packet to nearby clients telling them to play the sound here. + // So we send a packet to nearby clients telling them to play the sound here if teleport happens . mob.getLevel().getServer().network.sendToClientsWithEntity( new ExamplePlaySoundPacket(mob.x, mob.y), mob diff --git a/src/main/resources/items/examplestone.png b/src/main/resources/items/examplestone.png new file mode 100644 index 0000000000000000000000000000000000000000..5b0489d744636404ae4f0ea00d22bda663fb7c45 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$);LoovvyXX~Dpa^GyM`SSr1Gg{;GcwGYBLNg-FY)wsWq-ve#%N|Tt9XwC&;{b2 zE{-7)hm#W|UN%gO>HWns>;K1-@hLm}rrh*lmSxg;^2=t*!Tt%C{vT;!xjey>S>?x^ zMj&wPcAN{M7fxI#oj*&WDzjlie`12^VYRk}bGa{kwmj3G!zdJS^?u?Hj`aEW3npZ+ z^XwB)Fx{HWd|e{%lDeSYT?1}8(i@eU|1sRO^9DbiVl1@+602bw+|?PaJRXT2~frNzO=zRn7^oJhj+cBqCnk xmvitJKC Date: Thu, 22 Jan 2026 03:38:55 +0000 Subject: [PATCH 07/28] Add landscaping recipes and update item textures Introduces landscaping recipes for examplebaserock and exampleorerock in ExampleRecipes.java. Updates textures for examplebaserock.png, exampleorerock.png, and exampleore.png to reflect new or improved visuals. --- .../examplemod/examples/ExampleRecipes.java | 51 ++++++++++++------ src/main/resources/items/examplebaserock.png | Bin 547 -> 5617 bytes src/main/resources/items/exampleorerock.png | Bin 547 -> 5617 bytes src/main/resources/objects/exampleore.png | Bin 730 -> 8435 bytes 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main/java/examplemod/examples/ExampleRecipes.java b/src/main/java/examplemod/examples/ExampleRecipes.java index 40c542a..058ea11 100644 --- a/src/main/java/examplemod/examples/ExampleRecipes.java +++ b/src/main/java/examplemod/examples/ExampleRecipes.java @@ -14,17 +14,6 @@ public class ExampleRecipes { //Put your recipe registrations in here public static void registerRecipes(){ - // Example Bar item smelted in the forge - Recipes.registerModRecipe(new Recipe( - "examplebar", - 1, - RecipeTechRegistry.FORGE, - new Ingredient[]{ - new Ingredient("exampleore",2) - }) - ); - - // Example item recipe, crafted in inventory for 2 iron bars Recipes.registerModRecipe(new Recipe( "exampleitem", @@ -36,7 +25,17 @@ public static void registerRecipes(){ ).showAfter("woodboat")); // Show recipe after wood boat recipe - // Example sword recipe, crafted in iron anvil using 4 example items and 5 copper bars + //FORGE RECIPES + Recipes.registerModRecipe(new Recipe( + "examplebar", + 1, + RecipeTechRegistry.FORGE, + new Ingredient[]{ + new Ingredient("exampleore",2) + }) + ); + + //IRON ANVIL RECIPES Recipes.registerModRecipe(new Recipe( "examplesword", 1, @@ -47,7 +46,7 @@ public static void registerRecipes(){ } )); - // Example staff recipe, crafted in workstation using 4 example items and 10 gold bars + //WORKSTATION RECIPES Recipes.registerModRecipe(new Recipe( "examplestaff", 1, @@ -58,7 +57,7 @@ public static void registerRecipes(){ } ).showAfter("exampleitem")); // Show the recipe after example item recipe - // Example food item recipe + //COOKING POT RECIPES Recipes.registerModRecipe(new Recipe( "examplefood", 1, @@ -70,7 +69,7 @@ public static void registerRecipes(){ } )); - // Example potion item recipe + //ALCHEMY RECIPES Recipes.registerModRecipe(new Recipe( "examplepotion", 1, @@ -79,5 +78,27 @@ public static void registerRecipes(){ 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), + } + )); + + } } diff --git a/src/main/resources/items/examplebaserock.png b/src/main/resources/items/examplebaserock.png index bfc82382d32caeed91cd11fe1256e27b535a4b64..f948e26509500a5cc4c8e3dc4a9c49f69375b0fc 100644 GIT binary patch literal 5617 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 From 785c7d2c48f5427deb4d3e5d44754ed88a410145 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Fri, 23 Jan 2026 04:21:03 +0000 Subject: [PATCH 08/28] Add example loot table and preset, update incursion level Introduces ExampleLootTable and ExamplePreset classes for custom loot and structure generation. Updates ExampleIncursionLevel to use the new preset, demonstrating how to place custom structures and fill them with loot during incursion level generation. --- .../examplemod/examples/ExampleLootTable.java | 21 +++++++ .../examplemod/examples/ExamplePreset.java | 50 ++++++++++++++++ .../incursion/ExampleIncursionLevel.java | 57 ++++++++++++++----- 3 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 src/main/java/examplemod/examples/ExampleLootTable.java create mode 100644 src/main/java/examplemod/examples/ExamplePreset.java diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java new file mode 100644 index 0000000..ef87e87 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -0,0 +1,21 @@ +package examplemod.examples; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.LootItem; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.inventory.lootTable.lootItem.OneOfLootItems; + +public class ExampleLootTable { + public static final LootTable exampleloottable = new LootTable( + new LootItem("exampleore", 8), + new LootItem("examplebar", 20), + new LootItem("examplepotion",1), + new LootItem("examplefood",1), + new OneOfLootItems( + new ChanceLootItem(0.60f, "examplesword"), + new ChanceLootItem(0.60f,"examplestaff") + ) + ); + + private ExampleLootTable() { } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExamplePreset.java b/src/main/java/examplemod/examples/ExamplePreset.java new file mode 100644 index 0000000..8f83c99 --- /dev/null +++ b/src/main/java/examplemod/examples/ExamplePreset.java @@ -0,0 +1,50 @@ +package examplemod.examples; + +import examplemod.ExampleMod; +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +public class ExamplePreset extends Preset { + public ExamplePreset(GameRandom random) { + // Pass a GameRandom so loot is randomized + super(15, 11); + + int floor = TileRegistry.getTileID("stonefloor"); + int wall = ObjectRegistry.getObjectID("stonewall"); + int air = ObjectRegistry.getObjectID("air"); + int storagebox = ObjectRegistry.getObjectID("storagebox"); // replace with what you want + + // Fill background + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + setTile(x, y, floor); + setObject(x, y, air); + } + } + + // Simple rectangle “room” + for (int x = 0; x < width; x++) { + setObject(x, 0, wall); + setObject(x, height - 1, wall); + } + for (int y = 0; y < height; y++) { + setObject(0, y, wall); + setObject(width - 1, y, wall); + } + + // Put a storagebox + int storageboxX = width / 2; + int storageboxY = height / 2; + setObject(storageboxX,storageboxY, storagebox,1); + + //Fill the storage box with loot from our example loot table + addInventory(ExampleLootTable.exampleloottable, random,storageboxX,storageboxY); + + // Optional: only allow placing if the whole area isn’t floor already (example rule) + addCanApplyRectEachPredicate(0, 0, width, height, 0, (level, levelX, levelY, dir) -> + !level.getTile(levelX, levelY).isFloor + ); + } +} diff --git a/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java index 8e0b4d9..e3f7acc 100644 --- a/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java @@ -1,6 +1,7 @@ package examplemod.examples.incursion; import examplemod.ExampleMod; +import examplemod.examples.ExamplePreset; import necesse.engine.GameEvents; import necesse.engine.events.worldGeneration.GenerateCaveLayoutEvent; import necesse.engine.events.worldGeneration.GeneratedCaveOresEvent; @@ -15,6 +16,9 @@ import necesse.level.maps.incursion.BiomeExtractionIncursionData; import necesse.level.maps.incursion.BiomeMissionIncursionData; import necesse.level.maps.incursion.IncursionBiome; +import necesse.level.maps.presets.Preset; + +import java.awt.*; /** * Example incursion level. @@ -44,44 +48,67 @@ 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", "examplebaserock"); - - // Seed the generator so this incursion layout is deterministic per mission cg.random.setSeed(incursionData.getUniqueID()); - // Fire the cave layout generation event, allowing mods or perks to modify - // or cancel cave generation before the default logic runs GameEvents.triggerEvent( new GenerateCaveLayoutEvent(this, cg), - e -> { - cg.generateLevel(0.38F, 4, 3, 6); - } + e -> cg.generateLevel(0.38F, 4, 3, 6) ); - // Used to reserve space so later generation steps avoid overwriting the entrance + //entrance + perks (anything that must never be overwritten by perk presets) PresetGeneration entranceAndPerkPresets = new PresetGeneration(this); - // Generate an incursion entrance that clears terrain, - // blends edges, reserves space, and places the return portal - IncursionBiome.generateEntrance( + //your own structures (custom rooms, etc.) + PresetGeneration structurePresets = new PresetGeneration(this); + + // Generate entrance (this reserves space inside entranceAndPerkPresets) + int spawnSize = 32; + Point entranceMid = IncursionBiome.generateEntrance( this, entranceAndPerkPresets, cg.random, - 32, + spawnSize, cg.rockTile, "exampletile", "exampletile", "exampleobject" ); - // Now call incursion perks to generate their presets + // reserve the entrance space in structurePresets too, so your own structures don't overwrite the entrance area. + int ex = entranceMid.x - spawnSize / 2; + int ey = entranceMid.y - spawnSize / 2; + structurePresets.addOccupiedSpace(ex, ey, spawnSize, spawnSize); + + + //EXAMPLE PRESET + Preset examplePreset = new ExamplePreset(cg.random); + Point placedAt = structurePresets.findRandomValidPositionAndApply( + cg.random, + 2500,//realistically this would be lower if you didn't want it to be guaranteed + examplePreset, + 8, + true,// randomizeMirrorX + true, // randomizeMirrorY + true, // randomizeRotation + false // overrideCanPlace (false = respect canApply rules) + ); + + if (placedAt != null) { + structurePresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + entranceAndPerkPresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + } + + /* + Perk presets use entranceAndPerkPresets, so they avoid the entrance as well as any presets + you added to structurePresets + */ generatePresetsBasedOnPerks(altarData, entranceAndPerkPresets, cg.random, baseBiome); // This call clears all invalid objects/tiles, so that there are no cut in half beds, etc. GenerationTools.checkValid(this); - // For extraction incursions, guarantee tungsten ore veins for objectives + // For extraction incursions, guarantee example ore veins for objectives if (incursionData instanceof BiomeExtractionIncursionData) { cg.generateGuaranteedOreVeins(40, 4, 8, ObjectRegistry.getObjectID("exampleorerock")); } From 7512ef335f1256ac81bb0cecc3d108892af1b399 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Fri, 23 Jan 2026 19:48:25 +0000 Subject: [PATCH 09/28] Refactor ExamplePreset and add utility classes Refactored ExamplePreset to use a script-based preset application instead of manual tile/object placement. Added ExamplePresetCode for the original code-based preset logic, and introduced MissingTextureReporter utility for handling missing texture errors with logging and on-screen notifications. --- .../examplemod/examples/ExamplePreset.java | 41 ++------ .../examples/ExamplePresetCode.java | 49 ++++++++++ .../examples/util/MissingTextureReporter.java | 98 +++++++++++++++++++ 3 files changed, 156 insertions(+), 32 deletions(-) create mode 100644 src/main/java/examplemod/examples/ExamplePresetCode.java create mode 100644 src/main/java/examplemod/examples/util/MissingTextureReporter.java diff --git a/src/main/java/examplemod/examples/ExamplePreset.java b/src/main/java/examplemod/examples/ExamplePreset.java index 8f83c99..5b57cb3 100644 --- a/src/main/java/examplemod/examples/ExamplePreset.java +++ b/src/main/java/examplemod/examples/ExamplePreset.java @@ -1,46 +1,23 @@ package examplemod.examples; -import examplemod.ExampleMod; -import necesse.engine.registries.ObjectRegistry; -import necesse.engine.registries.TileRegistry; import necesse.engine.util.GameRandom; import necesse.level.maps.presets.Preset; public class ExamplePreset extends Preset { + // Pass a GameRandom so loot is randomized public ExamplePreset(GameRandom random) { - // Pass a GameRandom so loot is randomized - super(15, 11); + //width and height of our preset + super(11, 11); - int floor = TileRegistry.getTileID("stonefloor"); - int wall = ObjectRegistry.getObjectID("stonewall"); - int air = ObjectRegistry.getObjectID("air"); - int storagebox = ObjectRegistry.getObjectID("storagebox"); // replace with what you want + /*string output of the preset from the game decoded from url safe base64 zlib compressed text + you don't actually need to decompress this but it makes showing whats going on easier */ + String examplePresetScript = "PRESET={width=11,height=11,tileIDs=[98, exampletile],tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98],objectIDs=[0, air, 290, storagebox, 85, woodwall, 298, walltorch],objects=[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 290, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85],rotations=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2],tileObjectsClear=true,wallDecorObjectsClear=true,tableDecorObjectsClear=true,clearOtherWires=false}\n"; - // Fill background - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - setTile(x, y, floor); - setObject(x, y, air); - } - } - - // Simple rectangle “room” - for (int x = 0; x < width; x++) { - setObject(x, 0, wall); - setObject(x, height - 1, wall); - } - for (int y = 0; y < height; y++) { - setObject(0, y, wall); - setObject(width - 1, y, wall); - } - - // Put a storagebox - int storageboxX = width / 2; - int storageboxY = height / 2; - setObject(storageboxX,storageboxY, storagebox,1); + //actually apply the preset from examplePresetScript onto the world + this.applyScript(examplePresetScript); //Fill the storage box with loot from our example loot table - addInventory(ExampleLootTable.exampleloottable, random,storageboxX,storageboxY); + addInventory(ExampleLootTable.exampleloottable, random,5,5); // Optional: only allow placing if the whole area isn’t floor already (example rule) addCanApplyRectEachPredicate(0, 0, width, height, 0, (level, levelX, levelY, dir) -> diff --git a/src/main/java/examplemod/examples/ExamplePresetCode.java b/src/main/java/examplemod/examples/ExamplePresetCode.java new file mode 100644 index 0000000..be9deff --- /dev/null +++ b/src/main/java/examplemod/examples/ExamplePresetCode.java @@ -0,0 +1,49 @@ +package examplemod.examples; + +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +public class ExamplePresetCode extends Preset { + public ExamplePresetCode(GameRandom random) { + // Pass a GameRandom so loot is randomized + super(15, 11); + + int floor = TileRegistry.getTileID("stonefloor"); + int wall = ObjectRegistry.getObjectID("stonewall"); + int air = ObjectRegistry.getObjectID("air"); + int storagebox = ObjectRegistry.getObjectID("storagebox"); // replace with what you want + + // Fill background + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + setTile(x, y, floor); + setObject(x, y, air); + } + } + + // Simple rectangle “room” + for (int x = 0; x < width; x++) { + setObject(x, 0, wall); + setObject(x, height - 1, wall); + } + for (int y = 0; y < height; y++) { + setObject(0, y, wall); + setObject(width - 1, y, wall); + } + + // Put a storagebox + int storageboxX = width / 2; + int storageboxY = height / 2; + setObject(storageboxX,storageboxY, storagebox,1); + + //Fill the storage box with loot from our example loot table + addInventory(ExampleLootTable.exampleloottable, random,storageboxX,storageboxY); + + // Optional: only allow placing if the whole area isn’t floor already (example rule) + addCanApplyRectEachPredicate(0, 0, width, height, 0, (level, levelX, levelY, dir) -> + !level.getTile(levelX, levelY).isFloor + ); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/util/MissingTextureReporter.java b/src/main/java/examplemod/examples/util/MissingTextureReporter.java new file mode 100644 index 0000000..11f4f58 --- /dev/null +++ b/src/main/java/examplemod/examples/util/MissingTextureReporter.java @@ -0,0 +1,98 @@ +package examplemod.examples.util; + +import java.util.HashSet; + +import necesse.engine.GameLog; +import necesse.engine.util.GameUtils; +import necesse.engine.window.GameWindow; +import necesse.engine.window.WindowManager; +import necesse.gfx.GameResources; +import necesse.gfx.Renderer; +import necesse.gfx.gameFont.FontOptions; +import necesse.engine.screenHudManager.ScreenFloatTextFade; +import necesse.gfx.gameTexture.GameTexture; + +public class MissingTextureReporter { + + private static final HashSet seen = new HashSet<>(); + private static GameTexture fallback; + + public static GameTexture reportMissingTexture(String filePath, boolean outsideGame, boolean forceNotFinalize, Throwable thrown) { + // Normalize extension (matches vanilla) + String normalized = filePath == null ? "null" : GameUtils.formatFileExtension(filePath, "png"); + + // Find “who asked for this texture” (best effort) + String caller = findUsefulCaller(); + + // Log once per texture path (avoid spam) + if (seen.add(normalized)) { + GameLog.warn.println("[MissingTexture] Could not load: " + normalized + + " outsideGame=" + outsideGame + + " forceNotFinalize=" + forceNotFinalize + + (caller != null ? " caller=" + caller : "") + + " (" + thrown.getClass().getSimpleName() + ": " + thrown.getMessage() + ")"); + + // On-screen notice (client HUD) + showOnScreen("Missing texture: " + normalized); + } + + // Return a safe texture so the game keeps running + if (GameResources.error != null) return GameResources.error; + return getOrCreateFallback(); + } + + private static GameTexture getOrCreateFallback() { + if (fallback != null) return fallback; + + // 1x1 magenta fallback + GameTexture t = new GameTexture("missing-texture-fallback", 1, 1); + t.setPixel(0, 0, 255, 0, 255, 255); + t.makeFinal(); + fallback = t; + return fallback; + } + + private static void showOnScreen(String msg) { + try { + GameWindow window = WindowManager.getWindow(); + if (window == null) return; + + int x = Math.max(1, window.getHudWidth() / 2); + int y = 40; + + FontOptions opt = new FontOptions(16) + .color(255, 80, 80) + .outline(0, 0, 0); + + ScreenFloatTextFade text = new ScreenFloatTextFade(x, y, msg, opt); + text.avoidOtherText = true; + text.riseTime = 600; + text.hoverTime = 400; + text.fadeTime = 2200; + + Renderer.hudManager.addElement(text); + } catch (Throwable ignored) { + // Never crash from the reporter itself + } + } + + private static String findUsefulCaller() { + try { + StackTraceElement[] st = new Exception().getStackTrace(); + for (StackTraceElement e : st) { + String c = e.getClassName(); + if (c == null) continue; + + // Skip obvious internal frames + if (c.startsWith("net.bytebuddy.")) continue; + if (c.startsWith("examplemod.examples.patches.")) continue; + if (c.startsWith("examplemod.examples.util.MissingTextureReporter")) continue; + if (c.startsWith("necesse.gfx.gameTexture.GameTexture")) continue; + + return c + "#" + e.getMethodName() + ":" + e.getLineNumber(); + } + } catch (Throwable ignored) { + } + return null; + } +} From bab2f9ebae692e5c7c0c7c8d8336fa4c2ee8cf2e Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Sat, 24 Jan 2026 00:46:50 +0000 Subject: [PATCH 10/28] Add example wall, door, and window objects Introduced ExampleWallWindowDoorObject with registration logic for wall, door, and window objects. Added corresponding textures and updated localization entries for the new objects. Updated ExampleMod to register these new objects during initialization. --- src/main/java/examplemod/ExampleMod.java | 11 ++--- .../objects/ExampleWallWindowDoorObject.java | 38 ++++++++++++++++++ src/main/resources/items/exampledoor.png | Bin 0 -> 406 bytes src/main/resources/items/examplewall.png | Bin 0 -> 448 bytes src/main/resources/locale/en.lang | 2 + src/main/resources/objects/examplewall.png | Bin 0 -> 2796 bytes 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java create mode 100644 src/main/resources/items/exampledoor.png create mode 100644 src/main/resources/items/examplewall.png create mode 100644 src/main/resources/objects/examplewall.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 5b0f488..88cbeea 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -9,9 +9,7 @@ import examplemod.examples.items.tools.ExampleSwordItem; import examplemod.examples.mobs.ExampleMob; import examplemod.examples.mobs.ExampleBossMob; -import examplemod.examples.objects.ExampleObject; -import examplemod.examples.objects.ExampleBaseRockObject; -import examplemod.examples.objects.ExampleOreRockObject; +import examplemod.examples.objects.*; import examplemod.examples.ExampleRecipes; import examplemod.examples.packets.ExamplePacket; import examplemod.examples.packets.ExamplePlaySoundPacket; @@ -49,13 +47,16 @@ public void init() { // Register our objects ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); - // Register a rock for the example incursion to use as cave walls + // Register a rock object ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); - // Register an ore rock that overlays onto our incursion rock + // Register an ore rock object that overlays onto our incursion rock ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + // Register a wall object, window object and door object + ExampleWallWindowDoorObject.registerWallsDoorsWindows(); + // Register our items ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); ItemRegistry.registerItem("examplestone", new ExampleStoneItem(),15,true); diff --git a/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java new file mode 100644 index 0000000..2dea232 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java @@ -0,0 +1,38 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.WallObject; +import necesse.inventory.item.toolItem.ToolType; + +import java.awt.Color; + + + +public final class ExampleWallWindowDoorObject { + private ExampleWallWindowDoorObject() {} + + public static int EXAMPLEWALL; + public static int EXAMPLEDOOR; + public static int EXAMPLEDOOROPEN; + public static int EXAMPLEWINDOW; + + public static void registerWallsDoorsWindows() { + // registers wall, door pair and window objecs + int[] ids = WallObject.registerWallObjects( + "example",// prefix + "examplewall", // texture name + "walloutlines", // outline texture + 0.0F, // tool tier + new Color(255, 220, 80), + ToolType.PICKAXE, + -1.0F, // wall broker value + -1.0F, // door broker value + true, // obtainable + true // count in stats + ); + + EXAMPLEWALL = ids[0]; + EXAMPLEDOOR = ids[1]; + EXAMPLEDOOROPEN = ids[2]; + EXAMPLEWINDOW = ids[3]; + } +} \ No newline at end of file diff --git a/src/main/resources/items/exampledoor.png b/src/main/resources/items/exampledoor.png new file mode 100644 index 0000000000000000000000000000000000000000..2a16f30df1f0929219d43df5ef6220b040072b6c GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVw7M|G6-6`i32Ef%hSa% zB%<~0rH#FZ97J3%9?mLeQhL$QBbU*nC9qUsm4cf3GF}!#Movu^`v~RR#@Y-2Ui#&x z{QB(WJ=(?wAMQ&|WivNBKkL!j+`#YQzAx2(yyHIdX{oA&bosOQ?+R-!=MW9sB`~zxaj&d2QF?jo_^n<_~%((AD>n4wobf%;Fi>zHy`%xx#A)H zlXuRX4|z&I-hNWp-}vB7%#_uC)jvs9R5YyAw6G`NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVw7M|`7Ec$) zkcif|(+v5V90XkFA7j!_aDB+DvTa`HK(DV~0AUtjEHBMSMnM;#@>g3kAW+Nb)l}=!4E|t6G(kLo~GCe(|PT2&R zrIs~h8HyVsmY`0Xp<|>83Iv!~C`gzJCZzsc>%Zr@+d21r&-cCe{mv;qbO2#uY+(!l zfC(}%APfK?ar$S%H*57*w=f!3e}nuUhS&$;v;SMDe_5U6AL0)H&6n4GKDI{x{^N;1sv)v4ui;u_r>1CUY zJ$(!C02@I%V=-;MgXFb-{epFDK*0F@&-E#e*$xbA>zPpBlA;%6@1?9Npj6g4wF)4R zJtvow4O-LRX^`Vx<7kJpm09B{p-I++5Se};J`ND>$JfcurV^5{=g0|gWmU$Hs&TqD>OjqpJ4vqdKn)_?OF#+ z|3!fOh|KXBjW!#t9k9UMtGtgXi+@IW0c+e;Moaa z{6Rs&Y1ZdUp37RUSrVebAc`vqN&RmGwxLN3QvigH@m8U0kj<`qjU6X?gDdbwC^@z} z#z0LeRP!b;34Rip0+?Sxwr6TW_%MwtwH`2NM}z3F=yhX*q#Z7!z|>hVVM@|tu4c_; zxfhh%P^yVT9S8V<-N0YtFazGX*SpD z4u|!|kEL0pRb`;9f=yKIIHeXBE6;zF4MGQR5^z2IYrJhxb>5L*&A5gO)9*P5`+CFt zPutm(iumD|9Wz$y-AgXIYTqf2Z|}=f z4-UZUMr7PuT~h(R7NAc*ALyp}8X>64dvj)obgx_=wzefDz4;d!+y00}9(Wp0YAUaC zy;E4$h*Hj#dP!NFt@-@Y@bv!>@^ zpD2+o{P^#2PIbr;nrCjiAeZNik4s!-93ksmVB}1&wby zReW$rAm5KbqIr}~CJ!@pv5t>#rIQ`e?20T6($}Y$c_y|=mM|=%ZUPhEIJ^p4srx@6f78KbB z+d~tK>q0tF^eACCrLj^x>zlgkQ8fHryzPwG;a~@Ta?>|pU{x4JRR7;bcifB^p7M%# zn$Rg4>EBb<5cB&Xvs_+&xy+_MRz)OmDbme3?>3)OMgIX-6`M;f`k8WB>ko}#Sz0I? zt83jiPZkW=>*$ThdrD2Idy}lE&gZQ%iJ_vZgmrEo%b<*0vD!Pn|JBfJs1DmCTc({! ze^h=2zhnmDO(akT0*5KlWch?WIIC@!Tq2TNncnj zdt;wfp=rxdro(q%mfcK6dt`>64oTw563yy-8U&lx&nX6dV}$0|IX8ABf4H2r?i5Mn zNX|*^k>Da@4KYkb&5fI9So9q@{uh!PXp?>+r(g_+tXHSwmHJd}dsJ-G23%~T_2gf5 zW4lgrg5t>98MN&?@$4b3=kyYEYSRdvx@UcH!`?(cE3oj0?#9*@=24#Mn4tpn;qCvF z&T}!WMdr?6L=XB`5<(s9zQ!~xNa=TaJ3NuP8e64(_b9qwn7wCi%1q7j*W|3{Ny4!G$ zGJ|^2O34V0jnZVrho7jd$zh&RaB#{nlU<6Eli5~zlhTPYqpwcBC1QAaM$CU`#Jg%5 z9?oc!S-xW09xZs8-l8fS8IHiXwVvb`nuFn;Zck07GHtw5bv!q= zxBG&ik+xxsvNm3PZ=gU9Q z;0}3dcl&pDSXhYP-;_-R)#n^#+T&b{dK-)b!A;=FbIAM>ld;GopH(gT z%y;^=_I4VqtlZ{Cc|7}>+5w|blU2eCLM_{3Gq=2wD~qXkh3)?^`yrZ+RcGaeV>+q| z%kXR5wJI*@Yekqxne@NhF`L+wQ1}?u0*B>sf_b9R$e7bdeV}yp9;_;BPLRM%S1B@i jlHAEn`&1MG_ Date: Tue, 27 Jan 2026 23:05:53 +0000 Subject: [PATCH 11/28] Refactor mod initialization into loader classes Split ExampleMod initialization logic into dedicated loader classes for buffs, commands, incursions, items, mobs, objects, packets, projectiles, resources, and tiles. This improves code organization and readability. Also, moved ExampleRecipes and removed the unused MissingTextureReporter utility. --- src/main/java/examplemod/ExampleMod.java | 101 +++--------------- .../examplemod/Loaders/ExampleModBuffs.java | 11 ++ .../Loaders/ExampleModCommands.java | 12 +++ .../Loaders/ExampleModIncursions.java | 23 ++++ .../examplemod/Loaders/ExampleModItems.java | 26 +++++ .../examplemod/Loaders/ExampleModMobs.java | 15 +++ .../examplemod/Loaders/ExampleModObjects.java | 25 +++++ .../examplemod/Loaders/ExampleModPackets.java | 15 +++ .../Loaders/ExampleModProjectiles.java | 11 ++ .../Loaders/ExampleModResources.java | 31 ++++++ .../examplemod/Loaders/ExampleModTiles.java | 12 +++ .../{examples => Loaders}/ExampleRecipes.java | 2 +- .../examples/util/MissingTextureReporter.java | 98 ----------------- 13 files changed, 197 insertions(+), 185 deletions(-) create mode 100644 src/main/java/examplemod/Loaders/ExampleModBuffs.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModCommands.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModIncursions.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModItems.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModMobs.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModObjects.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModPackets.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModProjectiles.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModResources.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModTiles.java rename src/main/java/examplemod/{examples => Loaders}/ExampleRecipes.java (99%) delete mode 100644 src/main/java/examplemod/examples/util/MissingTextureReporter.java diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 88cbeea..5b4bc41 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,24 +1,13 @@ package examplemod; -import examplemod.examples.*; +import examplemod.Loaders.*; +import examplemod.examples.ExampleChatCommand; +import examplemod.Loaders.ExampleRecipes; import examplemod.examples.incursion.ExampleBiome; -import examplemod.examples.incursion.ExampleIncursionBiome; -import examplemod.examples.incursion.ExampleIncursionLevel; -import examplemod.examples.items.*; -import examplemod.examples.items.tools.ExampleProjectileWeapon; -import examplemod.examples.items.tools.ExampleSwordItem; -import examplemod.examples.mobs.ExampleMob; -import examplemod.examples.mobs.ExampleBossMob; -import examplemod.examples.objects.*; -import examplemod.examples.ExampleRecipes; -import examplemod.examples.packets.ExamplePacket; -import examplemod.examples.packets.ExamplePlaySoundPacket; import necesse.engine.commands.CommandsManager; import necesse.engine.modLoader.annotations.ModEntry; -import necesse.engine.registries.*; import necesse.engine.sound.SoundSettings; import necesse.engine.sound.gameSound.GameSound; -import necesse.gfx.gameTexture.GameTexture; import necesse.level.maps.biomes.Biome; @ModEntry @@ -32,91 +21,31 @@ public class ExampleMod { public void init() { System.out.println("Hello world from my example mod!"); - // Register a simple biome that will not appear in natural world gen. - EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); - - // Register the incursion biome with tier requirement 1. - IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); - - // Register the level class used for the incursion. - LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); - - // Register our tiles - TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); - - // Register our objects - ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); - - // Register a rock object - ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); - ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); - - // Register an ore rock object that overlays onto our incursion rock - ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); - - // Register a wall object, window object and door object - ExampleWallWindowDoorObject.registerWallsDoorsWindows(); - - // Register our items - ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); - ItemRegistry.registerItem("examplestone", new ExampleStoneItem(),15,true); - ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); - ItemRegistry.registerItem("examplebar", new ExampleBarItem(),50,true); - ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); - ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); - ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); - ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); - ItemRegistry.registerItem("examplefood", new ExampleFoodItem(),15, true); - - // Register our mob - MobRegistry.registerMob("examplemob", ExampleMob.class, true); - - // Register boss mob - MobRegistry.registerMob("examplebossmob",ExampleBossMob.class,true,true); - - // Register our projectile - ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); - - // Register our buff - BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); - - // Register our packets - PacketRegistry.registerPacket(ExamplePacket.class); - - PacketRegistry.registerPacket(ExamplePlaySoundPacket.class); + // The examples are split into different classes here for readability, but you can register them directly here in init if you wish + ExampleModIncursions.load(); + ExampleModTiles.load(); + ExampleModObjects.load(); + ExampleModItems.load(); + ExampleModMobs.load(); + ExampleModProjectiles.load(); + ExampleModBuffs.load(); + ExampleModPackets.load(); } public void initResources() { - // Sometimes your textures will have a black or other outline unintended under rotation or scaling - // This is caused by alpha blending between transparent pixels and the edge - // To fix this, run the preAntialiasTextures gradle task - // It will process your textures and save them again with a fixed alpha edge color + ExampleModResources.load(); - ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); - ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); - - //initialising the sound to be used by our boss mob - EXAMPLESOUND = GameSound.fromFile("examplesound"); - - // Optional settings (volume/pitch/falloff) – used when playing via SoundSettings - EXAMPLESOUNDSETTINGS = new SoundSettings(EXAMPLESOUND) - .volume(0.8f) - .basePitch(1.0f) - .pitchVariance(0.08f) - .fallOffDistance(900); } public void postInit() { - // load our recipes from the ExampleRecipes class + // load our recipes from the ExampleRecipes class so we can keep this class easy to read ExampleRecipes.registerRecipes(); + // Add our example mob to default cave mobs. // Spawn tables use a ticket/weight system. In general, common mobs have about 100 tickets. Biome.defaultCaveMobs .add(100, "examplemob"); - - // Register our server chat command - CommandsManager.registerServerCommand(new ExampleChatCommand()); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java new file mode 100644 index 0000000..fce8b12 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -0,0 +1,11 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleBuff; +import necesse.engine.registries.BuffRegistry; + +public class ExampleModBuffs { + public static void load(){ + // Register our buff + BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCommands.java b/src/main/java/examplemod/Loaders/ExampleModCommands.java new file mode 100644 index 0000000..e60f03b --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCommands.java @@ -0,0 +1,12 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleChatCommand; +import necesse.engine.commands.CommandsManager; + +public class ExampleModCommands { + public static void load(){ + + // Register our server chat command + CommandsManager.registerServerCommand(new ExampleChatCommand()); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModIncursions.java b/src/main/java/examplemod/Loaders/ExampleModIncursions.java new file mode 100644 index 0000000..e072748 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModIncursions.java @@ -0,0 +1,23 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.incursion.ExampleBiome; +import examplemod.examples.incursion.ExampleIncursionBiome; +import examplemod.examples.incursion.ExampleIncursionLevel; +import necesse.engine.registries.BiomeRegistry; +import necesse.engine.registries.IncursionBiomeRegistry; +import necesse.engine.registries.LevelRegistry; + +public class ExampleModIncursions { + public static void load() { + + // Register a simple biome that will not appear in natural world gen. + ExampleMod.EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); + + // Register the incursion biome with tier requirement 1. + IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); + + // Register the level class used for the incursion. + LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java new file mode 100644 index 0000000..c7a4c28 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -0,0 +1,26 @@ +package examplemod.Loaders; + +import examplemod.examples.items.*; +import examplemod.examples.items.tools.ExampleProjectileWeapon; +import examplemod.examples.items.tools.ExampleSwordItem; +import necesse.engine.registries.ItemRegistry; + +public class ExampleModItems { + public static void load(){ + + // Materials + ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); + ItemRegistry.registerItem("examplestone", new ExampleStoneItem(), 15, true); + ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); + ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); + + // Weapons / tools + ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); + ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); + + // Consumables + ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); + ItemRegistry.registerItem("examplefood", new ExampleFoodItem(), 15, true); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java new file mode 100644 index 0000000..f8196f4 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleMob; +import necesse.engine.registries.MobRegistry; + +public class ExampleModMobs { + public static void load(){ + // Register our mob + MobRegistry.registerMob("examplemob", ExampleMob.class, true); + + // Register boss mob + MobRegistry.registerMob("examplebossmob", ExampleBossMob.class,true,true); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java new file mode 100644 index 0000000..25670a8 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -0,0 +1,25 @@ +package examplemod.Loaders; + +import examplemod.examples.objects.ExampleBaseRockObject; +import examplemod.examples.objects.ExampleObject; +import examplemod.examples.objects.ExampleOreRockObject; +import examplemod.examples.objects.ExampleWallWindowDoorObject; +import necesse.engine.registries.ObjectRegistry; + +public class ExampleModObjects { + + public static void load(){ + // Register our objects + ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + + // Register a rock object + ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); + ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); + + // Register an ore rock object that overlays onto our incursion rock + ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + + // Register a wall object, window object and door object + ExampleWallWindowDoorObject.registerWallsDoorsWindows(); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModPackets.java b/src/main/java/examplemod/Loaders/ExampleModPackets.java new file mode 100644 index 0000000..8f09fd9 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModPackets.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.examples.packets.ExamplePacket; +import examplemod.examples.packets.ExamplePlaySoundPacket; +import necesse.engine.registries.PacketRegistry; + +public class ExampleModPackets { + + public static void load() { + // Register our packets + PacketRegistry.registerPacket(ExamplePacket.class); + + PacketRegistry.registerPacket(ExamplePlaySoundPacket.class); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModProjectiles.java b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java new file mode 100644 index 0000000..c910ab9 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java @@ -0,0 +1,11 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleProjectile; +import necesse.engine.registries.ProjectileRegistry; + +public class ExampleModProjectiles { + public static void load(){ + // Register our projectile + ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModResources.java b/src/main/java/examplemod/Loaders/ExampleModResources.java new file mode 100644 index 0000000..918470d --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -0,0 +1,31 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleMob; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; +import necesse.gfx.gameTexture.GameTexture; + +public class ExampleModResources { + public static void load(){ + // Sometimes your textures will have a black or other outline unintended under rotation or scaling + // This is caused by alpha blending between transparent pixels and the edge + // To fix this, run the preAntialiasTextures gradle task + // It will process your textures and save them again with a fixed alpha edge color + + ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); + ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); + + //initialising the sound to be used by our boss mob + ExampleMod.EXAMPLESOUND = GameSound.fromFile("examplesound"); + + // Optional settings (volume/pitch/falloff) – used when playing via SoundSettings + ExampleMod.EXAMPLESOUNDSETTINGS = new SoundSettings(ExampleMod.EXAMPLESOUND) + .volume(0.8f) + .basePitch(1.0f) + .pitchVariance(0.08f) + .fallOffDistance(900); + } +} + diff --git a/src/main/java/examplemod/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java new file mode 100644 index 0000000..4b5a9d8 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -0,0 +1,12 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleTile; +import necesse.engine.registries.TileRegistry; + +public class ExampleModTiles { + + public static void load(){ + // Register our tiles + TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + } +} diff --git a/src/main/java/examplemod/examples/ExampleRecipes.java b/src/main/java/examplemod/Loaders/ExampleRecipes.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleRecipes.java rename to src/main/java/examplemod/Loaders/ExampleRecipes.java index 058ea11..59ead0a 100644 --- a/src/main/java/examplemod/examples/ExampleRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleRecipes.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.Loaders; import necesse.engine.registries.RecipeTechRegistry; import necesse.inventory.recipe.Ingredient; diff --git a/src/main/java/examplemod/examples/util/MissingTextureReporter.java b/src/main/java/examplemod/examples/util/MissingTextureReporter.java deleted file mode 100644 index 11f4f58..0000000 --- a/src/main/java/examplemod/examples/util/MissingTextureReporter.java +++ /dev/null @@ -1,98 +0,0 @@ -package examplemod.examples.util; - -import java.util.HashSet; - -import necesse.engine.GameLog; -import necesse.engine.util.GameUtils; -import necesse.engine.window.GameWindow; -import necesse.engine.window.WindowManager; -import necesse.gfx.GameResources; -import necesse.gfx.Renderer; -import necesse.gfx.gameFont.FontOptions; -import necesse.engine.screenHudManager.ScreenFloatTextFade; -import necesse.gfx.gameTexture.GameTexture; - -public class MissingTextureReporter { - - private static final HashSet seen = new HashSet<>(); - private static GameTexture fallback; - - public static GameTexture reportMissingTexture(String filePath, boolean outsideGame, boolean forceNotFinalize, Throwable thrown) { - // Normalize extension (matches vanilla) - String normalized = filePath == null ? "null" : GameUtils.formatFileExtension(filePath, "png"); - - // Find “who asked for this texture” (best effort) - String caller = findUsefulCaller(); - - // Log once per texture path (avoid spam) - if (seen.add(normalized)) { - GameLog.warn.println("[MissingTexture] Could not load: " + normalized - + " outsideGame=" + outsideGame - + " forceNotFinalize=" + forceNotFinalize - + (caller != null ? " caller=" + caller : "") - + " (" + thrown.getClass().getSimpleName() + ": " + thrown.getMessage() + ")"); - - // On-screen notice (client HUD) - showOnScreen("Missing texture: " + normalized); - } - - // Return a safe texture so the game keeps running - if (GameResources.error != null) return GameResources.error; - return getOrCreateFallback(); - } - - private static GameTexture getOrCreateFallback() { - if (fallback != null) return fallback; - - // 1x1 magenta fallback - GameTexture t = new GameTexture("missing-texture-fallback", 1, 1); - t.setPixel(0, 0, 255, 0, 255, 255); - t.makeFinal(); - fallback = t; - return fallback; - } - - private static void showOnScreen(String msg) { - try { - GameWindow window = WindowManager.getWindow(); - if (window == null) return; - - int x = Math.max(1, window.getHudWidth() / 2); - int y = 40; - - FontOptions opt = new FontOptions(16) - .color(255, 80, 80) - .outline(0, 0, 0); - - ScreenFloatTextFade text = new ScreenFloatTextFade(x, y, msg, opt); - text.avoidOtherText = true; - text.riseTime = 600; - text.hoverTime = 400; - text.fadeTime = 2200; - - Renderer.hudManager.addElement(text); - } catch (Throwable ignored) { - // Never crash from the reporter itself - } - } - - private static String findUsefulCaller() { - try { - StackTraceElement[] st = new Exception().getStackTrace(); - for (StackTraceElement e : st) { - String c = e.getClassName(); - if (c == null) continue; - - // Skip obvious internal frames - if (c.startsWith("net.bytebuddy.")) continue; - if (c.startsWith("examplemod.examples.patches.")) continue; - if (c.startsWith("examplemod.examples.util.MissingTextureReporter")) continue; - if (c.startsWith("necesse.gfx.gameTexture.GameTexture")) continue; - - return c + "#" + e.getMethodName() + ":" + e.getLineNumber(); - } - } catch (Throwable ignored) { - } - return null; - } -} From ebde9ad5239418d5c1544352a9de7a9430b6874b Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Thu, 29 Jan 2026 05:05:42 +0000 Subject: [PATCH 12/28] Add example armor set, categories, and refactor items Introduces example helmet, chestplate, and boots armor items with set bonus buff, including textures and registration. Adds ExampleModCategories for item and crafting category trees, updates item and recipe loaders, and reorganizes example items and buffs into subpackages for better structure. Updates localization for new items, buffs, and categories. --- src/main/java/examplemod/ExampleMod.java | 6 +- .../examplemod/Loaders/ExampleModBuffs.java | 3 +- .../Loaders/ExampleModCategories.java | 113 ++++++++++++++++++ .../examplemod/Loaders/ExampleModItems.java | 15 ++- ...pleRecipes.java => ExampleModRecipes.java} | 68 ++++++++++- .../buffs/ExampleArmorSetBonusBuff.java | 24 ++++ .../examples/{ => buffs}/ExampleBuff.java | 2 +- .../items/armor/ExampleBootsArmorItem.java | 16 +++ .../items/armor/ExampleChestArmorItem.java | 17 +++ .../items/armor/ExampleHelmetArmorItem.java | 24 ++++ .../{ => consumable}/ExampleFoodItem.java | 2 +- .../{ => consumable}/ExamplePotionItem.java | 2 +- .../items/{ => materials}/ExampleBarItem.java | 2 +- .../ExampleHuntIncursionMaterialItem.java | 2 +- .../{ => materials}/ExampleMaterialItem.java | 2 +- .../items/{ => materials}/ExampleOreItem.java | 2 +- .../{ => materials}/ExampleStoneItem.java | 2 +- .../resources/buffs/examplearmorsetbonus.png | Bin 0 -> 398 bytes src/main/resources/items/exampleboots.png | Bin 0 -> 402 bytes .../resources/items/examplechestplate.png | Bin 0 -> 558 bytes src/main/resources/items/examplehelmet.png | Bin 0 -> 485 bytes src/main/resources/locale/en.lang | 22 +++- .../player/armor/examplearms_left.png | Bin 0 -> 1491 bytes .../player/armor/examplearms_right.png | Bin 0 -> 1513 bytes .../resources/player/armor/exampleboots.png | Bin 0 -> 2600 bytes .../resources/player/armor/examplechest.png | Bin 0 -> 2152 bytes .../resources/player/armor/examplehelmet.png | Bin 0 -> 2833 bytes 27 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 src/main/java/examplemod/Loaders/ExampleModCategories.java rename src/main/java/examplemod/Loaders/{ExampleRecipes.java => ExampleModRecipes.java} (60%) create mode 100644 src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java rename src/main/java/examplemod/examples/{ => buffs}/ExampleBuff.java (95%) create mode 100644 src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java create mode 100644 src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java create mode 100644 src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java rename src/main/java/examplemod/examples/items/{ => consumable}/ExampleFoodItem.java (95%) rename src/main/java/examplemod/examples/items/{ => consumable}/ExamplePotionItem.java (85%) rename src/main/java/examplemod/examples/items/{ => materials}/ExampleBarItem.java (82%) rename src/main/java/examplemod/examples/items/{ => materials}/ExampleHuntIncursionMaterialItem.java (81%) rename src/main/java/examplemod/examples/items/{ => materials}/ExampleMaterialItem.java (80%) rename src/main/java/examplemod/examples/items/{ => materials}/ExampleOreItem.java (82%) rename src/main/java/examplemod/examples/items/{ => materials}/ExampleStoneItem.java (79%) create mode 100644 src/main/resources/buffs/examplearmorsetbonus.png create mode 100644 src/main/resources/items/exampleboots.png create mode 100644 src/main/resources/items/examplechestplate.png create mode 100644 src/main/resources/items/examplehelmet.png create mode 100644 src/main/resources/player/armor/examplearms_left.png create mode 100644 src/main/resources/player/armor/examplearms_right.png create mode 100644 src/main/resources/player/armor/exampleboots.png create mode 100644 src/main/resources/player/armor/examplechest.png create mode 100644 src/main/resources/player/armor/examplehelmet.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 5b4bc41..356c4d6 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,10 +1,7 @@ package examplemod; import examplemod.Loaders.*; -import examplemod.examples.ExampleChatCommand; -import examplemod.Loaders.ExampleRecipes; import examplemod.examples.incursion.ExampleBiome; -import necesse.engine.commands.CommandsManager; import necesse.engine.modLoader.annotations.ModEntry; import necesse.engine.sound.SoundSettings; import necesse.engine.sound.gameSound.GameSound; @@ -22,6 +19,7 @@ public void init() { System.out.println("Hello world from my example mod!"); // The examples are split into different classes here for readability, but you can register them directly here in init if you wish + ExampleModCategories.load(); ExampleModIncursions.load(); ExampleModTiles.load(); ExampleModObjects.load(); @@ -39,7 +37,7 @@ public void initResources() { public void postInit() { // load our recipes from the ExampleRecipes class so we can keep this class easy to read - ExampleRecipes.registerRecipes(); + ExampleModRecipes.registerRecipes(); // Add our example mob to default cave mobs. diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java index fce8b12..45d4f3b 100644 --- a/src/main/java/examplemod/Loaders/ExampleModBuffs.java +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -1,11 +1,12 @@ package examplemod.Loaders; -import examplemod.examples.ExampleBuff; +import examplemod.examples.buffs.*; import necesse.engine.registries.BuffRegistry; public class ExampleModBuffs { public static void load(){ // Register our buff BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBonusBuff()); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java new file mode 100644 index 0000000..d6e3bef --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -0,0 +1,113 @@ +package examplemod.Loaders; + +import necesse.engine.localization.message.LocalMessage; +import necesse.inventory.item.ItemCategory; + +public final class ExampleModCategories { + private ExampleModCategories() {} + + /* + * Necesse has TWO category trees: + * + * 1) Item categories (ItemCategory master tree) + * * Used for Creative inventory browsing / item lists / general item grouping. + * * This is what setItemCategory(...) uses on items/objects. + * + * 2) Crafting categories (ItemCategory.craftingManager tree) + * * Used by crafting station recipe lists and their filter/grouping UI. + * * This is what setCraftingCategory(...) uses on items/objects (and also what Recipe.setCraftingCategory uses). + * + * Important rule: + * If you want to use a category path, you MUST create that exact path in the corresponding manager + * BEFORE any items/objects/recipes try to reference it. + * + * + * ------------------------------------------------------------------------- + * Ways to APPLY categories: + * + * A) Apply on the object/item itself (recommended baseline) + * Object registration (GameObject): + * new ExampleObject() + * .setItemCategory(ROOT, PLACEABLES, DECOR) // Creative / item browsing + * .setCraftingCategory(ROOT, PLACEABLES, DECOR); // Crafting menu default + * + * Item registration (Item): + * new ExampleItem() + * .setItemCategory(ROOT, MATERIALS) + * .setCraftingCategory(ROOT, MATERIALS); + * + * What this affects: + * Creative inventory category placement (itemCategoryTree) + * Default crafting category used by stations/recipes if not overridden (craftingCategoryTree) + * + * B) Apply on the recipe (vanilla does this often) + * Recipe override: + * new Recipe(...) + * .setCraftingCategory(ROOT, PLACEABLES, DECOR); + * + * What this affects: + * Where the RECIPE appears in the crafting UI. + * Useful when a station collapses category depth (e.g. Workstation depth=1). + * You can force “Placeables/Decor” grouping even when the station normally trims deeper paths. + * + * Rule of thumb: + * Put the “true” category on the item/object (so creative inventory and general lists are correct). + * Use Recipe.setCraftingCategory(...) when you want a specific crafting UI grouping. + */ + + // Category paths (so you can reuse them consistently) + public static final String ROOT = "examplemod"; + + public static final String INCURSION = "incursion"; + public static final String MATERIALS = "materials"; + public static final String CONSUMABLES = "consumables"; + public static final String TOOLS = "tools"; + public static final String WEAPONS = "weapons"; + public static final String ARMOR = "armor"; + public static final String PLACEABLES = "placeables"; + public static final String FURNITURE = "furniture"; + public static final String LIGHTING = "lighting"; + public static final String DECOR = "decor"; + + public static void load() { + + // ===== Normal item category tree (Creative inventory / item browsing) ===== + // "Z-..." usually pushes it toward the bottom; change if you want it earlier. + // The category "path" is built from these strings, e.g.: + // ROOT -> PLACEABLES -> DECOR + // becomes: + // examplemod.placeables.decor + ItemCategory.createCategory("Z-EX-0", new LocalMessage("itemcategory", "examplemod"), ROOT); + + ItemCategory.createCategory("Z-EX-1", new LocalMessage("itemcategory", "examplemod_materials"), ROOT, MATERIALS); + + ItemCategory.createCategory("Z-EX-2", new LocalMessage("itemcategory", "examplemod_consumables"), ROOT, CONSUMABLES); + ItemCategory.createCategory("Z-EX-3", new LocalMessage("itemcategory", "examplemod_tools"), ROOT, TOOLS); + ItemCategory.createCategory("Z-EX-4", new LocalMessage("itemcategory", "examplemod_weapons"), ROOT, WEAPONS); + ItemCategory.createCategory("Z-EX-5", new LocalMessage("itemcategory", "examplemod_armor"), ROOT, ARMOR); + ItemCategory.createCategory("Z-EX-6", new LocalMessage("itemcategory", "examplemod_incursion"), ROOT, INCURSION); + ItemCategory.createCategory("Z-EX-7", new LocalMessage("itemcategory", "examplemod_placeables"), ROOT, PLACEABLES); + + // Subcategories under Placeables (Creative) + ItemCategory.createCategory("Z-EX-7A", new LocalMessage("itemcategory", "examplemod_furniture"), ROOT, PLACEABLES, FURNITURE); + ItemCategory.createCategory("Z-EX-7B", new LocalMessage("itemcategory", "examplemod_lighting"), ROOT, PLACEABLES, LIGHTING); + ItemCategory.createCategory("Z-EX-7C", new LocalMessage("itemcategory", "examplemod_decor"), ROOT, PLACEABLES, DECOR); + + // ===== Crafting filter tree (Crafting station recipe list/filter UI) ===== + // If you want the same category paths to work in crafting menus, + // you must mirror the same tree here. + ItemCategory.craftingManager.createCategory("Z-EX-0", new LocalMessage("itemcategory", "examplemod"), ROOT); + ItemCategory.craftingManager.createCategory("Z-EX-1", new LocalMessage("itemcategory", "examplemod_materials"), ROOT, MATERIALS); + ItemCategory.craftingManager.createCategory("Z-EX-2", new LocalMessage("itemcategory", "examplemod_consumables"), ROOT, CONSUMABLES); + ItemCategory.craftingManager.createCategory("Z-EX-3", new LocalMessage("itemcategory", "examplemod_tools"), ROOT, TOOLS); + ItemCategory.craftingManager.createCategory("Z-EX-4", new LocalMessage("itemcategory", "examplemod_weapons"), ROOT, WEAPONS); + ItemCategory.craftingManager.createCategory("Z-EX-5", new LocalMessage("itemcategory", "examplemod_armor"), ROOT, ARMOR); + ItemCategory.craftingManager.createCategory("Z-EX-6", new LocalMessage("itemcategory", "examplemod_incursion"), ROOT, INCURSION); + ItemCategory.craftingManager.createCategory("Z-EX-7", new LocalMessage("itemcategory", "examplemod_placeables"), ROOT, PLACEABLES); + + // Subcategories under Placeables (Crafting) + ItemCategory.craftingManager.createCategory("Z-EX-7A", new LocalMessage("itemcategory", "examplemod_furniture"), ROOT, PLACEABLES, FURNITURE); + ItemCategory.craftingManager.createCategory("Z-EX-7B", new LocalMessage("itemcategory", "examplemod_lighting"), ROOT, PLACEABLES, LIGHTING); + ItemCategory.craftingManager.createCategory("Z-EX-7C", new LocalMessage("itemcategory", "examplemod_decor"), ROOT, PLACEABLES, DECOR); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index c7a4c28..abc11d6 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -1,9 +1,15 @@ package examplemod.Loaders; -import examplemod.examples.items.*; +import examplemod.examples.items.armor.ExampleBootsArmorItem; +import examplemod.examples.items.armor.ExampleChestArmorItem; +import examplemod.examples.items.armor.ExampleHelmetArmorItem; +import examplemod.examples.items.consumable.ExampleFoodItem; +import examplemod.examples.items.consumable.ExamplePotionItem; +import examplemod.examples.items.materials.*; import examplemod.examples.items.tools.ExampleProjectileWeapon; import examplemod.examples.items.tools.ExampleSwordItem; import necesse.engine.registries.ItemRegistry; +import necesse.inventory.item.ItemCategory; public class ExampleModItems { public static void load(){ @@ -15,10 +21,15 @@ public static void load(){ ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); - // Weapons / tools + // Tools ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); + // Armor + ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200f, true); + ItemRegistry.registerItem("examplechestplate", new ExampleChestArmorItem(), 250f, true); + ItemRegistry.registerItem("exampleboots", new ExampleBootsArmorItem(), 180f, true); + // Consumables ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); ItemRegistry.registerItem("examplefood", new ExampleFoodItem(), 15, true); diff --git a/src/main/java/examplemod/Loaders/ExampleRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java similarity index 60% rename from src/main/java/examplemod/Loaders/ExampleRecipes.java rename to src/main/java/examplemod/Loaders/ExampleModRecipes.java index 59ead0a..fa53d5c 100644 --- a/src/main/java/examplemod/Loaders/ExampleRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -9,7 +9,7 @@ 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 ExampleRecipes { +public class ExampleModRecipes { //Put your recipe registrations in here public static void registerRecipes(){ @@ -46,16 +46,74 @@ public static void registerRecipes(){ } )); - //WORKSTATION RECIPES Recipes.registerModRecipe(new Recipe( "examplestaff", 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 5), + new Ingredient("examplebar", 4) + } + )); + + 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("exampleitem", 4), - new Ingredient("examplebar", 10) + new Ingredient("examplestone", 7) } - ).showAfter("exampleitem")); // Show the recipe after example item recipe + )); + + Recipes.registerModRecipe(new Recipe( + "exampledoor", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleobject", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); //COOKING POT RECIPES Recipes.registerModRecipe(new Recipe( diff --git a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java new file mode 100644 index 0000000..65df04f --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java @@ -0,0 +1,24 @@ +package examplemod.examples.buffs; + +import necesse.engine.modifiers.ModifierValue; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.BuffModifiers; +import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.setBonusBuffs.SimpleSetBonusBuff; + +public class ExampleArmorSetBonusBuff extends SimpleSetBonusBuff { + public ExampleArmorSetBonusBuff() { + super( + new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), + new ModifierValue<>(BuffModifiers.SPEED, 0.10f) + ); + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + this.canCancel = false; + this.isPassive = true; + this.isVisible = true; + super.init(buff, eventSubscriber); + } +} diff --git a/src/main/java/examplemod/examples/ExampleBuff.java b/src/main/java/examplemod/examples/buffs/ExampleBuff.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleBuff.java rename to src/main/java/examplemod/examples/buffs/ExampleBuff.java index 2dec99d..e2310f5 100644 --- a/src/main/java/examplemod/examples/ExampleBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleBuff.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.buffs; import necesse.entity.mobs.buffs.ActiveBuff; import necesse.entity.mobs.buffs.BuffEventSubscriber; diff --git a/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java new file mode 100644 index 0000000..1297dfc --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java @@ -0,0 +1,16 @@ +package examplemod.examples.items.armor; + +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.BootsArmorItem; +import necesse.inventory.lootTable.presets.FeetArmorLootTable; + +public class ExampleBootsArmorItem extends BootsArmorItem { + public ExampleBootsArmorItem() { + + super(2, //armorValue + 300, //enchantCost + Item.Rarity.UNCOMMON, //rarity + "exampleboots", //textureName + FeetArmorLootTable.feetArmor); //lootTableCategory + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java new file mode 100644 index 0000000..6831006 --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleChestArmorItem.java @@ -0,0 +1,17 @@ +package examplemod.examples.items.armor; + +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.ChestArmorItem; +import necesse.inventory.lootTable.presets.BodyArmorLootTable; + +public class ExampleChestArmorItem extends ChestArmorItem { + public ExampleChestArmorItem() { + super(4, //armorValue + 300, //enchantCost + Item.Rarity.UNCOMMON, //rarity + "examplechest", //bodyTextureName + "examplearms", //armsTextureName + BodyArmorLootTable.bodyArmor); //lootTableCategory + + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java new file mode 100644 index 0000000..965fe75 --- /dev/null +++ b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java @@ -0,0 +1,24 @@ +package examplemod.examples.items.armor; + +import necesse.engine.registries.DamageTypeRegistry; +import necesse.inventory.item.Item; +import necesse.inventory.item.armorItem.SetHelmetArmorItem; +import necesse.inventory.lootTable.presets.ArmorSetsLootTable; +import necesse.inventory.lootTable.presets.HeadArmorLootTable; + +public class ExampleHelmetArmorItem extends SetHelmetArmorItem { + public ExampleHelmetArmorItem() { + super( + 3, //armor value + DamageTypeRegistry.MELEE, //damage class for enchant scaling etc + 300, //enchant cost + HeadArmorLootTable.headArmor, //head armor loot category + ArmorSetsLootTable.armorSets, //armor SETS loot category + Item.Rarity.UNCOMMON, + "examplehelmet", //helmet texture name + "examplechestplate", //chest item STRING ID + "exampleboots", //boots item STRING ID + "examplearmorsetbonus" //buff STRING ID + ); + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleFoodItem.java b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java similarity index 95% rename from src/main/java/examplemod/examples/items/ExampleFoodItem.java rename to src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java index 1f5ce13..ce4279b 100644 --- a/src/main/java/examplemod/examples/items/ExampleFoodItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.consumable; import necesse.engine.modifiers.ModifierValue; import necesse.entity.mobs.buffs.BuffModifiers; diff --git a/src/main/java/examplemod/examples/items/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java similarity index 85% rename from src/main/java/examplemod/examples/items/ExamplePotionItem.java rename to src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java index 17bdcdc..a2293fa 100644 --- a/src/main/java/examplemod/examples/items/ExamplePotionItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.consumable; import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; diff --git a/src/main/java/examplemod/examples/items/ExampleBarItem.java b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java similarity index 82% rename from src/main/java/examplemod/examples/items/ExampleBarItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleBarItem.java index 0e5160f..7251215 100644 --- a/src/main/java/examplemod/examples/items/ExampleBarItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.Item; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java similarity index 81% rename from src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java index 8c15c37..f54d3fb 100644 --- a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java similarity index 80% rename from src/main/java/examplemod/examples/items/ExampleMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java index a20ddb6..12e9220 100644 --- a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/ExampleOreItem.java b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java similarity index 82% rename from src/main/java/examplemod/examples/items/ExampleOreItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleOreItem.java index 8e3ed98..6a04fcb 100644 --- a/src/main/java/examplemod/examples/items/ExampleOreItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.Item; import necesse.inventory.item.matItem.MatItem; diff --git a/src/main/java/examplemod/examples/items/ExampleStoneItem.java b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java similarity index 79% rename from src/main/java/examplemod/examples/items/ExampleStoneItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java index f217e3b..faeff08 100644 --- a/src/main/java/examplemod/examples/items/ExampleStoneItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.placeableItem.StonePlaceableItem; diff --git a/src/main/resources/buffs/examplearmorsetbonus.png b/src/main/resources/buffs/examplearmorsetbonus.png new file mode 100644 index 0000000000000000000000000000000000000000..b4edeee83d053b964ec4c2fadb52a57f5ddc91e9 GIT binary patch literal 398 zcmV;90df9`P)JJj;T|CTa%_R$nn8I{@T!5SEZOrg{i~%!HYbVkdI2K!YCa z^NMcj20%wCJOEJA9XJGN>2vg;L=OqDYw-E}$9KwNgM$-QKYB>)9s4iYj2P55)u;12;< zNWgpyj+%6AuR4;b;6*n4M$r<$RWT0YB0Wd zdX}cGVxS$2Ks&%m3DhD7H_2d4b6WL?kh3Jna+Dqthy%kgh0u-(C?u$ckvlucc4Fpz0B*gr7$M$;Y5)KL07*qoM6N<$f>61k5dZ)H literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampleboots.png b/src/main/resources/items/exampleboots.png new file mode 100644 index 0000000000000000000000000000000000000000..98ab72f23b0d4965bfb0c19f5b7283a6641b3c1e GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVw7cMVB^{OdkawLnx~6n zNJQ(~X?uAO83?$^cen}|D?|m9Em*KLNsvoy!J?%J?Ho}@csc?X?0T^IQFo`qwwCy0 z)$hvn^WRzoXo&oq+bFtbhW*{=v$|*AU!Es%`NQ-r=gx#b5DvPsTyH0{vUWuR|AAS( zRr+48fgY7B?y2UpZ+p|QebOvr26w-;KeeYZgy@9qb-l;OVd%xM?#>kn=QzF-dEc%F zU9$_j*^`-VyTFhC$)f|?r*PNp`%;`S$KzeYuSvcVvrm@GIed@(;5q*Z--=)bWA2Mz m?H?RD8+}oACv&UQLsm`}m%p-e%+COQ$l&Sf=d#Wzp$PyUWRed6 literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplechestplate.png b/src/main/resources/items/examplechestplate.png new file mode 100644 index 0000000000000000000000000000000000000000..19048bad6cbe6f97aed11a0153498555d716b371 GIT binary patch literal 558 zcmV+}0@3}6P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHSG0RTA7*ybMq00DAIL_t(oh3%J3N&`U39AS<&mUO^!njp7abxryK| z>+A%P+{2$Eh|x_zuVCEhDko6JO&&8Mq!0H(xaV z$WyJDYjrp9(`YO^QOr*{u9}*|@=C;0Aa%xKe$DN|fF*_I#_`I;Q9#Tuxd7mSApker z0r1QV3+#j=0QTu>oyn0n3J7AZ9gF#xH=tlR?A-$}>W_?H^V)2h zIEw4Bas_5;Rk@J4Au-xv8z__!ykANP`LrnFSHTu-0A3kWw#86qs&A*)0g7Dk@NsZA zWgGv*f91xAI6?A0jfKrl`D|!34IT!Gfuq)ha4EcY$|%hxC*51 w1)uF6Rdw$XR{_B*F~?7NT*~~K|AK#f1IJ8@{H!MhFaQ7m07*qoM6N<$g25}~NdN!< literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplehelmet.png b/src/main/resources/items/examplehelmet.png new file mode 100644 index 0000000000000000000000000000000000000000..820f9fc893bca82a238af5a21239501649a2ea2f GIT binary patch literal 485 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVw7cMRNSzlejd;Tnw~C> zArY-_ryJ%m849?@EA=&MOya(vddoS7U34eA`vW;o*17o;L^NLRUZ1Pj++a~MB z9v5>a?w|Wq@@&0a|22U%*6)v{&(kyQwEtQ3bl-Z6jZx2+?om0!`A6JfquHKMYzMft57cvuq-)M`SZ;jvukPEk3=?N;yPWfP&N+wu zpIw-CZC9MraFcoIlMSA}+xDgZU(Ixga|Y{@`Rl{(?I>VMkTJtBi0lPxy zT$O&TuE4I^IjQ4Gsa~tF!GHEsohvq9KXcus#$VpydgXnVDTg>i{107vXgD`-UA1W( zlhtO)&D;m(RE9ocZrAajf6UwA)~3nK;+&TIyQD)b7{A{Rh}tAkqg*xD`ytDrue`l0 W!VE<3W-0)~k-^i|&t;ucLK6Uz?zW%+ literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 7cac2ee..c824585 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -15,9 +15,13 @@ exampleore=Example Ore examplebar=Example Bar examplehuntincursionmaterial=Example Hunt Incursion Material examplepotion=Example Potion +examplefood=Example Food examplesword=Example Sword examplestaff=Example Staff -examplefood=Example Food +examplehelmet=Example Helmet +examplechestplate=Example Chestplate +exampleboots=Example Boots + [itemtooltip] examplestafftip=Shoots a homing, piercing projectile @@ -29,9 +33,23 @@ examplebossmob=Example Boss [buff] examplebuff=Example Buff +examplearmorsetbonus=Example Armor Set Bonus [biome] exampleincursion=Example Incursion [incursion] -exampleincursion=Example Incursion \ No newline at end of file +exampleincursion=Example Incursion + +[itemcategory] +examplemod=ExampleMod +examplemod_materials=Example Materials +examplemod_tools=Example Tools +examplemod_weapons=Example Weapons +examplemod_armor=Example Armor +examplemod_placeables=Example Placeables +examplemod_furniture=Example Furniture +examplemod_lighting=Example Lighting +examplemod_decor=Example Decor +examplemod_consumables=Example Consumables +examplemod_incursion=Example Incursion \ No newline at end of file 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 0000000000000000000000000000000000000000..dd90fff8ed32654182b7c6619a6febc155ae771d GIT binary patch literal 1491 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/player/armor/examplehelmet.png b/src/main/resources/player/armor/examplehelmet.png new file mode 100644 index 0000000000000000000000000000000000000000..687a019636f5c54c2b18f81ca1356666383610da GIT binary patch literal 2833 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 Date: Sun, 1 Feb 2026 03:28:31 +0000 Subject: [PATCH 13/28] Refactor and update mod category structure and objects Reworked ExampleModCategories to align with vanilla creative menu roots and subcategories, and updated item/crafting category registration for mod objects. Modified ExampleModObjects to assign new categories to registered objects and added a new object and recipe (exampleobject2) using the new mod-specific category. Updated localization keys to match new category names and structure. --- src/main/java/examplemod/ExampleMod.java | 1 - .../Loaders/ExampleModCategories.java | 183 +++++++++--------- .../examplemod/Loaders/ExampleModObjects.java | 10 +- .../examplemod/Loaders/ExampleModRecipes.java | 10 + src/main/resources/locale/en.lang | 14 +- 5 files changed, 111 insertions(+), 107 deletions(-) diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 356c4d6..53a5e32 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -32,7 +32,6 @@ public void init() { public void initResources() { ExampleModResources.load(); - } public void postInit() { diff --git a/src/main/java/examplemod/Loaders/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java index d6e3bef..66ec22d 100644 --- a/src/main/java/examplemod/Loaders/ExampleModCategories.java +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -7,107 +7,102 @@ public final class ExampleModCategories { private ExampleModCategories() {} /* - * Necesse has TWO category trees: + * IMPORTANT (Creative Menu requirement) + * ------------------------------------ + * The Creative menu tabs currently only browse a small set of hard-coded ROOT categories but this will be changed in the future. * - * 1) Item categories (ItemCategory master tree) - * * Used for Creative inventory browsing / item lists / general item grouping. - * * This is what setItemCategory(...) uses on items/objects. + * Placeables tab roots: tiles / objects / wiring + * Items tab roots: equipment / consumable / materials / misc + * Mobs tab roots: mobs * - * 2) Crafting categories (ItemCategory.craftingManager tree) - * * Used by crafting station recipe lists and their filter/grouping UI. - * * This is what setCraftingCategory(...) uses on items/objects (and also what Recipe.setCraftingCategory uses). - * - * Important rule: - * If you want to use a category path, you MUST create that exact path in the corresponding manager - * BEFORE any items/objects/recipes try to reference it. - * - * - * ------------------------------------------------------------------------- - * Ways to APPLY categories: - * - * A) Apply on the object/item itself (recommended baseline) - * Object registration (GameObject): - * new ExampleObject() - * .setItemCategory(ROOT, PLACEABLES, DECOR) // Creative / item browsing - * .setCraftingCategory(ROOT, PLACEABLES, DECOR); // Crafting menu default - * - * Item registration (Item): - * new ExampleItem() - * .setItemCategory(ROOT, MATERIALS) - * .setCraftingCategory(ROOT, MATERIALS); - * - * What this affects: - * Creative inventory category placement (itemCategoryTree) - * Default crafting category used by stations/recipes if not overridden (craftingCategoryTree) - * - * B) Apply on the recipe (vanilla does this often) - * Recipe override: - * new Recipe(...) - * .setCraftingCategory(ROOT, PLACEABLES, DECOR); - * - * What this affects: - * Where the RECIPE appears in the crafting UI. - * Useful when a station collapses category depth (e.g. Workstation depth=1). - * You can force “Placeables/Decor” grouping even when the station normally trims deeper paths. - * - * Rule of thumb: - * Put the “true” category on the item/object (so creative inventory and general lists are correct). - * Use Recipe.setCraftingCategory(...) when you want a specific crafting UI grouping. + * So: your itemCategoryTree MUST start with one of those roots, otherwise the item/object + * will not appear in Creative even though it is registered. */ - // Category paths (so you can reuse them consistently) - public static final String ROOT = "examplemod"; + // VANILLA PLACABLE + // ===== ROOT ===== + public static final String ROOT_TILES = "tiles"; + public static final String ROOT_OBJECTS = "objects"; + public static final String ROOT_WIRING = "wiring"; + + // ===== VANILLA: TILES children ===== + public static final String TILES_FLOORS = "floors"; + public static final String TILES_LIQUIDS = "liquids"; + public static final String TILES_TERRAIN = "terrain"; - public static final String INCURSION = "incursion"; - public static final String MATERIALS = "materials"; - public static final String CONSUMABLES = "consumables"; - public static final String TOOLS = "tools"; - public static final String WEAPONS = "weapons"; - public static final String ARMOR = "armor"; - public static final String PLACEABLES = "placeables"; - public static final String FURNITURE = "furniture"; - public static final String LIGHTING = "lighting"; - public static final String DECOR = "decor"; + // ===== VANILLA: OBJECTS children ===== + public static final String OBJECTS_SEEDS = "seeds"; + public static final String OBJECTS_CRAFTINGSTATIONS = "craftingstations"; + public static final String OBJECTS_LIGHTING = "lighting"; + public static final String OBJECTS_FURNITURE = "furniture"; + public static final String OBJECTS_DECORATIONS = "decorations"; + public static final String OBJECTS_WALLSANDDOORS = "wallsanddoors"; + public static final String OBJECTS_FENCESANDGATES = "fencesandgates"; + public static final String OBJECTS_COLUMNS = "columns"; + public static final String OBJECTS_TRAPS = "traps"; + public static final String OBJECTS_LANDSCAPING = "landscaping"; + public static final String OBJECTS_MISC = "misc"; + + // ===== VANILLA: FURNITURE children ===== + public static final String FURNITURE_MISC = "misc"; + public static final String FURNITURE_OAK = "oak"; + public static final String FURNITURE_SPRUCE = "spruce"; + public static final String FURNITURE_PINE = "pine"; + public static final String FURNITURE_MAPLE = "maple"; + public static final String FURNITURE_BIRCH = "birch"; + public static final String FURNITURE_WILLOW = "willow"; + public static final String FURNITURE_DUNGEON = "dungeon"; + public static final String FURNITURE_BONE = "bone"; + public static final String FURNITURE_DRYAD = "dryad"; + public static final String FURNITURE_BAMBOO = "bamboo"; + public static final String FURNITURE_DEADWOOD = "deadwood"; + + // ===== VANILLA: DECORATIONS children ===== + public static final String DECORATIONS_PAINTINGS = "paintings"; + public static final String DECORATIONS_CARPETS = "carpets"; + public static final String DECORATIONS_POTS = "pots"; + public static final String DECORATIONS_BANNERS = "banners"; + + // ===== VANILLA: LANDSCAPING children ===== + public static final String LANDSCAPING_FORESTROCKSANDORES = "forestrocksandores"; + public static final String LANDSCAPING_SNOWROCKSANDORES = "snowrocksandores"; + public static final String LANDSCAPING_PLAINSROCKSANDORES = "plainsrocksandores"; + public static final String LANDSCAPING_SWAMPROCKSANDORES = "swamprocksandores"; + public static final String LANDSCAPING_DESERTROCKSANDORES = "desertrocksandores"; + public static final String LANDSCAPING_INCURSIONROCKSANDORES = "incursionrocksandores"; + public static final String LANDSCAPING_CRYSTALS = "crystals"; + public static final String LANDSCAPING_TABLEDECORATIONS = "tabledecorations"; + public static final String LANDSCAPING_PLANTS = "plants"; + public static final String LANDSCAPING_MASONRY = "masonry"; + public static final String LANDSCAPING_MISC = "misc"; + + // ===== VANILLA: WIRING children ===== + public static final String WIRING_LOGICGATES = "logicgates"; + + + // YOUR MOD ROOT CATEGORY + public static final String MOD = "examplemod"; + // YOUR MOD SUB CATEGORY + public static final String MOD_OBJECTS = "objects"; public static void load() { - // ===== Normal item category tree (Creative inventory / item browsing) ===== - // "Z-..." usually pushes it toward the bottom; change if you want it earlier. - // The category "path" is built from these strings, e.g.: - // ROOT -> PLACEABLES -> DECOR - // becomes: - // examplemod.placeables.decor - ItemCategory.createCategory("Z-EX-0", new LocalMessage("itemcategory", "examplemod"), ROOT); - - ItemCategory.createCategory("Z-EX-1", new LocalMessage("itemcategory", "examplemod_materials"), ROOT, MATERIALS); - - ItemCategory.createCategory("Z-EX-2", new LocalMessage("itemcategory", "examplemod_consumables"), ROOT, CONSUMABLES); - ItemCategory.createCategory("Z-EX-3", new LocalMessage("itemcategory", "examplemod_tools"), ROOT, TOOLS); - ItemCategory.createCategory("Z-EX-4", new LocalMessage("itemcategory", "examplemod_weapons"), ROOT, WEAPONS); - ItemCategory.createCategory("Z-EX-5", new LocalMessage("itemcategory", "examplemod_armor"), ROOT, ARMOR); - ItemCategory.createCategory("Z-EX-6", new LocalMessage("itemcategory", "examplemod_incursion"), ROOT, INCURSION); - ItemCategory.createCategory("Z-EX-7", new LocalMessage("itemcategory", "examplemod_placeables"), ROOT, PLACEABLES); - - // Subcategories under Placeables (Creative) - ItemCategory.createCategory("Z-EX-7A", new LocalMessage("itemcategory", "examplemod_furniture"), ROOT, PLACEABLES, FURNITURE); - ItemCategory.createCategory("Z-EX-7B", new LocalMessage("itemcategory", "examplemod_lighting"), ROOT, PLACEABLES, LIGHTING); - ItemCategory.createCategory("Z-EX-7C", new LocalMessage("itemcategory", "examplemod_decor"), ROOT, PLACEABLES, DECOR); - - // ===== Crafting filter tree (Crafting station recipe list/filter UI) ===== - // If you want the same category paths to work in crafting menus, - // you must mirror the same tree here. - ItemCategory.craftingManager.createCategory("Z-EX-0", new LocalMessage("itemcategory", "examplemod"), ROOT); - ItemCategory.craftingManager.createCategory("Z-EX-1", new LocalMessage("itemcategory", "examplemod_materials"), ROOT, MATERIALS); - ItemCategory.craftingManager.createCategory("Z-EX-2", new LocalMessage("itemcategory", "examplemod_consumables"), ROOT, CONSUMABLES); - ItemCategory.craftingManager.createCategory("Z-EX-3", new LocalMessage("itemcategory", "examplemod_tools"), ROOT, TOOLS); - ItemCategory.craftingManager.createCategory("Z-EX-4", new LocalMessage("itemcategory", "examplemod_weapons"), ROOT, WEAPONS); - ItemCategory.craftingManager.createCategory("Z-EX-5", new LocalMessage("itemcategory", "examplemod_armor"), ROOT, ARMOR); - ItemCategory.craftingManager.createCategory("Z-EX-6", new LocalMessage("itemcategory", "examplemod_incursion"), ROOT, INCURSION); - ItemCategory.craftingManager.createCategory("Z-EX-7", new LocalMessage("itemcategory", "examplemod_placeables"), ROOT, PLACEABLES); - - // Subcategories under Placeables (Crafting) - ItemCategory.craftingManager.createCategory("Z-EX-7A", new LocalMessage("itemcategory", "examplemod_furniture"), ROOT, PLACEABLES, FURNITURE); - ItemCategory.craftingManager.createCategory("Z-EX-7B", new LocalMessage("itemcategory", "examplemod_lighting"), ROOT, PLACEABLES, LIGHTING); - ItemCategory.craftingManager.createCategory("Z-EX-7C", new LocalMessage("itemcategory", "examplemod_decor"), ROOT, PLACEABLES, DECOR); + // ITEM CATEGORIES (not Creative-visible right now, but valid categories) + ItemCategory.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectssubcat"), + MOD, MOD_OBJECTS); + + // CRAFTING CATEGORIES + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodobjectscat"), + MOD,MOD_OBJECTS); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 25670a8..3c87cb2 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -6,12 +6,20 @@ import examplemod.examples.objects.ExampleWallWindowDoorObject; import necesse.engine.registries.ObjectRegistry; +//NOTE item and crafting categories subject to change public class ExampleModObjects { public static void load(){ // Register our objects - ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + ObjectRegistry.registerObject("exampleobject", new ExampleObject() + .setItemCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS) + .setCraftingCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS), 2, true); + + //this wont currently display in creative due to how creative is coded but this is subject to change + ObjectRegistry.registerObject("exampleobject2", new ExampleObject() + .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS) + .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS), 2, true); // Register a rock object ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index fa53d5c..0d127b7 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -115,6 +115,16 @@ public static void registerRecipes(){ } )); + Recipes.registerModRecipe(new Recipe( + "exampleobject2", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); + //COOKING POT RECIPES Recipes.registerModRecipe(new Recipe( "examplefood", diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index c824585..e5f47a6 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -3,6 +3,7 @@ exampletile=Example Tile [object] exampleobject=Example Object +exampleobject2 = Example Object Mod Category examplebaserock=Example Rock exampleore=Example Ore examplewall=Example Wall @@ -42,14 +43,5 @@ exampleincursion=Example Incursion exampleincursion=Example Incursion [itemcategory] -examplemod=ExampleMod -examplemod_materials=Example Materials -examplemod_tools=Example Tools -examplemod_weapons=Example Weapons -examplemod_armor=Example Armor -examplemod_placeables=Example Placeables -examplemod_furniture=Example Furniture -examplemod_lighting=Example Lighting -examplemod_decor=Example Decor -examplemod_consumables=Example Consumables -examplemod_incursion=Example Incursion \ No newline at end of file +examplemodrootcat=ExampleMod +examplemodobjectsubcat=ExampleMod Objects \ No newline at end of file From 28cd55e7ce39655a418a0461dbf83fa17c788daf Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Mon, 2 Feb 2026 02:47:07 +0000 Subject: [PATCH 14/28] Add example tree, sapling, log, and chair objects Introduces new ExampleTreeObject, ExampleTreeSaplingObject, ExampleLogItem, and ExampleWoodChairObject classes, along with their registration in loaders and crafting recipes. Adds related item/object categories, updates localization, and includes new/updated resource images for these items and objects. Also renames ExampleSwordItem to ExampleSwordWeapon. --- .../Loaders/ExampleModCategories.java | 16 +++++++++-- .../examplemod/Loaders/ExampleModItems.java | 6 ++-- .../examplemod/Loaders/ExampleModObjects.java | 22 ++++++++------ .../examplemod/Loaders/ExampleModRecipes.java | 20 ++++++------- .../items/materials/ExampleLogItem.java | 11 +++++++ ...SwordItem.java => ExampleSwordWeapon.java} | 4 +-- .../examples/objects/ExampleTreeObject.java | 27 ++++++++++++++++++ .../objects/ExampleTreeSaplingObject.java | 12 ++++++++ .../objects/ExampleWoodChairObject.java | 11 +++++++ src/main/resources/items/examplebaserock.png | Bin 5617 -> 789 bytes src/main/resources/items/exampleboots.png | Bin 402 -> 402 bytes src/main/resources/items/examplechair.png | Bin 0 -> 446 bytes .../resources/items/examplechestplate.png | Bin 558 -> 558 bytes src/main/resources/items/exampledoor.png | Bin 406 -> 445 bytes src/main/resources/items/examplelog.png | Bin 0 -> 446 bytes src/main/resources/items/examplesapling.png | Bin 0 -> 480 bytes src/main/resources/items/exampletree.png | Bin 0 -> 606 bytes src/main/resources/items/examplewall.png | Bin 448 -> 545 bytes src/main/resources/locale/en.lang | 6 +++- src/main/resources/objects/examplechair.png | Bin 0 -> 725 bytes src/main/resources/objects/examplesapling.png | Bin 0 -> 466 bytes src/main/resources/objects/exampletree.png | Bin 0 -> 17702 bytes .../resources/particles/exampleleaves.png | Bin 0 -> 600 bytes 23 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleLogItem.java rename src/main/java/examplemod/examples/items/tools/{ExampleSwordItem.java => ExampleSwordWeapon.java} (87%) create mode 100644 src/main/java/examplemod/examples/objects/ExampleTreeObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java create mode 100644 src/main/resources/items/examplechair.png create mode 100644 src/main/resources/items/examplelog.png create mode 100644 src/main/resources/items/examplesapling.png create mode 100644 src/main/resources/items/exampletree.png create mode 100644 src/main/resources/objects/examplechair.png create mode 100644 src/main/resources/objects/examplesapling.png create mode 100644 src/main/resources/objects/exampletree.png create mode 100644 src/main/resources/particles/exampleleaves.png diff --git a/src/main/java/examplemod/Loaders/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java index 66ec22d..65dd8c0 100644 --- a/src/main/java/examplemod/Loaders/ExampleModCategories.java +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -85,6 +85,8 @@ private ExampleModCategories() {} // YOUR MOD SUB CATEGORY public static final String MOD_OBJECTS = "objects"; + public static final String EXAMPLEWOOD = "examplewood"; + public static void load() { // ITEM CATEGORIES (not Creative-visible right now, but valid categories) @@ -93,16 +95,24 @@ public static void load() { MOD); ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS", - new LocalMessage("itemcategory", "examplemodobjectssubcat"), + new LocalMessage("itemcategory", "examplemodobjectsubcat"), MOD, MOD_OBJECTS); + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD, MOD_OBJECTS,EXAMPLEWOOD); + // CRAFTING CATEGORIES ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", new LocalMessage("itemcategory", "examplemodrootcat"), MOD); - ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", - new LocalMessage("itemcategory", "examplemodobjectscat"), + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectsubcat"), MOD,MOD_OBJECTS); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD,MOD_OBJECTS,EXAMPLEWOOD); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index abc11d6..198e969 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -7,9 +7,8 @@ import examplemod.examples.items.consumable.ExamplePotionItem; import examplemod.examples.items.materials.*; import examplemod.examples.items.tools.ExampleProjectileWeapon; -import examplemod.examples.items.tools.ExampleSwordItem; +import examplemod.examples.items.tools.ExampleSwordWeapon; import necesse.engine.registries.ItemRegistry; -import necesse.inventory.item.ItemCategory; public class ExampleModItems { public static void load(){ @@ -20,9 +19,10 @@ public static void load(){ ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); + ItemRegistry.registerItem("examplelog", new ExampleLogItem().setItemCategory("materials","logs"),10,true); // Tools - ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); + ItemRegistry.registerItem("examplesword", new ExampleSwordWeapon(), 20, true); ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); // Armor diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 3c87cb2..6953650 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -1,9 +1,6 @@ package examplemod.Loaders; -import examplemod.examples.objects.ExampleBaseRockObject; -import examplemod.examples.objects.ExampleObject; -import examplemod.examples.objects.ExampleOreRockObject; -import examplemod.examples.objects.ExampleWallWindowDoorObject; +import examplemod.examples.objects.*; import necesse.engine.registries.ObjectRegistry; //NOTE item and crafting categories subject to change @@ -16,10 +13,7 @@ public static void load(){ .setItemCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS) .setCraftingCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS), 2, true); - //this wont currently display in creative due to how creative is coded but this is subject to change - ObjectRegistry.registerObject("exampleobject2", new ExampleObject() - .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS) - .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS), 2, true); + // Register a rock object ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); @@ -29,5 +23,17 @@ public static void load(){ // Register a wall object, window object and door object ExampleWallWindowDoorObject.registerWallsDoorsWindows(); + + // Register a tree object + ObjectRegistry.registerObject("exampletree",new ExampleTreeObject(),0.0F,false,false,false); + + // Register a sapling object + ObjectRegistry.registerObject("examplesapling", new ExampleTreeSaplingObject(),10,true); + + // Register a furnature object this won't currently display in creative due to how creative is coded but this is subject to change + ObjectRegistry.registerObject("examplechair", new ExampleWoodChairObject() + .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD) + .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD),50,true); + } } diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index 0d127b7..84a1a44 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -115,16 +115,6 @@ public static void registerRecipes(){ } )); - Recipes.registerModRecipe(new Recipe( - "exampleobject2", - 1, - RecipeTechRegistry.WORKSTATION, - new Ingredient[]{ - new Ingredient("examplestone", 7), - new Ingredient("exampleitem", 3) - } - )); - //COOKING POT RECIPES Recipes.registerModRecipe(new Recipe( "examplefood", @@ -167,6 +157,16 @@ public static void registerRecipes(){ } )); + //CARPENTER RECIPES + Recipes.registerModRecipe(new Recipe( + "examplechair", + 1, + RecipeTechRegistry.CARPENTER, + new Ingredient[]{ + new Ingredient("examplelog", 5), + } + )); + } } diff --git a/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java new file mode 100644 index 0000000..5d2a978 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java @@ -0,0 +1,11 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.matItem.MatItem; + +public class ExampleLogItem extends MatItem { + + public ExampleLogItem() { + super(500,Rarity.UNCOMMON, new String[]{"anylog"}); + + } +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java b/src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java similarity index 87% rename from src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java rename to src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java index 43e856f..76fad1e 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleSwordItem.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java @@ -4,11 +4,11 @@ import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; // Extends SwordToolItem -public class ExampleSwordItem extends SwordToolItem { +public class ExampleSwordWeapon extends SwordToolItem { // Weapon attack textures are loaded from resources/player/weapons/ - public ExampleSwordItem() { + public ExampleSwordWeapon() { super(400, null); rarity = Item.Rarity.UNCOMMON; attackAnimTime.setBaseValue(300); // 300 ms attack time diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java new file mode 100644 index 0000000..ca7fae6 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java @@ -0,0 +1,27 @@ +package examplemod.examples.objects; + +import necesse.inventory.lootTable.LootTable; +import necesse.level.gameObject.TreeObject; +import necesse.level.maps.Level; + +import java.awt.*; + +public class ExampleTreeObject extends TreeObject { + public ExampleTreeObject(){ + super("exampletree", // textureName + "examplelog", // logStringID + "examplesapling", // saplingStringID + new Color(116, 69, 43), // mapColor + 45, // leavesCenterWidth + 60, // leavesMinHeight + 110, // leavesMaxHeight + "exampleleaves"); // leavesTextureName + } + + // Optional: override drops if you want something different than the base TreeObject default + // (base TreeObject drops 1-2 saplings + 4-5 logs, splitItems(5)) + @Override + public LootTable getLootTable(Level level, int layerID, int tileX, int tileY) { + return super.getLootTable(level, layerID, tileX, tileY); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java new file mode 100644 index 0000000..74c8d0b --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java @@ -0,0 +1,12 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.TreeSaplingObject; + +public class ExampleTreeSaplingObject extends TreeSaplingObject { + + public ExampleTreeSaplingObject(){ + super("examplesapling", "exampletree", 1800, 2700, true); + + } + +} diff --git a/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java new file mode 100644 index 0000000..eaf623f --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java @@ -0,0 +1,11 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.furniture.ChairObject; + +import java.awt.*; + +public class ExampleWoodChairObject extends ChairObject { + public ExampleWoodChairObject(){ + super("examplechair", new Color(116, 69, 43)); + } +} diff --git a/src/main/resources/items/examplebaserock.png b/src/main/resources/items/examplebaserock.png index f948e26509500a5cc4c8e3dc4a9c49f69375b0fc..fe2a36aeb8e0dcdf80ff52c67d3678a2f07894fd 100644 GIT binary patch delta 658 zcmV;D0&V^AE0qS2a|y-(egMCkr%gkVd>?-T0s<)-5n{Vg00076NklPu1 z5QSfPk{p0DAtAvom&h(ch!v4YBqYQlin8Pw*?{0ubAiAVi*Cnd*DJSsT3LmfMZ5h| z-*efn_5gt3CELjC^*I@7qw~$i)KxQnz&9-o)30>0U4=S4$EUWcdOulGcsh59;E#X! zBXG6jX4NPF{KVaUdO4T|g+isp`1#ka<*Af3m*3&zD?5blfpzfm9?f2HU`s6k#0fYC zZt+@;hU?i{cnU_~8S4cxe=Z6<0&C#)7*<}60$GAJaJKTpQHb_{b~4w6F>GVRG8}(TZL8o-y$Kf3T3|9kS5TkG*xG)by zQLyN(@C5;F*`wQdNR`jpWSA2eh_c8S6 z2;4b{?i8E?I9rE}t|v)%G2CX~ejXSVVZj5L&D|^VDah}+d;Gh=q-fu>qGXD}lhqLORrfju0OWCPLI0O^gsL3u{ZG zg~rD(u(2>PQ9C}tI_k$ri8V$E!Nmd0nsiC%0B?Og0N`;p62^%bO!jYO!`(xp^mP6+(cx3I%J}&U1KHT&Pg_XOaQSCPJJmP#*`1~wUHb&zcX~>l zSMhVYSL_?ae_&-};W8G63IXf18k)R)PpcAoi9^cBw`nH_j3u?ghTm0V5nj>5A8<5i z7n2=1xs7#c;`${6b5?+7)YTB8X}WeOkFuuZ8R(2$CAorJDF#pYT{CQ1Bw#P(d?Hi` zv!HJ}+_hSXA_3xT-F7Abu!8GKaAZz&A>9Eav`a4FoTEFH(Ygy!2RbA7>t_i{uad;$ zorQDmk+Px+CDiVO9a5hgR5|_uq(CDas>Ni9ae5`FM~SQ z*pfTT&Voz5Uvf}25uh@;qL@0ga2#a$_dHd(pqC0jfZGKn)0Kc&%6!uaIZ`44QU#n$ zTgxg2ae`bJ3K2~^004>POzngqg>8;WBa1-V8QyO5 z!MqL7*R+{G0VOUJfv9*yG5m#v0{BO@5xSRRxL7@YGJd1N1Uzdw@|A%(G@~{-TX{CIcNSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#*F<%yf~_Z!d!Or9=| zArY-_ryb;C3>0zTkGz?9_S%&93#JIoU-0LcPTKl6I#c`=mg@Xr%y!jG?7f?FbH`;L zHREN+D=b!j&nu7nKJVUc-al5I3tFtUcE{J8QTcG&KqXY>(=?_{DJLd%u5SpR$TfS$ z?$Y{4Yql`d^2msm>(%UQhPo!PC{xWt~$(69D_fuuA{{ literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplechestplate.png b/src/main/resources/items/examplechestplate.png index 19048bad6cbe6f97aed11a0153498555d716b371..23dc5457e38b6fd56ab32a6c7f006344a3202dc0 100644 GIT binary patch delta 33 pcmZ3-vW{g!G3POcI)?qTmiszStdZwrVq(%_+@Pgczi~+yBLKuQ3k3iG delta 33 ocmZ3-vW{g!G3S2L_t(oh3(RzZo*I)hT&(+JOTv?7l5mu z8-8vHS3$S}M6ZBw6(n|+pery~rVbfxDXkzo$@;41^y{DVz5Quv`R}~c4Ngrj zO$N`+Yt4RbGV~d|Jqm>8BO}n!sm!08!sCU(fpI5<4@0)K9x@ZTAg1~)d(n&Pc(QTJC( z=_mN^3&irJB^Vob=I`!te9~_suYum%NE`VLoSg7(BKYm{Aisg&>|B(q{U`DoczA%RDL6QE z7RB;~1-QEY=xHObfuL`B&|F?hegmb{E)Vh>xVU`u0e@~_-~|W+GhS=z!zTa$002ov JPDHLkV1hCCd~pB( diff --git a/src/main/resources/items/examplelog.png b/src/main/resources/items/examplelog.png new file mode 100644 index 0000000000000000000000000000000000000000..1740e636423bde8e19806c198d34bbd16813ffa4 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#*FHc(%w_8sT~CQlc~ zkcif|(++YTG7xFAKWZuBQpomkL3M?IW6Xi(?MeqVT_(ugaW3R4WbZCxuwD6fb>{44 z0S9ZAtLWYh#7<-RwEahXXDx5shjU!SJ3znHwjhVgN} zLVGfU)t2Pq`L+{$7F`g0v*+yfuWN5fUOy!=;kx0GSb2-7$_l+Jk|itSwlKw=Q}0QB zZC9P^|Vt@P?gxtI4{=xeimb9hW^0Vv)J>@orS^#Z|tqnFD=fz3cv)nfyI$ gxbzc?4Bv5KZYJlpnExr)fC0qd>FVdQ&MBb@0Ho)msQ>@~ literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplesapling.png b/src/main/resources/items/examplesapling.png new file mode 100644 index 0000000000000000000000000000000000000000..f94db9fa5a56c1346ed748ff18ed7b6c86ddd7d6 GIT binary patch literal 480 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#*Flu>urmWX zNJQ(~X@*^g97Nje#iJdQH{aNmUa(Q@jZTZM*hh}0*4Egrm)kZ3eBl#oYHb(DJ)*rv zAuR79W7_IM-!m#_));jCoUQWtneDS${d$fiRb{D%xaZfL*wLc7dG(zCFP=_2nlt4p z>nxKJMb0XrWi0QfH(Z+M{NQJkPeaSA827w$N=1MFam1X_xG+&^;krbjhqYd+amoA- z<~S@`et7fU;sZ>zmbe3-t*vm`Xn>&10!pT+`Qkq(#}Dp+Y3-b7_&^ zfgii$mMXFT3r|_2{!1rP`f7=v!bjPT%K<%_j~F*3W`{oB$XW9D+|PZ!-Sb{ZUoY@G zaEg_|vWP+C1oMZpd=E~>Fx1bzeae#Ywf!9{Q_g}%EHlo>owRHWSy{(uxY+H~v)^T) PaAojx^>bP0l+XkK*ZjO4 literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampletree.png b/src/main/resources/items/exampletree.png new file mode 100644 index 0000000000000000000000000000000000000000..46507e4656ce6f9c178cc5b2b01cf656ef41d0a6 GIT binary patch literal 606 zcmV-k0-^nhP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHU>0vtGIGdKbO00E*&L_t(oh2_>wOH@%D$MH`QxD(SLbs>X6NQ6wJub?1` z)-Boz?OOB>W-;raHxRh!1%x2tiQ_|%hH0X$+)S%8Gv_{Zy^EM5{Z{AU|Nr0f z``&Z!IsEUscG&b~2n6RZy#VKagH2meT*F+{KXw9|7Nfj7 z=0iv!iS!5r?b-)R9$?_GVb0^$qr#kz?X|x1ppaLlbP0rAJ!LW)A9mPjclzxed&oNu zb;^`3fn^VocQo8@wwi;sruXgDT%Acb!J!|8&qzz|fy6 zI{&BL&LzKUtwjB|?RQT6LjIj6dluBb5fzabR&ai0ZbYSDK~@(oz%5gMjobIF)&fRO zpz=M=Qp$C zg};ugyoCvqPk;uEN(US`05D~$J?pws^crBj-k5P}K4%U;k zux1TFo3=GrOkBv{iGT?oDmsl+PgnJLSTCf{C^E?c@>4^rsD7Vb@;D< u4S5Q=ei8mEAUWN#Eflb*|F0|Xk1w-iS95;F*bM*x00{s|MNUMnLSTZJ@2+Y9 delta 302 zcmV+}0nz@U1i%B3n|}cq03i#=iwN%k009I^L_t(oh3%Fx4uU`sMW4n3FJMLC4J5Xn ziPLx{5-Q^rAQXlRV4>0K7i`#tMH9#9@0gjLW#8XjfM3_bm&nN<04r86O>pA|z>(wN zT+5fbzd@tRJ8!ohMJ^NnV z=XpaDd;z-zq?#Ilgk(}YR`v-!lvI6-E$sR$z3=CCjK#eU748MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh z?!xdN1Q+aGJ{c&&S>O>_%)r1c48n{Iv*t(u1=&kHeO=jKF)=a8>na@X*J5B`lJseT~s`p~byeXE`=;&N z?k8Qi)qU30SkY>NK8&OuMQ((KTl8z<^7eLnKl zS=Qsz`uI%ii3|rz)fo54tTs3NemiFW!rso`Z+6{HpI?0;VdeEm6^DYOoDa70-z$t` z?lCFS+#Ukfk*egZ$kmE$jc$D0lgMXy`3JpoKTH z8%nOq-`%v9@s<)}(`i43^_CU0_W7mF^SJlooBy*1`nA4$u3h@_f8UF(SBpIx9$0I-*1-OTMU@tjJ-+Pt^7cR<8^pGNPV0(M8kt@Qg- z;+NM5DeQQ|vcd93-N#Ss&Y%AHef!o*#jW$ZHSU9?Kt`4{Jb1xy@FHW=?B^;U%zpk( kn%5Pu&;O?iEE4}z+rRv{O!^zAmn=xg)78&qol`;+0DV|G3jhEB literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/examplesapling.png b/src/main/resources/objects/examplesapling.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa5421729df8c604fa1b6bcd201d5f1ca0cc968 GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#)a5IZU6V+3@8n5TskwLoM_R~dgKsi&Yi{rDelfzXAV6@l^YlCEeO-Ub@8J7 zq|C$HGoQUry=QK$-)g_#&+n~-QNexPhz&K7pE-Z#vw1r?nqOGHT)N0HUuoUJFH!>M zVrp~yCFgUVj7iTsYPG?>mdz&p$!QOt6XB){&k3)Zaz9U@maS!l8cXJC$5juA-WwJ-ES{Nt&{ewM$o14SVE-M~(tEA1ZM^Qhk+?AN%Qxzopr0N$Id A0RR91 literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/exampletree.png b/src/main/resources/objects/exampletree.png new file mode 100644 index 0000000000000000000000000000000000000000..769e6eb089db27237c62792255080cf17cd4170f GIT binary patch literal 17702 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/particles/exampleleaves.png b/src/main/resources/particles/exampleleaves.png new file mode 100644 index 0000000000000000000000000000000000000000..796ca9816fa5f7cfedb59d4f17d652d277c65751 GIT binary patch literal 600 zcmV-e0;m0nP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHU>0w^u8#4jEI00EpyL_t(oh3%HXN&-<7hQFc*ut16SW~*9+1ffvtprCC~ z6up4pdKPyb^a4>7v~AHgg1|Q7N>|Y%C=~bt(_%Qob!M(JS4~Ckw;8>j``>fV`8^5_ zhr{7;IA+7rtP_}bt~mGg_xC&v)NFaW=`0>Aqi#&=%%nM>>H~bx_}6oy_d4MSfJdGa z<3}#wqH^tB#@2eOYmAQ-(A4Q@XUgmX@Wy+l#G?C6n4B#6NPcUq0+1tTrIR&3DcTp` zL-$S9fl1xHQx9{e36S>+!IJ_F^=XVV7Vwl~AA%0s+B!+6L@``zs}38i1L-p+ia76r zvw`)t>HyHEPl;mas5B=^ev*eEMQhRm;Dy&j9V-Xw7axZmZ##UQwGlR&!yJ>9s#gh~ zRJc*M#<*AkrNeua0oYJK{3{0000 Date: Mon, 2 Feb 2026 03:14:06 +0000 Subject: [PATCH 15/28] Add examplesapling loot and update assets Added 'examplesapling' to the loot table and updated the English localization with 'examplechair'. Also updated the examplesound.ogg sound asset. --- .../examplemod/examples/ExampleLootTable.java | 1 + src/main/resources/locale/en.lang | 1 + src/main/resources/sound/examplesound.ogg | Bin 16094 -> 27010 bytes 3 files changed, 2 insertions(+) diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java index ef87e87..e9de350 100644 --- a/src/main/java/examplemod/examples/ExampleLootTable.java +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -11,6 +11,7 @@ public class ExampleLootTable { new LootItem("examplebar", 20), new LootItem("examplepotion",1), new LootItem("examplefood",1), + new LootItem("examplesapling",1), new OneOfLootItems( new ChanceLootItem(0.60f, "examplesword"), new ChanceLootItem(0.60f,"examplestaff") diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 39bfa46..fa195d4 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -10,6 +10,7 @@ examplesapling=Example Sapling exampletree=Example Tree examplewall=Example Wall exampledoor=Example Door +examplechair=Example Chair [item] exampleitem=Example Item diff --git a/src/main/resources/sound/examplesound.ogg b/src/main/resources/sound/examplesound.ogg index 40c9c2073366ddebe3c9b5e88342db158104326a..08ebdb8fb7f01d8ad30905c4278e7ea2b57b76dd 100644 GIT binary patch delta 23297 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 BcB=pY delta 12295 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 Date: Mon, 2 Feb 2026 03:39:32 +0000 Subject: [PATCH 16/28] Add detailed comments to example preset classes Expanded documentation and inline comments in ExampleLootTable, ExamplePreset, and ExamplePresetCode to clarify the structure, usage, and logic of presets and loot tables. These changes improve code readability and serve as a guide for developers learning how to create and use presets and loot tables in the mod. --- .../examplemod/Loaders/ExampleModRecipes.java | 1 - .../examplemod/examples/ExampleLootTable.java | 50 +++++-- .../examplemod/examples/ExamplePreset.java | 116 ++++++++++++++-- .../examples/ExamplePresetCode.java | 124 ++++++++++++++---- 4 files changed, 246 insertions(+), 45 deletions(-) diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index 84a1a44..b1e9662 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -167,6 +167,5 @@ public static void registerRecipes(){ } )); - } } diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java index e9de350..8ad567a 100644 --- a/src/main/java/examplemod/examples/ExampleLootTable.java +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -5,18 +5,48 @@ import necesse.inventory.lootTable.lootItem.ChanceLootItem; import necesse.inventory.lootTable.lootItem.OneOfLootItems; +/** + * This loot table can be referenced from presets, object entities (like storage boxes), + * mobs, or any system that accepts a LootTable instance. + */ public class ExampleLootTable { + + /** + * A reusable LootTable instance. + * The LootTable constructor takes a list of "loot entries" which are rolled when loot is generated. + * Each entry can be: + * - guaranteed items (LootItem) + * - probabilistic items (ChanceLootItem) + * - groups like "pick one of these" (OneOfLootItems) + */ public static final LootTable exampleloottable = new LootTable( - new LootItem("exampleore", 8), - new LootItem("examplebar", 20), - new LootItem("examplepotion",1), - new LootItem("examplefood",1), - new LootItem("examplesapling",1), - new OneOfLootItems( - new ChanceLootItem(0.60f, "examplesword"), - new ChanceLootItem(0.60f,"examplestaff") - ) + + // Guaranteed drops: + // LootItem(String itemStringID, int amount) + // These are always added when the table is rolled. + new LootItem("exampleore", 8), + new LootItem("examplebar", 20), + new LootItem("examplepotion", 1), + new LootItem("examplefood", 1), + new LootItem("examplesapling", 1), + + // Group entry: OneOfLootItems will attempt to pick ONE option from the list below. + // In your case, the options are "chance-based" items. + new OneOfLootItems( + + // ChanceLootItem(float chance, String itemStringID) + // 0.60f = 60% chance for this item to be granted IF this option is selected. + // Because these are inside OneOfLootItems, the group will choose a single option, + // then that option rolls its chance. + new ChanceLootItem(0.60f, "examplesword"), + new ChanceLootItem(0.60f, "examplestaff") + ) ); - private ExampleLootTable() { } + /** + * 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/ExamplePreset.java b/src/main/java/examplemod/examples/ExamplePreset.java index 5b57cb3..708721b 100644 --- a/src/main/java/examplemod/examples/ExamplePreset.java +++ b/src/main/java/examplemod/examples/ExamplePreset.java @@ -3,25 +3,119 @@ import necesse.engine.util.GameRandom; import necesse.level.maps.presets.Preset; +/** + * ExamplePreset (Script-based) + * + * This preset is the same idea as your code-built room, but it is created using a big text string + * in Necesse's "PRESET script" format. + * + * Think of it like this: + * - The big string contains a saved layout (tiles + objects + rotations) + * - applyScript(...) reads that string and loads it into this Preset + * - Then we do extra steps (like filling a chest with loot) + */ public class ExamplePreset extends Preset { - // Pass a GameRandom so loot is randomized + + /** + * Constructor + * + * You pass in GameRandom so anything random (like loot) can be rolled properly. + * In world generation, Necesse often uses a seeded random so the same world seed + * produces the same results every time. + */ public ExamplePreset(GameRandom random) { - //width and height of our preset + + // 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); - /*string output of the preset from the game decoded from url safe base64 zlib compressed text - you don't actually need to decompress this but it makes showing whats going on easier */ - String examplePresetScript = "PRESET={width=11,height=11,tileIDs=[98, exampletile],tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98],objectIDs=[0, air, 290, storagebox, 85, woodwall, 298, walltorch],objects=[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 290, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85],rotations=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2],tileObjectsClear=true,wallDecorObjectsClear=true,tableDecorObjectsClear=true,clearOtherWires=false}\n"; + /* + * This is a PRESET script string. + * + * It's basically a "saved blueprint" of a structure. + * The game can export these, and you can paste them into code like this. + * + * The important parts (high level): + * + * width / height + * - Size of the structure. + * + * tileIDs + tiles + * - "tileIDs" is a small list (palette) of tile types used in this preset. + * - "tiles" is the full grid. + * - Each number in "tiles" refers to an entry from tileIDs. + * + * objectIDs + objects + * - Same idea as tiles, but for objects (walls, torches, air, storagebox). + * - "objectIDs" is the palette. + * - "objects" is the full grid. + * + * rotations + * - Rotation for each placed object (same length/order as the objects grid). + * - Most objects use rotation 0/1/2/3 for directions. + * + * ...Clear flags... + * - These tell the game whether it should clear decorations/walls/etc when stamping the preset. + * + * The string is huge because it contains *every tile* in the 11x11 grid. + * 11 x 11 = 121 entries, which matches the long arrays you see. + */ + String examplePresetScript = + "PRESET={width=11,height=11," + + "tileIDs=[98, exampletile]," + + "tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98]," + + "objectIDs=[0, air, 290, storagebox, 85, woodwall, 298, walltorch]," + + "objects=[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 290, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85]," + + "rotations=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2]," + + "tileObjectsClear=true,wallDecorObjectsClear=true,tableDecorObjectsClear=true," + + "clearOtherWires=false}\n"; - //actually apply the preset from examplePresetScript onto the world + /* + * 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); - //Fill the storage box with loot from our example loot table - addInventory(ExampleLootTable.exampleloottable, random,5,5); + /* + * Add loot into the storage box inside the preset. + * + * IMPORTANT IDEA: + * Coordinates here are PRESET coordinates, not world coordinates. + * + * So (5, 5) means: + * - 5 tiles from the left edge of the preset + * - 5 tiles from the top edge of the preset + * + * We are assuming the storage box was placed at that coordinate in the script. + */ + addInventory(ExampleLootTable.exampleloottable, random, 5, 5); - // Optional: only allow placing if the whole area isn’t floor already (example rule) - addCanApplyRectEachPredicate(0, 0, width, height, 0, (level, levelX, levelY, dir) -> - !level.getTile(levelX, levelY).isFloor + /* + * Optional placement rule: + * + * addCanApplyRectEachPredicate checks a rectangle area and decides if the preset is allowed + * to be stamped there. + * + * This can prevent things like: + * - placing the room on top of an existing base + * - overwriting important tiles + * + * The lambda (level, levelX, levelY, dir) -> ... is a short way to write a function. + * + * Our rule says: + * "If the world tile is already a floor, do NOT allow the preset to be placed." + * + * The ! means "not". + * So: + * - if isFloor is true, !isFloor is false (so placement fails) + * - if isFloor is false, !isFloor is true (so placement is allowed) + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor ); } } diff --git a/src/main/java/examplemod/examples/ExamplePresetCode.java b/src/main/java/examplemod/examples/ExamplePresetCode.java index be9deff..e4c644b 100644 --- a/src/main/java/examplemod/examples/ExamplePresetCode.java +++ b/src/main/java/examplemod/examples/ExamplePresetCode.java @@ -5,45 +5,123 @@ import necesse.engine.util.GameRandom; import necesse.level.maps.presets.Preset; +/** + * ExamplePresetCode + * This class describes a small "structure" (a room) that the game can stamp into the world. + * In Necesse, a "Preset" is basically a small grid of tiles + objects that can be placed onto a level. + * This version builds the room using normal Java code (loops and variables), + * instead of using a big PRESET={...} text script. + */ public class ExamplePresetCode extends Preset { + + /** + * Constructor + * Constructors run when you create the object: new ExamplePresetCode(random) + * The GameRandom is passed in so things like loot can be randomized, + * but still be repeatable (important for world generation). + */ public ExamplePresetCode(GameRandom random) { - // Pass a GameRandom so loot is randomized + + // 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); - int floor = TileRegistry.getTileID("stonefloor"); - int wall = ObjectRegistry.getObjectID("stonewall"); - int air = ObjectRegistry.getObjectID("air"); - int storagebox = ObjectRegistry.getObjectID("storagebox"); // replace with what you want + /* + * 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 background - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - setTile(x, y, floor); - setObject(x, y, air); + /* + * 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) } } - // Simple rectangle “room” + /* + * 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); - setObject(x, height - 1, wall); + 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); - setObject(width - 1, y, wall); + setObject(0, y, wall); // left edge + setObject(width - 1, y, wall); // right edge } - // Put a storagebox + /* + * 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; - setObject(storageboxX,storageboxY, storagebox,1); - //Fill the storage box with loot from our example loot table - addInventory(ExampleLootTable.exampleloottable, random,storageboxX,storageboxY); + /* + * Place the storage box object at the center. + * + * setObject(x, y, objectID, rotation) + * + * Some objects use rotation to decide which way they face. + * 0/1/2/3 usually mean different directions. + * For a storage box it usually doesn't matter much, but we set it anyway. + */ + setObject(storageboxX, storageboxY, storagebox, 1); + + /* + * Fill the storage box with loot. + * + * ExampleLootTable.exampleloottable is your custom LootTable from the other class. + * + * addInventory(...) searches for an object with an inventory at that position + * (like a storagebox) and then generates loot into it. + */ + addInventory(ExampleLootTable.exampleloottable, random, storageboxX, storageboxY); - // Optional: only allow placing if the whole area isn’t floor already (example rule) - addCanApplyRectEachPredicate(0, 0, width, height, 0, (level, levelX, levelY, dir) -> - !level.getTile(levelX, levelY).isFloor + /* + * OPTIONAL SAFETY RULE (CanApply predicate): + * + * This says: "Only allow this preset to be placed if the area is suitable." + * + * addCanApplyRectEachPredicate(...) checks every tile in a rectangle. + * If ANY tile fails the test, the preset cannot be applied there. + * + * Our test: + * !level.getTile(levelX, levelY).isFloor + * + * Meaning: + * - If the tile already IS a floor, then this returns false (bad) + * - If the tile is NOT a floor, then this returns true (good) + * + * In plain English: + * "Don't place this room on top of an area that already has flooring." + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor ); } -} \ No newline at end of file +} From 6498fcabb7da1ffc85ff20028de5578d2ba15a49 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Mon, 2 Feb 2026 21:34:09 +0000 Subject: [PATCH 17/28] Add pre-antialias textures Gradle task Add a new Gradle task (preAntialiasTextures) to run PreAntialiasTextures via Necesse.jar for processing all resource texture folders. The task is grouped under "necesse" and accepts the project resource directories as a semicolon-separated argument. Numerous PNG assets were updated as a result of running the pre-antialiasing process. --- build.gradle | 10 ++++++++++ src/main/resources/items/examplebar.png | Bin 657 -> 784 bytes src/main/resources/items/examplebaserock.png | Bin 789 -> 1025 bytes src/main/resources/items/exampleboots.png | Bin 402 -> 406 bytes src/main/resources/items/examplechair.png | Bin 446 -> 423 bytes .../resources/items/examplechestplate.png | Bin 558 -> 648 bytes src/main/resources/items/exampledoor.png | Bin 445 -> 427 bytes src/main/resources/items/examplefood.png | Bin 354 -> 431 bytes src/main/resources/items/examplehelmet.png | Bin 485 -> 534 bytes .../items/examplehuntincursionmaterial.png | Bin 317 -> 284 bytes .../items/exampleincursiontablet.png | Bin 332 -> 463 bytes src/main/resources/items/examplelog.png | Bin 446 -> 477 bytes src/main/resources/items/exampleore.png | Bin 494 -> 549 bytes src/main/resources/items/exampleorerock.png | Bin 5617 -> 1025 bytes src/main/resources/items/examplepotion.png | Bin 469 -> 599 bytes src/main/resources/items/examplesapling.png | Bin 480 -> 514 bytes src/main/resources/items/examplestaff.png | Bin 395 -> 451 bytes src/main/resources/items/examplestone.png | Bin 467 -> 520 bytes src/main/resources/items/examplesword.png | Bin 389 -> 464 bytes src/main/resources/items/exampletree.png | Bin 606 -> 716 bytes src/main/resources/items/examplewall.png | Bin 545 -> 560 bytes src/main/resources/mobs/examplebossmob.png | Bin 4070 -> 10700 bytes .../resources/mobs/icons/examplebossmob.png | Bin 402 -> 449 bytes src/main/resources/mobs/icons/examplemob.png | Bin 398 -> 441 bytes .../resources/objects/examplebaserock.png | Bin 1078 -> 2183 bytes src/main/resources/objects/examplechair.png | Bin 725 -> 1048 bytes src/main/resources/objects/exampleore.png | Bin 8435 -> 2088 bytes src/main/resources/objects/examplesapling.png | Bin 466 -> 547 bytes src/main/resources/objects/exampletree.png | Bin 17702 -> 30999 bytes src/main/resources/objects/examplewall.png | Bin 2796 -> 5257 bytes .../resources/particles/exampleleaves.png | Bin 600 -> 946 bytes .../player/armor/examplearms_left.png | Bin 1491 -> 7333 bytes .../player/armor/examplearms_right.png | Bin 1513 -> 7328 bytes .../resources/player/armor/exampleboots.png | Bin 2600 -> 9571 bytes .../resources/player/armor/examplechest.png | Bin 2152 -> 8431 bytes .../resources/player/armor/examplehelmet.png | Bin 2833 -> 7592 bytes 36 files changed, 10 insertions(+) diff --git a/build.gradle b/build.gradle index 6104eeb..aa88ff0 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,16 @@ task createModInfoFile(type: JavaExec) { // Makes compiling also create mod info file classes.dependsOn("createModInfoFile") +task preAntialiasTextures(type: JavaExec) { + group "necesse" + description "Runs pre-antialiasing on all textures in the resources folder" + + classpath = files(gameDirectory + "/Necesse.jar") + + main "PreAntialiasTextures" + args "-folders", "${sourceSets.main.resources.srcDirs.stream().map {file -> file.path}.toArray().join(";")}" +} + task runClient(type: JavaExec) { group "necesse" description "Run client with current mod" diff --git a/src/main/resources/items/examplebar.png b/src/main/resources/items/examplebar.png index 92d03dff9b0e1f276bbb9d2de2188d6dffcf84eb..f23f5a87b38e921058fca9a3c717a528ce4a27c2 100644 GIT binary patch delta 761 zcmVVfd5ax3Q23dv&43DuoKv_nS@$QrRC~m+OB9w%{f1u?I3mGPXE#d1I zL9r~bFbdsaSVM#)2cY?!q}T(67EC{KNRSf(czyn<@D12uuz!$&xiX$14;CUQAwiS_ zDDgRZFrkM8DIoy&`A^Yb3~Z&mV6%Vx{t2c(^}JPg<>}>M9@P5 zuLE%Te38{&hJVNYp?c1Wfq{vEks*192Lsa&;>$N;LI5MqnU63r{QLV4BN6@wdi@&1 zZLmXNl3EOEV1Fihh`{8rI)KQg86n|(jr~=af69c->I~k^c%@b!3 zWqir-8g4e&0WhCy$!IWKdvb$l*L&SJWq6MkQcOSb=Kzr9xJttRAj@GsKT&uLt3xdM ztr!@O0KNa0f#C+%eFh#0cCb1{V8sLqfpzb1Od{KPPmqU!jb zII!3ihkuxPiQy>&C{~2;F){#c!m#KP!!?Gh|8Eg?0J_g_Fx)}a1oY5z3}!Mc(J?WgrcRLM*nAGthtmP* zK1a{TFbjhiyufCm$2lRN6LbIrg@BOHu{nSd=yGZXV5&uF$;@TwM{x>z$iT|@WFq4n z-Cy7+fW;B4JfOt$#QGdx6u?svF(E>(&#^iHJ++b&BBc79lthR(M2Jo8xT2M$egQN@ rFq+%=@;RaOL{34($UsFf;N5co)hztjDNsxU00000NkvXXu0mjfV5(MN delta 633 zcmV-<0*3vN29X7jBYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;&yra{vkl71T8V z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHS777sKhmw!xz0005iNkl zze^lJ6o9`s9*P=8FrtMP!Jk|&=vml`g+@yo3;Vzp`6C2jL2NA|VyR*whhQfXI|)R@ z6AzLI7AK-u+y!nY#X0J%Yi@7P!bM-T%6CKojvX@R+>N_Lruwtv30>C8D7&R|^`B zII;SZ^aYwoL|OGpY8P+cvl~;O5a!ZjsC;;_i75ZPW>4W2y(jN#G)H7mkHi{szmd=j z_+LyWM`>JrpcO6rD*Uc!jzrXK2e{q2ra`p;kRb~|y?>N)z>u%A4Zr|BFK=MC^AI}i zwvme-R9>PRNYbkTHr%zq0XI$#J*flhN3@Wn7lnF4KF?C%K2Vi`@$4X1e=hBb@^C6DH|Yh3DI| z3-FWs>)3p7nsKW?Ecxp_4NWzeSjG)yrc(0w+y}}TiAo;dJE7l5RPtA2;645U_TrY% TFCNJL00000NkvXXu0mjfq0|bt diff --git a/src/main/resources/items/examplebaserock.png b/src/main/resources/items/examplebaserock.png index fe2a36aeb8e0dcdf80ff52c67d3678a2f07894fd..358c39d54c1b35850e6be7d1fc9c599739823995 100644 GIT binary patch delta 1004 zcmVqb)F-S3>n~P#6a_~Td9L2{Q7&_qm{|pcaG5{iotQ6u=bngZ+ zRG|x@L_0VHU@lZ)kiqaVD+3Dy2(vISGk`FJ4H9Dpi-Qs|1b_brTJ)b0=tLBZZXn1G zn0*Y;9Ke9=^KA@$?;t+>2iFVrIU~ByG5wDcxC}6_Gcqv!hq1jGN`#=Qz(Pz6SipY< zQY=SA2Cmc!G!I)s1Q!7q4j{*J1_qGT=&9mA1G0l~ISy+GK!O?^@OXm#Kit~?NRh>W z17HaS>>)sk<$uU_VfQWJP{0xe#Q7XzIU?v8NDl>EY2ZJel8cx^5w~_mV6}rDLl_|d z$}$W%0lMK}Pk>c3VvryKMx?CB_#eSTD8}LdgzeZ3WnjSO445cF4zJ~KOR(kwVnPAt zcQSmA623SbKqwRtK~IX$fgWQZm4(e8IfEBVpbzGFhNVbe+>UIk{u}M(4z%=nr1}R z-N3+r5I_$BM(nO-!Wkkk^|<1kkpX`N266z*QnYye&w!~JyF&85qn7B@-aw1BkueMcLd%LU_{O@@aXu5mN*Id7{eJTApnwP zW`LB2zd?5FV3+_c0TFSIKO;fBiRoL22)ejCLjkNr1lju^PY59Vj~Nn(uqD=FEU^Lh z7k`EWP%T4s1Or}x-Ny_Jm?b8XG1x-@(-8~=0V5U_psE~CM#RVktUxo^VCFC~5C<3; zu(gtz{xbusa2%GyOJC5~1m2PYW;i|?*_j|8~q+_2*_+^Bu^lF2vln^ z>|~gbPADw!1+@!9-V6pv?NF5E~^2>b=|{y_0>bjui_tsz`AAs;g! zs^0(24D(Se2ie7lWsC^!20I1=20NgOK^RvkU`qoqwFeo_Gl1|Rh6`XJY{PYo12{$r ab`$_%a0;l&B}DiD0000z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu z000SaNLh0L02U0x231Vt>0(00076NklPu15QSfPk{p0DAtAvom&h(ch!v4YBqYQlin8Pw*?{0ubAiAVi*Cnd*DJSsT3Lmf zMZ5h|-*efn_5gt3CELjC^*I@7qw~$i)KxQnz&9-o)30>0U4=S4$EUWcdOulGcsh59 z;E(ttaJA!R)qf}e{KVaUdO4T|g+isp`1#ka<*Af3m*3&zD?5blfpzfm9?f2HU`s6k z#0fYCZt+@;hU?i{cnU_~8S4cxe=Z6<0&C#)7*<}60$GAJaJKTpQHb_{b~4w6F>GVRG8|8BtKd&YH-F(s6a>-y$Kf3T3|9kS5TkG* zxG)byQLyN(@C5;F*`wQdNR`jpWSA2u_IV0}Sk9R8S62;4b{?i8E?I9rE}t|v)%G2CX~ejXSVVZj5L&D|^VDah}+d;?M_9RL6T diff --git a/src/main/resources/items/exampleboots.png b/src/main/resources/items/exampleboots.png index 240c79a295fb9d94c0041af88b5d47a7453a6cca..4f01a02cd361ee078906a93b5d5a4a8838b46b38 100644 GIT binary patch delta 380 zcmV-?0fYXM1C|4jBYy#1NklCWI1|V!r~t0Lqd)-XD~%c3rJ}Pqa0zOhtJU+j2sfM5TJ_BVHTl>6f9T3K@o5C-etHCmS<;RWBkeR3q>tZi~%S{k~u&F7|E&&K$a7z z4T;V~C=P)65SMOjK1Wl_2nq?HIYf6hP@;ecXEPGnA|loS@F-xUa=}Bid81vk(T>T0 ahX4RuL~9#t)3P1_0000z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0x2o3vVYn}0002iNklTmVNRJ}#s4hS(y0IuBB7(-qT$Zq!6Qr13J*^FsxJ)lkx z0000xgatcM)_X z(YfG0!$TCqgcyVfRU$-{lr+l)KNx;8=rC%6F}`3RIt0Eke1n@ayKXvGGiNu=gNp;x z3M_>1hQ!b*2e=rxFno>dV{)^^&M21cXAm;*@x7RhTM+pH~jY&#+ z7<#!tgFzkc!GEB-KormRHccQoGYohL;O!THic+ALz$Ka-gDgqDd&KY0BYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0v!xqoPYiH00033Nkl@1W|Af+t)0c%AmVU68v*|^h8CNrkTcsQ%~ zZg*byp5D6+{5pxCggUj0Uwq6Y@Y^sXRUGn`0tfw(LCnDrMgZDNLD#nw0t&u(6$W-|YG3^sRxjE@4~% zmaHC{(8oZU^j*x|aa8m%;78kRdG=9^=wrbB3WT9G^6YaF{ulHyAg{on=){_4!;{mo zx=$AnyMe%YEfcLk{;7la9Dr|}ge%s-eP?3IeMiEej~)T(TveVByMaGftuHaUZ7fAF P00000NkvXXu0mjf|HrO= diff --git a/src/main/resources/items/examplechestplate.png b/src/main/resources/items/examplechestplate.png index 23dc5457e38b6fd56ab32a6c7f006344a3202dc0..e12990509b00069cce688fc8cb818fb33ce792c8 100644 GIT binary patch delta 624 zcmV-$0+0Q!1c(KYBYy%fGi3$l62jGfh zsLx4?Es!2u<{^g+wh+JwVq~8KeTdf~un>*%zvda3^EK)fRL8i5@ZL= zK2S&?hroZ3<^2pjVEYd<90Ako88(9Ha}4LfG)%sbp#;U9F!6~DlfmMQ3=oe7GWdh} zwhT648mJE?jW96`Fu+IyFrUL*4D9gB*&_VdkRy zcfdmcTo7R^Xn$aF2y-#aM@9^WU?Et@z~s@zVIcvN|IYx4dh~({B?P`Pe4}}aK(_-W z7yM%Q#h}0-4|c=@hKFDp#)tV3mNsCtX|*XzT0r*!x;a4pAkPIbpTc|s^8vazsXmA4 zhnWX6e^8YJjGzhxR))jMbeN0LQ$0f~YHY*uIj-CRtA9g~a{)!840zu1PZOy6X&JJ9-SkLWof6$D0VLnHzAWes~JS*Ehu1Az@=+V+7TnAcKHx zU_JT^r8P}#2ZfZL5qfi-5iO)}%HzvOr1Xpk^>i4?$*}|8JqG~yM(5?X&znvF0000< KMNUMnLSTaR9SdIo delta 534 zcmV+x0_pvT1+D~;BYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0xAKpDt{q=0004UNkl($m%ZGV>e$k{>W3Um}_-6@Y854J5kI}Ij)+T!}3bRQy_K5 zVt&o-!hj`(=6}ZV%EVDX%rChB;DI3kH{1d6%nRl5GtNyQ`L7rNkWCBhgd+g<>1v(H zkvIwnVy+#F`ItALU^wjE12F23j9>HGY??TV>#=eLW@=Trkhvi-+F=_glo7mNN(lM1 zDB@Sa7Ht4t8B?~!P-m)dr`G|BT=4L5a5rTf1q3k_#DBp1>oV7R`Gq(Nl(N&jdfKti zdivH>ou{fpZ%DW*NWR&siU#WdeDVd%o~a<_Qp~SQ^#^ek5Trztrt!DfF!nni#-1<% zsym~VE0F97eGjNJPQ3+(92mcBDtg4Y3Z(7@pY0x1b?*^Z0l_OV$4_}&%KV!Df`5Di Y$4ra-tPdvzFaQ7m07*qoM6N<$f|iHhssI20 diff --git a/src/main/resources/items/exampledoor.png b/src/main/resources/items/exampledoor.png index 715846a3bb280b62ecd1f2a72a699f4724f5d133..911d60c2630ab6b9abbfbe3a8bff8e28f8e54062 100644 GIT binary patch delta 402 zcmV;D0d4-h1FHj&BYy#MNklt#+wacKIdiNW%$hSnIs1Q^)LeUfZc#EB!~%t{~&{$8Jxj}@iXw_ z_1H&-k6`(|40~Y?#v2k?9f0O@ib@Jl=)ugx783YE0LAB;41bzLIYf#<3Pl~EkQn^g z5a#n03@b>AiU@`XSX#iVZ}5izy3gM-yk!Wj^TX<^eN9K<;>3p7;17X3hCHyZCNWF` z)4~kGa9@Ia3ZVN$20#c> z_?Y1_Sp7GKZ(y34ff=ixu!aE00n`owTxJq%4`A~-wz!vNkOli5=5t~_Fm!SOAs>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0v;3ixqqX;00032NklD!Z7>40z%RB-F2^WB?pBsK|30Fb50z|KXa1|tTcyNT(fpI2|}V^#XrxpydaZjzEDC9{7L& O0000O8umGCKEdL5@Z zEJWBqX(D07X9w0q2=lpsAUDH@_g^V;0MHCZpc!B{U<(O+A@CohF(|@{qG(vR;uy>W zSVIEE0cbv_YW@I)Cd_=~kiZrKC_X=P>=s21frTKhkQlt#5P#-#6Du`}LV!>R4(@^o zzF08pM4L=zhl zLnj0fX+ae1^Y*?@ln_{5QU@+mj6~%ALqh^w@)@OwFyI42Cj?-r9zE_ECm~UX^!2WkQ{fANx4E?$Q-JhhE z^@P&FpiBddY-}tHj~>0DN|y;}4mc2iW-|Wz^#>zB@U@D;4v>=*p{S^VIRIz~BdM)o zs)PhCpTm5Gw{r?@#Zufcp|oej$Us9d;N5co`$vE`nGgP(qyPW_07*qoM6N<$f+d%% AumAu6 delta 328 zcmZ3_{D^6SaygNCr>`qKgrSP-hU6MiRY2o!;Jk2A`A>qCVD@3 TdSs?F0}yz+`njxgN@xNA0~CW0 diff --git a/src/main/resources/items/examplehelmet.png b/src/main/resources/items/examplehelmet.png index 820f9fc893bca82a238af5a21239501649a2ea2f..69341c63f8e971958b3f208f64294d5f3e287015 100644 GIT binary patch delta 509 zcmV6Xe5MR^EJ!|-Aq?hV zydi=kiZ%RPz_)o5bJYvMGoT!5VV{TTeP+7Al}89x~m`6rv6 zfcdz5zKLNIOg&0&81O{MLTWI>d}y`F3PtNNhGS&K*x=8GptwVdKcam}tiHjY209oz zz`m|vs6g?(6@LS28bA*vm_9;jWAKLn%m?TpfgU2T;33xMF!ci-0@zAI253iCc>5fA)OI&LIPwtOg}zXkdg~fLIUPfbi;}D zIZPi>ybC{VdWF#_11}Xvo2mGOl#bd-U00000NkvXXu0mjf24mCO delta 460 zcmV;-0Wz@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHSG0RbVfsDFQ+0003gNklV4e5Jg`ij)5qW3(zInLu>~Xxd%t!9!UkAZ;%uy>9_z?+=Ddv685q?<4DnLk-weg z8O?tlkJb>ZJMYG4o-Q?sKlyUyzOPC87c#4q^Br3Nj+_SfKk@*;f+b*+7%zcZ=cT+P z!U_BrFtIkg@_z=v3oF2X3lwK5oIs~D)%`8)%>a>@w$p6=oX$YM^F#u=w;`N>*#o8I zut`m}zGwfd0>TNH1*D&^R^GU90%i|v*Z{C*ovk09 zcH+S5E0ibz`5c5Ln3$;^0w6PC<|7-9k_(_g5B7NtpCDBo(emj%%sdo32WKM0=5?44 z&_f2vXDHf;4Z*=50_Z+O4+)r0afJ|}gf#d=fZDkMJw%3H8hN00hzy-_09XEpi4&5? zl^cdnNr+w^5L=N9-82C6A+C}TR~<3*$^n>*36=HamJb7-21Z*%44|em48!6Z+9(CJ z4XK8a8@kALV(pqCH%_V8GG!#SUqGAQ37U^aJEsF)4gdfGym$y%S!rVc0000G(&67Iy3=9k`>5jgR3=A9lx&I`x0{IHb9znhg3{`3j3=J&| z48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?gFHN;HUF$kZGI+9+AZi4BWyX z%*Zfnjs#GUy~NYkmHibX8ohI2xS e^wuu-gBlDBmt@UWma)tRd%)Ax&t;ucLK6UGpJ0Lj diff --git a/src/main/resources/items/exampleincursiontablet.png b/src/main/resources/items/exampleincursiontablet.png index 979cfa6905c409c44a3350e17f2979c45a6f3de8..7f5c3ceae01adc20775436e44ff6199215992732 100644 GIT binary patch delta 438 zcmV;n0ZIPM0?z}GBYy#wNklA13dHs?s2LSal0`-F3fG;HQg}{H10qI#KU_;)&c|}xo2#Sb-)y*=ydgj+F45BhdWaC47Dm&+ z;0*y-9OFvugwg=6@&Y~A4E{8L9ukD|Ke0X^dL<#LJ|nh-L`ehRxCLldH{voIr8fM= zBf#)hLILcE$A3Sm2FRCQOM z;MF&X%YkQKAufOa9ztJ*dPGnVRUDnqfToT>xDCn>U}RusW?*>#9=$_?D$dLdY45|- zf%t#_g8U272MalbIM%Sh+B0Hw3_=)$&<5*Zpm#WM@h@RI8UOEJhchDZ^^9=!Xwbz8 g@$sgD0q>pz0J43L1q=K31^@s607*qoM6N<$f=@}sfdBvi delta 306 zcmV-20nPr;1Iz-DBYyw^b5ch_0Itp)=>Px$21!IgR9J=WmOT!FKoCYhjin8QXf$GC z?IpZ|r!cYh7}h49!Yg~#A8%uQ5o zpdnq5Do>KQFy3{&!gvjP4QNosuVSwOZ7+E*a3iL@V6Op<@9zQHW2sWD2c+)>$6f;j zH=*slY2}3d28d@XGo^9^_y6e8xYQX?Wxe{a^8kju{Jh;X%Xm2D=fUn_xWxvdb2g@F zHU_tK$H}krIV)J4LgfbJGdq79sEcV`oAdhwDmU=w3P)gEcc+5wfB*mh07*qoM6N<$ Ef}?SX^Z)<= diff --git a/src/main/resources/items/examplelog.png b/src/main/resources/items/examplelog.png index 1740e636423bde8e19806c198d34bbd16813ffa4..c0f130b5aaa90e3a9aea7ddc164fa03ff59c1c54 100644 GIT binary patch delta 452 zcmV;#0XzP_1Kk6VBYy#;NklQUY za9YDcgEWYqtr+pyK~XkD_c1pEH^WzkuP6=xiZKGkz;3`664VF*xP>5#tQjm(JbaYl zI7}XENRZ?J96ly07KIsv7-0I5LxP+T!0Tg}Yg8DNz)Tpukbhw*){wyK09-zvUpEV@ zuje$)Cpk?M69Po}7-Tcdw;=WbhQmZT2CD;LJ_p6yTZVTy4MO%YvgM=%1HKUW53)R- zAsWRzU|ce!FeEY%>*LQ1pYgf}#R0_n9A=_Joju%e2L^kFhNczU<4IF zu(BIlIe=yeBLf8h#R0HXimPRV%N~3!9zt%zmk3cp0zGCBFmxF470;vw2HqACG$b&Z uSd{pfm^8pZ%{V3$BBNci(XQEmhX4SUa)a9GP&SbO00000BYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0vj+VsedN-00033Nkl32JxVKco0Ecz=O9U!6`(L9^6B53ULREb^tx8?W<{ppvCOcR^x9EXI( z1mt1l4L@Ustkl}LMnZsee&Zf{E)$VR$5#SC-v_J`n5sD~nl>fuJE6|;i>$lAN`UMK z3$-+^VVyvfD?z5?QUn~-@!eW&(W*`B15r#JOMd@0G#>rKF{SbZ91h153j#xiT>oU& P00000NkvXXu0mjf@Xn$V diff --git a/src/main/resources/items/exampleore.png b/src/main/resources/items/exampleore.png index 62c0e6de60bc79adc39782324f14fcbbc4959a50..d9433bd8755c3dafd0dba3224728c6582f422b2c 100644 GIT binary patch delta 525 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 delta 455 zcmV;&0XY7G1mFXZBYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0vQ}9M}H`m0003bNklwsge?~F2!e%$U5e?puu$|47J`L`5N*UOtRPly;Q?l= zaZSu5%&ahq`I{v3<~`P3!i+*xP&RQtDOJn<(9>4l+^{AG;|>edJ?7t z@0Wnmo;G96Br^B<|bKu>t zMDAUQ^;O4e_Bz)%Cy?j@b@C#f1bCx-j*Y~GBR>Fm6k_vra!$bbyI!Rt2mV)NtS9;` zSsT@KP9X6eh|^GvDdGXJVQW?6u?ckj&iTGgi=OBk*FbPiz{&*xIdT9L$OG`r4&cdL x0Dqm^$~gh+KioPs32@>Bn9p9xIe}EEegQF~M#|>SgWX}cqJIM7o(SqV3ndM}Qnw<5 z0!gM*k}I%e3=d!Y z#aa*SpDYIUIe)n!fi)YVyPTHg0t*TG4WqK z3oHy&2Des7f=vYG3Zlvfnm5Tn(S)yjpr}rVMFGf?TaW)>O$)#@g13C2C<>@iZcsA~ zFe2v)SjkB!iBZJ?l!OFTdq%J{furQaY8O?rAvqyI^&@fsDb9&%D~@(f2fT9%0HJ7n UZV@rL=Kufz07*qoM6N<$f(?$Sg#Z8m delta 369 zcmX@i+|4{ext@WsILO_J@#aaLdIkmtmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6VX;z);M<#V&fq6ez-3;1OBO zz`!jG!i)^F=12eq*-JcqUD;nTiilftY!awnc^D{m-qXb~B;s&#f&}a0hKdjJT`Rd} zu-yvparrgpq0V7JHV{Z=lMedA_fH@}G3=pi+iso=#gFr)6b)B6GpHWDpSt7x0U$`& zv4}}Yz|g0Nky~&6`jm){N5!X4US4qN%nqln!W&bWH8KVOAC^%DVy2YvC72l4nPLm#950cbua zDb^$z#2H}vv4sRtQ2;dn>?1-xhq+FRK^@F&Wax$oF=BH76Mw@10~Tu85A!k1JYX6i z%4n<(K+oqK4D1X)7=F?utnh^ZJez?mhxw2#Kmw~n?l63Wi=W?ckZ8xEIDnGWPHymE z3jw0?IX(-oG2A3A1jzOI(haj1U{OQJQc&rMtvo;u0g`|~4S+%dglN(0ejx@{niW_0N9J1 delta 442 zcmeBRxy(F4xt@WsILO_J@#aaLdIkmtmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6VX;z);M<#V&fq6ez-3;1OBO zz`!jG!i)^F=12eq*-JcqUD;nTiZPm*%qp(m@qVvcqr6O&?}iCY>k0Y^EISpK$5_krtNA6Fiw!e#~hE0>^I0xgdJs#D&uNvm~lA z8z%H8CYT;pYg;&%`@(0-GwnHyLJ?Q*C;s3_pMSq#LIyj}J^=;Ot;x*SCGsw*3+ml9 zsOOe5T@ulCmGQ|BpcVY{Zk^PX)Z}8^#=zN@(PqN)FRfIKVD61u%RRX diff --git a/src/main/resources/items/examplesword.png b/src/main/resources/items/examplesword.png index 5aeae299e0a0ce1d833a519d0ffb95b299f8530e..5e8aa5e825360cf43af0134fedf97d08fdf123aa 100644 GIT binary patch delta 439 zcmV;o0Z9IZ1JDDIBYy#xNklWXE*2~{ z;z|^Zw1_RDqd=2EodFa=ghGi_N3!73u##aRSW1OK8B7~9*uX-Dsv$vgBBXXm;0%Gg z4EIq2=Pbh!nuG+|*^pWxfl?B}5||W&B*Qa?=ioSRV`u>TTz{EC5j{=7LWrW$6eS8^ z`J9xL4hsp8Yse`lu)3a}A%Qg!k`odPWC9i<^f7|cL={5=N}6Dx62R*KQbIzJL4kp+ zx&&kiAq?{@C?tUF|Hv3vV*UqVBr%xs|7dC%u-QXYIRMiL%$fuP9uyMVimb#(5HTSD zQ-f|Q$TFBO(SJkY)(Xl0AdDO`*z96qU?9!0$f~JPGm_*098P5fg+w?*JUEx^pDYGe z_Vo2%qI^b`CfPrU#|-2GYJ|Z5hcEsz%&imxyW!rmKVVu(iiLh5K#WbWasoZwP*jsa z4a3{9fVHfEX$0m@6gvmJS}0008174GD9Y~26=002ovPDHLkV1f#xw*~+J delta 363 zcmcb>+{!#bxt@WsILO_J@#aaLdIkmtmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweEpc6VX;z);M<#V&fq6ez-3;1OBO zz`!jG!i)^F=12eq*-JcqUD;nTiileYE9TV)<^jb{db&7x{|kYH7oHAoWt?cWsD z#dvuEZ{t)RZb=Cs2$7fqrh6oFGK7Sf<(PDKsJt_}@1-LP_!xJ!9GSquxJOa&oPd3i6GL_^qqgD3mLv5^Z=@zEHl%ES*U)-o zt>`2k5Xcfd-(JtLpykNFlSvv)eLL5)m2+$eIQ_g>yA|XJiFdZY4<7)61u934YECmI s-JZ~Ls`KG=?hf9C2Su3{Cx|dGOiI+`yLPg~2k0vXPgg&ebxsLQ0PfUtfB*mh diff --git a/src/main/resources/items/exampletree.png b/src/main/resources/items/exampletree.png index 46507e4656ce6f9c178cc5b2b01cf656ef41d0a6..772af699ccda22cf4af322283752b483c3cad0fd 100644 GIT binary patch delta 693 zcmV;m0!sbf1k44HBYy&uNkli;AFu=^i7Jm}>Lf}8haxVsVtUi}w zkU(+MJ%;;W{so3hU|Neo1596IxPdn$P#gersStx8F!tUvP!sS2qYPU}U<-l&?-<@Q zL@|Vc?f=a11x(*$xQ*f-1qL}3J}ki*FzBJ^Q)W;C^EWeW!x|Dy3I4QLJXz=rVp1t2X6?#QZ=wZL2*2?4=*%bfXm%yc!&~G zFbSABLoWnO8H};|2sus->I~o>Ghi@anA0>L!=s=q4HA68@Dghv4Bj+wm*E~(7Yi^5 zz{Roo_yfZS22#MF3;{+^1%gzDgFPg{Adca&l)6L|v45VXes~d%9_K(a8GkVRz%T=) zAcECahh1! zrWgZ}fPa^PhXJPl?*IEZbrIDj1X=FG;0ZPa76O+TuAmsI$Djk|OEE}-=`9S~$!`-< z;&XJbfej0>+jYu|@$*A~i$UY^-y<~OIp&^4FHH`Rt bipx;|_;u3o0tj){00000NkvXXu0mjfmZB_W delta 582 zcmV-M0=fOn1>OXZBYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0vtGIGk-V&0004^Nkl=mmrz^fm@*k%{9& zkcMfZt=vqjGc)HtbiIq1BmGwA;s5{N^ZVX&?>YSMx^~&X1$TRO3V-qSQnjzwDf%x| zxys=Djo0AJ8Gjfz9>pCw0%OL&st>3pF>(So$|i#IXPzMMeAaXn-?aleHo!d#VX8+t ziS!5r=P$hg=YE4tTTxuYT+}~y0-6@1ygTMYNFj;z2n6lg2TLAc;ILuN2}q`^Qi4SZ+#PFBbhtB-D8#gzyNh5#&;6N=Ld3>Uy5Wx!y75@N7&fb49D zE&QeGgfWDr7@QflHSNV2U7$h@n!MbdG^kK(qruc^qUoQhy5v!!DQ_ibDXDV)3Peg%Ghh0&581O0f(y09Zo+xh{CY@PZ_3NmY+E1cz@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0v#XPB7eLn0004HNklNz22B{YCdNU zK%YJUEn3F?Gk<2rb$=De=01Cq1q)-~ta5qqR{_DEeW}2%`}|ixs@#^-B})JX3{rOK z1EmS*a2wv7+j!try%mmSC9q}ZNduTmn+P*<5~iz(BJD0*yT)(tmh55O6`V2Fk)pV8aH0CQa>{ z&R#146Yj%NYW}Xw4^$-rF)?87Y~phf0!cioXF7DsY;`9f{{z_^_vmT;rP{kcJwjl6 z3;cf#ZFv=i>IW(nSS|5H$DP+SRKz9^LLMhG7cd1g{j{tn%~Y zm;uj1yN_!?(nd~w7@DJK^I6Twlwa3at~!q~>yH~&8IWGf;5U4ozD0T=Gku|r#B#~3 z=GD`qf$!hi(nBx|i?{?fkAdN^c^L3)1-eDw^fg14lqN^BrCXqWez;;dKOkDm#MF<$ zhd_m%6;s6(i5}nD-B+K$l@G>8@fhNSft@R#cQHv58PV};&$Zgd;O*AI-s(v15apJd z&^`fP)yGUD$e{~FGsnj8gyZAVbr~TcM_9L?I)9Vrv}y<#Vp|7s^BD1>h-s3|3>89> zNQL3(^mL?-0pkEhBs*Xk8v7ygYMKth3vHZ;0U`!qu zGeDFeAPj<;8gw!>*e=u&75o=O+`#H3+AufziL*T7o;wufDJg;?^CL%4>>yX|0EFS1 z_h8odWejhku|LKz9A@&By+z~95W{wc@2=?;WMBjpXzISk7=om`U#D=$YU&-DNLRM< z49ODoeb_Dx;s#_)Tfi3tCkNpP)>QH6&fEg99;Tb+f`4wpr|C`j0Ws8&xA%XL)A{S# zHHWjZq!Zk^!Hk%xeV8fThoT8)37X`|2OIo*m*eOb@ng*zgA7IdOeB}w(Y$;&3H%vH zx7Zf{>t_rctIUA9Z{5+50p`)18u0Kh@~SOr+BZbZq8!+uMW+1wXH6MHLqmMR(#^E( z7)~Ah$Z-Vh#ig;_30>&!5^+9}LLWtniD$NWm&jsImo|^pS7`^9kFxCi#r!9Q)f!b6 zeTbFglVK%Ejy)=P2h+^vvD}lx#5g$1En6^avSprAb2b_}IFR)1gZKFcigW80MNHz% zNIS}Ls9?vBKDGI$3~FkG@VhFe?E@bzdFlQ2m)F~MWt1;inf#q{zJ+3LQq%3hF!!)k zcU$HAy=39VRnagE+xiO8$xye)7Q6L}7#0#2Zm5MMKSLO^Q38LD=|>EnlG`1jySC9^ zcJk}?nbw80Ijw4}{K)7`Ep$DxV+>B5x84ryh=n98Kg2>(jWF~1PO;Duqf@pG9*m6` z{zf!ys9IH}x;{EC4iSY8RwBJd=4>0_)KQG~#keY_rsaE^)KKm*xnpB_8mN*VlqK__ zu`!N9&={X{PVF;`9IBXcDly_>gVm$^2*XO*mD+Hgd`NaF%Pv&R)pYWxFF>xS?sc=&Y3_>dvr9+9a5=>)+t3=c$G3%>WRijdGG^++i8(X#4S8^{sZMh7v*xiXCC6F4M^Fhl@SE~m%3X{vtM5zW6DMckwPXfb6);HmNS4&w$BXyAQl1q-}v2Gx*^rVWM?eT`*FR&(Rz|my(hp3qEL20{MiB2#$ z5|w*Zhsx_#jOXV?4eMt;-WK|{#Pa!k70*o&=k3#{Se^#OAy&hrZfxY+j(wf+Zj*=T z2;_3Pi<>kJTTE0F`TXt&I|iO{KK0-`U;KoW`kNT&>Y1t^n1%OnIL@%`lRq}NF8IHtV_ z@Q0x}jEfdJB@lQ_=T#2`E$6aduLQB&BihW%Gs3V`nk^CmA;Lz<5Z?sG^$VTODL~i% z=H}_3`^mhgI{svY!$a@vaUF6o$Vf7LdxufJfh5LNWY<7|+S0zkmn@IYN6}POi&hra zldzu1pJNJAV9Xapf^dvDLz4T+hy*eXm#oiLRibrec+bQpsbp@IrRMeE6c?o~_i&

slD%SWd6avGQ7&4ons>Yne2bO<~Ct>_qtwzq+&Ex75nr{ZNUl#nci%j;-l@j+Cx) z`y>Tqgn)_P+l-^rZ8~~maP6jb&E|OjA&b|@I-;)>M#Fe~vPAy;W|=Rr)NJ1DnYQCF zs6-yoj;Gu(o0QGTp~tE@X3!2m&(aR0u4xV0kB9@g@GBw?EXII@2m)`}iRrfipjB=I z^)1LW&|AVar4L`(Zy}*fy-W?|Zj${X*sT=Jd-UiL^zg{ZN$sT5M_OSpku-^x7VSGM zmhf^~VgE{!t=e zH7j*s1pauXPBCsBTx5{ABXZfKTPbo3CLa8($IwlFt=^yi?Sqz}G5VF|jCuP7<@27x zkuN@gJMSlhupdc*Tjo!?nZqr^76Qs{UEM)l#Ex%{I~xc&g!XfEwsyox%RzXR=OBE+ z*W3H3aKe&nEP$y@aV|{}vbu@<4#Nhnos-$F;@jBLvD`H{099IS2T9DXrVkeL;Jh^0 zZ!sFWmPJi%J>8M@{%nkBqVwAaY)sV&-?8!0MH?zmTLUaS_;VlSC+g#qsdcUW(w_m8|W{oU_n1thN~v%1nL~XAEAkx+~)~;NG!4_;GsCd1mBE zb(K6#M1f1QAKQgAU`sctgYCZ{nrE5C3&jbIa4UAty%9*zBo4nG9COSVcpORX3IWvg>cjtWWd@e(XW44+%$*mJv-ow!oUoHSXBFAQ&JV_#))_WjD=)YSQqvXVn z9Z3gAKQVBau*I|lrqInh{2o16kHgP5!RM3dLMz?MiP1)u@n@-3%J@!OB`6-Qqp1VW z9(3N5!C$BnY^563!bQu79_#Ql{zDlJWq!~0QveS(Y*bx=ZJ}`UGxhoTN51hS=;G3= z+0=c>?#JbahdG-mCv04f0PIfNylw&49iH|rHZ)>O2W1?d?J2WHvU)ZB)QVH5&C0jm zLuua0af`xw$daDdOM+^@1y#u_omhxEF`~J^i@t}Zx90xbz;exvhCRj=&Eh8B82rge zPEL^jSmx+)38*5@%vNBOD)3D*w_tb=HMchSTcKBk-qS9W5I4XzS8Y<%=gVz4s5huU z$H$9QOKmkzV2_u7W3sLu^*GL*unZ4c$Mm=^H_JfM+|okdp|s|?@<`I*{%`cXd4g;t z>$0c(a;#tWW%TM;48h;@L(=;hx&iWTmYST_bRv14qUR4NHF3XOWL$u!B8D{?T~KT3 zRY&!fM2XPvV3PItHO&P{)*H7zFfm;f0`omj76nC!bjXrs{sD~ANXH1Jc!;ubGGYEC z|6@Xwn6h9r9Geod@*a-mJ##v~Ji;QtLnPJDYA^W8?ogJy@pvtx6Bm0qqq{7Mz4xHe zQb}`|9Dnvo!DK?g?3G?(N>o^o;T2luT3OafU!}XgR}2G6>lyR+{o3VTEu_#(HtHZ( zJ-zrt*8AZ--Me@1hRTFIG9i^xToQGvvXha=M(2N}$uIISW|gLfdq1%+P&~{??!>(7 zU@wsz=!rGxW*!6}@R$M_P%kJzZD;3{%^_{vD(N0S=Uk309n^nOS(B~k+)3H^FuXm4 z>JX}>AR*2b9k+V1QKk&Xin`0n(tdAZYA|bMwbg5s51s0yX|Knh95*QyH=o!LM`&;sO8;8YO)OWzp8`MF+ZSIpaC+SbL-Jp25cykR z{TrS5=FJ0whu4cPSNHDbzKOdLPYC4Nz2%G(!?Z}C)X6pGW?{%aD*ZoH;>3t=mU$$nH znM$k8gHOk{1NTeUqG#vAtF#I@+^&6p)Y%seNXt#PxLD4{A z{`kbSD}oEZ-?E4;_&u7GhM7(R7ASTYhNFy#%Nd&Hlz)?3q<*jchp;1s``@yVQn)t} z$+e|&PamJ24shX35O*!}pKC#nRWFD;w<0cRAGsEvGZ2mo2k-wXKL~_M`VW|1frC=E z(!R~m+ukFW|4E!s2Q$RDTortBUq;R-g6D57Yi-R&XILlAgs5Z=mE3_MwT4Mqzyk2a z@Ls?)yCp=0z6=7H1!qPa&&r*%w)B|z$Cn}n-|`Zaoxij^Hkb=C)mjq-+eJcb?aMG$ zhddkYTr8Da!&nzBTSJk_QGELm%TOP!IHnvsDfw%Rtr&N8I3a}$6)yPzTkap+gyGDZ zML*_|;EgXq(Wb`CGKE!N%SiLQk=UteEuEc&q=iwjhT6aqfOs~`fONCG*w6u>%US3HA98<;#5AP zP3cYwotJ=PV?cG zZX0~Bw@1hKCQ!uk=3WqtGlku8*2eb+TIP7{8X;oj4h2$wP>N0U+wbjPN!G8+I=Env zA&$IBtqqk>ZfJBc#N5A=JI})CFp=Fk0+0Kl%1);PL_ zl!0V~(}dN%FU={{+aXM5E~6v(pn=FTaMKJ^~Ce54CKu)73~B&*Q6 zh^n^Sp(fJD;VJ3Iw1IMMBa~Tu%u~}L^C7bcH_3Yl{SUGabw2EN8_ha-WRU{#0fF#k zpT02BW{2hey6Pcx`?#KKbRQ-Fkx)mmI3Hg}vC^mQlf)QD!n|9G+XG#kC6nA;BFof(2#-sMDA=#ZXEHOPcCs-36Q^eCZ2QHO2>0!G>R1@Wl3h{URU@Fgl*i>m4E zcAK{eYP(i+e^bnpgZBPoOp_88J=napLQ#`EAO&+lWKudD<1?Pyugf#+=#Ej+d`WTc ztP-@S&91K7|3Ue0ZY(#~->aYXF|u+Fkf|b8{|$Cx&o2CFa))XIf@h?NYWp}Qh`u{*St$i9UUha#=Awd{^6o>DFGM4WhdQW_Qxql7FVWW;5T#i(!KEW&O=;}&}~ zn?&SoL5OX)k2*Z6gHl#r$W~h(^`whXI(Y=K!u9{Z3i1-J5LC z9zUw;JVZ(l9aVm=1BGhTELBtUg>v?`y4x&Rz{%ZSY2!Y}X!ZAwEC*TE1BIR0rN$ul ztZj%qa5946cR*U&iMt$p3&Vb}t`>%8FF8A_kSx=I)czd&318Ufb@cf@%qdUPJ6W&Y z^t%InCYN|k;?{jC_muPw+g^7ud)`H?yoyuAxrQP6LD^7BGP?FbPV+M8pEM>^M0hmO zNm0uh=d)fh_P|f;4$Lzt?q_@2=#9Vu$tHYt_;x%IT@d+BC%k7nsxHgJJ`U7b%Stsg z5{k^29xJ)^Jwc7YI~wCbaJXb`I}1fc7wSoV1Xz^G9l-E5Rkt5JV8+RA35q#*CA&Y4 zK6qWgLk)Jku`Jx<&3k57ug)lc_QV-TMaLmiNba+X3Ri;u5LKH=rHQ-F9O>8))bU*q zeyLDG*6r3*0u@nAkKeJ5Ep#5)tYbqCFS1$U)9|_cNqE*@zzo+=Ul5=|dQPo>BAIAS z;>EAow%pz}v=t^+o7u*W!N>|M=5T*My;9Q$n7!%k*?B{jXP-gb&8l|a4fNaFiTf5L zwzmulMwG7V6ek+uPE?wwuV4<$W*-&|dLl(qThG!bT1vtl@o&NB-VhM5i4O$4nSEvj*#jmP8$GddGW|Dxk% zH_C@2z90k#pN5!ZkQ|jdJpPa2yeKZ<*UDZSIu1;!ifIa)LAjrTw{{np;_tcXy2ay zWVp%yFBz`sAxR+*tR$$RFXsU@ti)Ms-fxDZ|6(|YX@;|>b9e+&dzT?e;&SFjbwAvE zna7PgKfNhg2Gs8asCdbTARPQtGGnX0`)@=c(A*+T{0J!2$i8;XN1eFPGzJJ(Mxl0H z%$*mCfmM1JTYLN$C5g4v04fm0pB!UVqDeN$li6h~Fz214EZYA9z+TH_ZoqW-ix!cV z)b2I7q4C{0HaF?Lly@GzeF?OM|AzD~sZf`<>{%^0G=+qeA9rsi0bSvzu(dN30=kh8 zedkV8SCYp$&Nu*UP*gD_=&K0-6lTa9`Sp3VwvXI$Pl{RZ6bG3m?og{g0_zU*f2;xx zoWp0%egT-@D5kWxz`eS>-rnC`#AgVBF^XP55Bf5;=8pP_7f)|FWq7Rek4jCIR85TT zf3!xe)DyB@a>CU>vJAX(Z;B)6SLSyxmwM(fuL&W#?>W+V>*%+QPQ{rIL5B^->aX;; zE%Ox492!q*&BO!oavwmaot%@|pWv)+<~1(`+~rhi)qTyU2_L&A6rT`4yJ_|L)4c_| zSE0f|S9Zj&MG69LUtRSD8p+@!30dU(H}36WP+Y=%9bLNWE6+yP5=S&6^s@NMa` zq;`I|hE&?dDYj$@WC-~~O;lUzXQ5N0JGHkUSaRt=5HC{hW$00Mr;Kwbm3LQhciu}L z&~$Wjv#x|OF)?bPVpuPZ88qNJ6Xub`GM^E+;ljlruWOKtg77nXi0+_sk}`L>(zQxF zt-45Bgyn9ViHrWH9T#$64PU0bvQy|6#Heno=ihP^AAYplFXWJIo=~@stY6^*L|@`5 zhbOjHlTwR0E7_wJkm*K1mK*n65sB<}4#rzBrLI4B@kU!ZZywY>ew3z~q?!~;qblOb z1eeg&Md`%#%?xqal_&(5WKA9=!*&q5SOU|H3*ky-xyBe+y%$H%uA^ifC%X<)QiA9^p@4&vqI2O%mrwA{ zRDp6?CGVJE^9%akEm*nzoK|Ls7lOT*2k)q8GDz`&pkdc`4VF zOBFvmYlue%HY; zOoqJ=#NG(Z%Yn1bZn7IikT}0n&qV`j0s(S4BJ%MQnMFL~0ckZnBa)(f1ylLSHm}FL zb&~TwLdUPbJ4i%q`(CRK8&*<{e@GIW&&1wR$@G(jbZ*Z+vR)-bS;9d5KIwr0?hpDs zoabuPo1ICXY!2V(mi)+Z!K6IlbFKQq%uChO3w&&NkvHnzc?3SgL{hytrNL^w`I5Vm z`qEIKNU7z2B`gHXHGA*9jZpRv6noqs=QFv^p`4`vR}&T*nx;1gFTX39(`rlb7lm&e zondZ6n)w`d-Y3AeFBYg;XsmZ#har2hLG4bS^>e}Cic6uLhcF@AJ9inXz^ZQZ2fR+8Mp6pIf|t1 zD3j=d?<>BalBBvMu=&gC3o+4r=U1Y2ChuvJpilD1Z>Pm1_t_ELsF;$fLzO90z^u#6 zyaP#aw^IzCGHY)9z>mO#4iG5hPB$f`Nzl9e3Gd{ek`$)Zo}Fxn90pEG`9yv)v=V)c!EdE-r%@%%utcr6p& zJwq6TuYBcKA_jnGFax>T9jbZzI-#kGe{~H;5#6!o3pVqGnd1?bG0P`ofWRqqgQ0m~j^fZ&yE{YtBStJKJR*7V!MV) zV1|JPR0mm7UfcJk2PbrNHH+NmgfK+r3b$`9t`LpH8c=C+J4a!JBUTuXOHN0R`)8;M zzH6IK0&{PVhPps%M_1-%nr<($ZMLK%Vr=MN_hfHY9J>>%Xr>MseoiOz=T+<1%V;E+ z#it`IJZ->nB7s79mIW&6&_Eaz8J3-X%sjNMi80p_tcwwfy&4B;v~f)Z&BwtPQ(350 zF+9r(6aq-SM#QhQM2%L#yMjg_{x_VfWP%e%K)~Cu;h)S!mm>7|^fl-c|EZZKNzXa4 znuXTC1-$=^=l^n6j&F^k#jt2l1sQg=QC-uQUJI$cj__I+hB>{uX=GD`)F$CKV z>V@p-qkYn@^`8Rp+Mg%;zD>W}zsy$pW`EWipnoMj*q8Kw9t8gL!M=Z)?Z3PXc0vWO wht%HCJv|#6aMke$R3i-Z6nL6!i4WahmAd-{9N>F=-KJ!dj!?%X^xdG7Q1 zexKZQcXibLbp59g1nC|xUMFf?%3X)gma6_!QI!3jh1aJ0wtHGM+`7n*?wTn<3c zqon1^bDx0gjR8kITp%dc7=mcmA!r^<(S{)?YC8mt`a=*t1A+`L-K#ln18yw)?wI2t za0Wwh@QFH_&ZQ$>BnVoruRbtP=Dp9sq*mln7YD6TjD~^vp7mKeN(frH?dYKcCol7) z0^10FTef||yPB5Ucz<)qft%TTGjX6JtLVHT-6! zXoiQB4Sui=RHsSP(^j0&*N132{$FM%x8}=S&mfbY;13L}MQC{ZxN^=t~|IY^Htt>Lc zChDICzi|utjXG5s(7dl=V7JTrQ&K`U9aY}IdE$Q-A3!ff8>Fdh*^wI9NGSYK=;0aO z#X%NU_L@+;Cn9}1oIObAON5$`dtp6CtBvfCR~ei`77z>$MnUKB@$0>`$|tKM6|*_i zl#VUQLuts_AY=z!l9k0sdG|XD?Xj}DHWZ=(ZO5!Ivo&zy|Lpmi?Y?c8Mo;M7&nSq} zm_)+vYKMjygrYMt!L#!phGu3qrerTAwSW+AMD|hn@{c0l#Nkp0+;QrZK6i#fvi;4o z-A3g3j)e~#4>S^0S)Fu=)qT=A4XB{Azx9)HY2PZ6RFlWisd?E4mC3H}iO1dXph^|9 zr(m5rO{flBl}^t8P43+`+*c;^QMOrB84Mwz3fQedYuaN5tt<=G(DV3V*45z5khXq zVH~ll+GYv$<)JSk-eUbVApfVnyyUdV@|6AK1Tv#5Ag+*@0{{l&@s53U<3Zh5EL`sM zYbxi1xdO5~`n_Vs;>~~h$Tf+3@>Q>BV&bsp96P7Qc;M@wlp-Pt2`CY+tzWG6)bXj8 zFE6aT=bCjJ|D(6{#=q+|*W+4=MTi-3>O<+gPIiukQc1zL)NJNeVofVT!LkG;sc><> z^8l92RLfdo&6@6ROsx$wIsX1+I@LuX_E;?YWbx01Skn}xh(!VEVY@=UW_?{MRXd-M zlRW}$D*zZ<0*rgY)Z72%D^TDfYTM(TqtNTGsEYKD0B1l6tF7-^SlWvEmY_8%Qd!5&CxH z94`7H;^gy8GoPoZ{D+xQ#qUjzQ^_xIb?5(#k@aJo5Bo)r(sL#WuXmvw6Mu#HH|m+j zT9k^$mhd$pe~sQG^TaU9wsQw(+c1MX9a$!tW3<+>NK3Qf9~L~Hud@Q7MYt?xZ3C9j$%3j{&Ob#jZQHhtDL zQF(MWJb!_cH+QSgGIeY@Yb$jerTYor{yOljx%$=KZFk~}4U)V^Zp*MN(Qr~mw0D@N zfPvF34@E{K7suqRg?UT4ioQV~vSTES8B^k9+Ur_H6ElNbu9qU5*0*?0FQ%gFZoi>5 z%*^wP5a<=2p=L3qsmpKnTU2cMM#k-3XQDXLi!fa|*xxSMG zeP^0p+Va5NNfn|<_n-<>fc`?UrLz{+0>=#UVMZj9)!kRgtxk)lGdLFAEfwYS!7+4@ zn8tP>P44si>xoRAr--U3jbjlZw8+|zdW;IEmD1MVv2o!{&E6LcP*3st5OO%mH2>Vo0d%Hbu>h3i354Cb1Pxgvk%Z%EHWFYR$Nr#>eJlfDMZ`J9`@X)zvptwW zm%pD3WcEF9(g9tr+1Z*kuTsa;7bMVQnO+}Y~>>DA5E;(h~COQ zs0|y6zYGJOkdHOZSdx??AO`*#pTFX#q|kA0kGW;iz2fRM7JVk`xi{JepVkLUXoHq$ z=9E#~5=1aaS9P_`jaA-Blp_o?A>arvZe{(IAwvmu_xZ(@b*;ul6w1hXJZGeO@cVy_ z?+fkyR4r6Qgmlo)O-|5NYylpex?Y)CXG=br@<*j;FveP$0q>CM`&D5ZRhgwgjkYQR zy%yD|jVp|bxuX2+8LP+smS>e)m|KOwvOnjTx?$)fo1azm?7(y(2v;g!+@WMg%#BqP z+kUn*GhfmV-pJ~NUjpV_M(vyP*FvFkS6kfLQ<<%2~{TA6PcrvS9YP@pA8Y)0QqeDQx`YUegXb|?iUe~bBnWSiyk z;)LFk(|N;YS21eB%!J}!erYk>o4sdiltzWRbE38=l4^lv*)f*+`0`G3hWuX z&M8!pnNg1AAzJQ10?%D92e#te9eflaOBqF5^QYD*-Z=fB2e;^Tm}dOl(W=~ zI1(fU7*N?s>FVZXdvVdi0;7s*V{KMLYQr?sWBP+=qc-aV^86WQ8$r7yc5$*4Yi)08yCU{_j{?+;2+rX zkj_v1$)eF+-`%2JNo|B5>Qv7tJEX}kVXE3->q-ZXuQHO!?6qM(v)zU!+5ML9^{Oj% zjv%&!^l(*$nEpRY%8 zcIv!)eq?rnLw7x>Ei#JV=_xc#gn?k0^NoNmp0~^jEs=gpvrJ0T8nJX# s{W5kv9=jea$bceq)qTeQW8^xa*q*IJ_10?^cBsP~b#OhzwD-UIAGamwbpQYW diff --git a/src/main/resources/mobs/icons/examplebossmob.png b/src/main/resources/mobs/icons/examplebossmob.png index 471e69df3ef160406c29442b51d0cb79c29b023d..068bd480a50a1ed098746980dba2e0ed7e5b025f 100644 GIT binary patch delta 424 zcmV;Z0ayN#1Hl83BYy#iNkl@aYpt1_Sjl0`-8+hPfQw#s5JH7J7P;P-3)%V2Yr%Eur=WBO!VV2&SKfK*TYU8?gYl(VDLo STC$4(0000z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;&yra{vkl71T8V z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHS801`O{U4P_)0002iNklEq(rw{N3JrmCvq)r_v5{w_ZH;RC8H)J5nn zhq)9(J>6Z54sl>EO2r|L4-AZp7nqu2%~>$5BpFY7X_=hk365&!-Sn)wq7c-*Q*6?(Sr!2scp8wD%Rp0`I+85Z`6Et@*%>V#^ W9A6E7F)!u-0000K9KZ$-#}WnJ|aIUCx5$QU*!@$hR=_ z80BL;KxTn$M0e>!hHrT7k!RpSkq5dQ#%E+=pb@~DDhRn0=6@ELI&_zVQ$Hix+uS9W-NoF|Ss6|(gte8G7zH;moRu`hX9OhD7>gks%(47l&5xR@`^^z9M#UHlUm#-Q>TR8OJQ zvc#lQkjv2337KpEd3W+E1J!dLuc|BqsGS1BAQyw`yZ>Br5)8nIWaO5Y0NcpH&BXw8 z5#DA1Pz~dcuh{EFjFu2A@}RXXq4otMdJ71qpM^kDV;LkjDFOg}*^X(NVz@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;&yra{vkl71T8V z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHS802Lep7=NY$0002eNklUmow2cjB&a2A;UMkn&la| zP+cCI2@iAngfX!$MOTOJa{8qTVqHp%59#CLGt*b1NSb7ZGp8R z4wwc1$jNyymp)vwgB2?EpQbKmM6a!hNvAl}{h?2thqF#2(7wRdo}jslX$AmtF;d2; Sjb{!30000G-8 diff --git a/src/main/resources/objects/examplebaserock.png b/src/main/resources/objects/examplebaserock.png index c7ddf9211df20af773b34bea5e628b36aa17e8f3..1dd1765b135b6e6df45cb8dc77e83841787f94d9 100644 GIT binary patch literal 2183 zcmZve3se(V8pkJ-*F;E20+ca4LwKkQIEC<1AYdSgS{{0|Ygvjz1q@WXzNJ!SA}xZB z5Q+yVwh}jptf*C+aztbqe5}y5l^8%2aG^d*m4_|}R)pCb+1;MqZO%#N%e^z-z2A5L zzyFz{qy#m8o@5>bLHy;K*wx@lfNuxF0l&A-Wko;`lCV5BW=&4&hSxN$xVivslu~lZ zFS4a9l|6J-3#0xlR8>!yW3gpQf_=0l_&s@+*yRqL;@h5*-MGOp95>}B-QBUM?|EZB zJCRIk%B3`!r~cIG%FI}NX?M|1?wO{H1*6eftU&M~Fs==^YbB)r0vq2tUr=_kcBy{E zyL;cB>FV;qVz=(0=R}|Y+J+=C!`K09QZ!w$!aRjrrrQtax#`ns2N&+-e%|bLH!+d6 z1ooL_qDVJRT&S>|>CoeVYWdN-3>7TqX2n$qeygCBIjea2g3#Ryx9g9<1w_~Rp`C&8 z!|KKD&w3OdZ!-cIs%-)s9fTTLXlXcJ<4c&24~UMzDtk%`<0xAe#!{^$%wpUGq3u#C zP%@M6!;-`VFpBLv^R<%V*bvX@9Me2>Fx;9!?+}&@w<-;c?@pcES}7;Xp?rCl`NII# z-RU-Oim0K?V#p0<28wMGpmQe%JF=En-Dn?LJ<(b=tajGPE+dk!_%4Gyd`HL@6ARs3 zaQLAR-OL1OjNX(+plg*oVYk(u>YrfM-#lL(pzuWu)ho?I;$>cv7f~7|4%g4RkFd!} zSoPt3^H#zv$39TvU~^|+)i<=(5u>Eo?I|BY4Sir{6km6*;LHWrO-!RtbL}HZ83?$K3C$_m`5Nw-MWgXuHW<55eel&Ux~5VZoojuH9b-f15buGW@g62^L!S0x~pjornmx?f;8q zePT%igOFl-HndSb`Q~v3s$%pji46HoZag9nd$t7gI_On=%rWA&*!y%mOn!1H$F-3! zzXy67H(<~}#$2X!cgPrRs(#!qnt9x5Zr0_WWW8(|7=bzJD0S6E(0ovU$D$&{qP)T_@7PkXam z86HbJZv2?`NpdiupJ83t&^b%no@;dS+4Cqi0tk{vQlr8;uWVMila0o~=8E=V_B=JL|#`2Fl--$BxX*%_bkGlq4 z%oLhmtV*jb9culZ)-iTiAJUg1eld8^PUW)Jvr#$Rxy!w$UwPJvxLnI`C>J`#ugJ>Z z6cD^(;il1=Ybn`HcIDagb>S0q(y3C7e@tQj(*qv`oXBMr`HPD9xnMeTY$%LE{#Yt# z1h8^r`rn*d&Ou2$WaQzdB-eK7Ty+2?P$*C&&@&M1T1q?V4gj%G01z;miI5EppjP9@ zdH)vfe>99EWC#pEHO|iign*kt2=x7HSo0aBU5uNYu&Sr(Tq_LM01S@*hsX{OEue)0 zY_p0%!k}4z92fv0*u}4*#zqIg014%HN&wRqs*tanQ|0%w)_qH?U|Xm*Tb((?%A9-* zU|}r;gnVli7777#ssU@x2POZTer_d`5S_D(?K9Su_+*dJnie?yumYf6Yh4kH-h5+3 zAm{k(T>2g%F75v5`P75wi{7uhD6EgT6qWL%?vWtnvG77g>ZsQpI(jPDbDFPBhzwpm z0-pJ_VjmezhmzfpEcA`1b}A$=kQ%yh^O3_o)yG|7Ln=j^H7>x2YIPzBY&=Yfj!mfT zff1wrGwN-~oDa^a6x`tqTblq5ZAM)W)d->Q57}2u`}$Ia!Mi(Wb2xhntd<8#m^);v zT|w*Zk^0@*;7Hq`itb6hbD6A*Hf|=Y-TOsxkAucD!9SeFG7OQBYh|+eiBA#!0)BmtY67%V2$=ZX(Tr91x-%Vrb1&K2|5vjDyA0p|PjD-V00000 literal 1078 zcmeAS@N?(olHy`uVBq!ia0vp^4M2Q>gAGWY-;ikoq!^2X+?^P2p46!aa#+$GeH|GX zHuiJ>Nn{1`6_P!Id>I(3)PNdW7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXICJ|AhslL zcNc~a48;sw?4nmpfg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHic?2q%-whHrIs3=GUw zo-U3d6}R5r^_?`^K*Tk1VuiBp>)4GVkC$#(J+D$#(}+?0Ez6nt6DEJ3+Zmi1?O!6h zu4RI!7tk<e9x@4IGO2ydR(=)Bwb zYqtWUio+XY1uLNj1_cHV2Brg63|vL8*H2n!x$c;oc7r)*!+tdfLFSJOUdFFizdDiO z$2*(VJni zg8&0qUVz~mBbVUcyd|=Z8nYS7SZ6aU6fv3nT^)N+h(S>Jz&r;|1}3N@;M9-*70drJ zxW0b=Z@!fZ!#z93S^u_PscTsDwY{Zd8jFwvlg$SC1v>Ni8#Zw^Y%=*f^Zic-15Or3 zfn2@^+Z!c6-gb~+a$q=fhH=$LiHU`(Sy^w^o|E9d`adq7iDmh$nWkmz>IZH`mS6t< zc-Q84xp(e$MHXLtY`=OR3n>qvy*`0K z=)rw2h8kb(yYG+b~~7y$x~BY5x-;5C>(+4QCP>)v^_`~PN$*XF+}cK@23`~Td# z=hwC!zon^e$zwL9n0XwQA9ZVQsRqy>d@3)Ni?a=>s zegT8Mfq~<6s*T%{E%o|$M4Wf;eEmjJSy^sBivddmg8{n%+i{yZ&IAs)F#=uob&^wF zSCwS$zW93n-g63!O~sP)YznUktA|Of>JUxloWXOGumBfyuGf0H53%7p00i_>zopr0M-|bP5=M^ diff --git a/src/main/resources/objects/examplechair.png b/src/main/resources/objects/examplechair.png index faf4597172461331cdacdee44f263b8b04885188..24218ef5434f53c6d0f52eacced22b111561ab8d 100644 GIT binary patch delta 1027 zcmV+e1pNEe1(*nsBYy+mNklXgRz%&~9BRd2}69B_#0vIg>7)Gmr z(L^vpLtwND7_96z7)Gmr(L^v3LtwNGFxmzf&H1BkfYCI-FxmzfO#~w{ z1XxDno%j$4VDu$Q@rM7KhfJ`JCIHf-iqP~a<50>}gL*ocP=Ai2S5AYugPDPu;V;8q zya@oPh7qU+oCfIaK3oY8pF4&^34klW>7CR5gCbUlK@c3y!cZC(=x-U`gPHFb-oxBW zclY5)0C?RoIu5`vvcZsP1Hf|LJ7`WDsAz-b{QC?KQJkR2AP*}61~f&H8cw5)Ks?c< z!>9?Cm1K});C}`}QeyTi!&e5F{wx2l587g8GyxEfn*Yxko->#+n1WTp>Uur~J`|lW zaaaz#!*FMy+5#jcHj)y+aO?*&jJ5$t4uJvb2gB;#focPMV)!&9k_xHeL{tfY)(^%T z5TL#?O#M(A5gVxm0V!3$fcAsm4sh!qmOx;2KYEFPOMjf+35(Ql8r0(eUl_jN3_pBw zBP};m5>6!d0_fQf26^lS!wYbHdNR0y=~E1+!88v8&w#`;Ea#*50tT{;KuI{^O#lPk z4<0Q6h)d(NE&&EbKNx-FjgHx$k%NJq;ReG^aPLQhK^<&0t{e|k2bKrwVf?}HlkTMy zso^v_5PyUxieMvnu#%9J*aSuXU>*U4`Gx2NfIj=hFv7uLY6DOb6+^ZJ-~o=!dojA< z_4~H}yM{$FCFD;~SbPD_J&@yBq7%RX*1=f)CBz^!AS06B7`}ns1EcAf!f^T*7#0jP zPXI$LD}m}@N@wF>`q>y*8GbSRCMgI&dZ=Z;7=MEp1E?WNi#QlERRDU9lVFg*8y>*% z0&301{Rc(99D^*_K0@}w9QBCdG1csU#qbI~8;Q%)K>NrlnMPLxF^p_5WJ&-o2Ce~( zDPnUzOdTvo(%aFbhSO*XFv3fK(F8D>02oHW2o8ZE+Y6xg$mBqegc55%spAu)RlsO3 zV1H>#i;#|kx*)f|7Zy?!b^bB1TdNa7)HSe4uR1U zV6+4nEdWMx2v9N?up2Gls^}b$qxPc)IDA>*KUxKh@B)yS5`dAHffu+t{xdkJK!lvI6-E$sR$z3=CCj3=9n| z3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?e4kz*hIH*ONp z@9piH$~fOyQQ?Q3UBi224zYcW*PL1!R%{gyx{>v`F-76#Oy4<^zy7KBE!cchvVCU0 zUMCCCdivGOLK~K8U?9iSYC+aVKKJwLB*5lLq_)P1G3%Uwt8A<@HDvhk~P=54Q5(D~x0AF)7mA9s<^ps_yX4?#8?5 z5}`eo>(}+3{hO)evpX&tEGN32F{}Q4bGww+q+eE3Rqnp5TXgZ$kmE$jc$D0lgMXy`3Jpp7@P8%nOq-`%v9@s<)}(`i43 z^_CU0_W7mF^SJlooBy*1`nA4$u3h@_f8UF(SBpIx9$0vggFIdWECu|917+}GA~(u z`#oDQ{tD`2r2A%!?MBhM%~9x z>&~D4_*W7F*CDj&>#{!W_L6|m3$rwS}0|5V$* W{J2c|8>g2nNXXOG&t;ucLK6U7;W(K9 diff --git a/src/main/resources/objects/exampleore.png b/src/main/resources/objects/exampleore.png index 7250791c2d032e46477da3f5cb6c07a35e7af563..d56047fa6e5ca345bb83c97e3526015b9c0d2d85 100644 GIT binary patch delta 2076 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 N07*qoLz@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0w5O27Jp1K0003NNklYyd2Zf$PF5XLbw-T{wh?}`!+`yj)Ta8aTS{PLB00000NkvXXu0mjf$3d!D diff --git a/src/main/resources/objects/exampletree.png b/src/main/resources/objects/exampletree.png index 769e6eb089db27237c62792255080cf17cd4170f..31c3476491424f5e83458c2f0026c4bad7b3d737 100644 GIT binary patch literal 30999 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 diff --git a/src/main/resources/objects/examplewall.png b/src/main/resources/objects/examplewall.png index 8e1b1337199d9a4c252184757dfea649a69cdda9..a77d7c6cec4a1491dd97ddbfbf423ea04cd4044d 100644 GIT binary patch literal 5257 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 jlHAEn`&1MG_FZU$B$1hKKHBV-Ot z56pcUMrsTxb%_{8y>5C17sDHXP*t!z&xmFcyet3(o+*PdSb8_ZUN8;v2m^jVY?p@- zhcPhm|G$8#fPaU9hXLdnP#_Xe&p;&jf8*RO1{)u1uyRRM_HpqK#}j?X0Ym;tE`X7mSZ5o8c#_z#5GVs}o{d=z8R%NUR(tjq(YykBTp2$z}c z4D1ZBl!YrL!rUjxAi?mR;RlL4MC-)Bd{G85hQm!qaDRFoX8;0Y_CF|4o?LqhPS&uP zftiGytO>;M|Mv{<8O#|>;mR&HU1E>|IvOMZDqleKLWZSad3-4ulvP0~52P@jAsWS6 zkeMKXPYjrSF~aaB!LouY{2pa zJ%*8OLx1)-10Db?OWC326uMIVIz6}$dPHDL$uPyZ$~urdvYE&!246}9rFL9q+Sb{C zt+i#aVd!b+W#9de2o@>Yz(j%hNTz{1~s@QWJiJw1xW}q2r<58c!w=G|NjJ5 z`9Go6KC(HWK!cSnuo%W;4!YTlK(!3O6bnvaC4UTsV0q+-K(-5CN(2Sq7huqU41nbT zh9*?&^o(>Ft?VrrjvYLK&5sNi0my&{AbT993>0|SVg{c%pz;M&wt&=}Y=UGFV$6Zr z11r0b?SPe8ATdJ43wmnaY-J!LFuyYZQwBJOK`8_l%g7#oJ^Kyg zy?^KT@!1WpO=THmAStE^Q@OaL7~{+5uNcSxj6l7h?1i^>2UcK=pb89&`S3ClQgr=d zz*j`zav#*NGZr7HB+B0e%wr@q1;Z>Q#+^X580JG$AUpsm>Dl1xiGWfN zPK>Kez!Rdl%wnJr;B_Yv?xD!~(II^bV-^np!Sfk<{2DA#00000NkvXXu0mjf9Sw-{ delta 576 zcmV-G0>AyT2iOFVBYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHU>0w^u8#D6ay0004;Nkl!6@*P!zp@;CdE!9rOZG6tr#8HiE!5;!0Q1BPbO3 z0@GqR!*yn^GgnPT?zb7ep8MZ(&-pzH4u`|xa5!ee(ySAhcdj`1_4oHY4b*ITy6G$) zE2C~q>&&D%pnvKEe9-vUbE5Y;;Rt|7o)hCoF5sea?On#!da7%Tj}_3=>1b!l>;mw{ zd#1#q`%RdfEcr-&Ypep0BWI|EAGbV~S?}D>|^|tB&(0`{-iDKxeG$%@al7}EgYtjPX zh1WzKD+lTqABP=pJA9qB5jL8`9Fvu*R|%d}xKX#pxL5(D!+Vqg*kUs=Nf9-5MrPFy zzcHPj>V~r=t;12YI3J)+Ez}hBv(lU>`GwyAWjrqtD+lTqABVhGAWtx3nyNLno@fe_ za&;Dr)j5^f08sa8vCsF_q1CUPuBVgLdA7P~`jh%!naP2}W`M)tm?eJ?c!^`>JK{3{ O0000r!QCK&`>orY%$iBnjbF zy&=Z>s^s5oLrN+1;6+NvN{WYL0A1Zy=|l%O$ZZvqr{m|13+b!Nwr8SW(K z<~!%!|NQ6szW@IuAx=zp^>T$Eh`vA)lLSEoBmOU>QSgyDI}RU$AfL4hVj>r3r!Kbv zATR|YbX7BRbUC|c^g#1tE4F8n>kBLfAt{eDTjY zoD*$lBERD={9rUrZY-kV#ZFm0XEfyaQWL_O*_|@4ksWf*rkh4|#Hnh`M!gg*ts?rw zTE|2ZUur^_W79}uF#c##l++QYU&h3C1r1xGG)z&9-2mvU?6fm#2?1mta>?SAX$D7{ z^=#*>#rQi@q^X&yUr)0&rTLg;Lg$a+Zy`ZOwDcuZJ#eT)lmC6$%DaFXsddYqOc2D z7#BSW!QHH2Q8FkT5kua#cCdDN zvZfI0w+74z*@#q^Tn%iK*-QFudM;$R$=S(74&M~a%~f)1T(HYv19JC;0XCi|wB3E4 z=qr>}fLxrsJsm=T)b3gQ8f==5bt{T*aW|5sl19PRlaX+f2WkrR3nPniY|GHynx58> z7PF!isMnASZ-1oZR=dEjw$TO^)w}Y!d^_t3s|~q}c8~`8)4HH7_m&h>&_6;n-?2n+AeY%rGTv4syU?*?eb5wzkPyw26g?9Gg(?_>{-V&EXW$0Zj(Ci|1+8xPaH^zv4`0syFZlg z&ZO53CB1F*yf?ebdGsMx8Ri$s6qVyNFCD`G4vv_J6uHm2^x5T72W?MC!43f&(!P;X zv5sG5OY-z1&7n=n%l z<{1)BjS`vnr^Tpo1~WK>q9@VL=w^2+ZtS$sRue*hL?xj;s7$piUt(ux6kRXXC{|IZ z6zOS0l?|5+wnibPVS2Vj5(_Fb<2(W5;5w#$CxfQW*~mRQ1-{)}fL7l3)Cu=RurV$4AAvf>Re)y_#nT%3E?I#pGdOb8+7zG>OV_~{U? zh^o)-z-uu9D_>L2LtB5dy?=#9v4OiNl|;frO1sxKrm~|APi$78ld~4MVw>PKH9HR zb>9T~c~jo6`pSFMJJAVCi=0xCv1a5@L`x^h`ICf=7l!AKa&nIHS!Tg!TjHfG%|M+u z(F}n^qq+Lj_u9vTCD3jyT#X+A~rrQgNR@+S1^IE~u%tdzfF)F_ve_}1_vnspJGHr%iM_M(t z`*>(&uziJf?%}gd4%qy-Aov0IW3sedtHd6?w?&o8m8uU?`R$~aQodEq7F_7KCEp8t zG{Eg@-zucHPjFi?my)AZ(i_>Gl6!>U`=~rLj}XPCu;ri7($o##mlaO?uwg(Msl(>o zdb2fYhg%iH}3L(b+~+L{N~46(=5#JS$a&7^Ogm__4TmLpK?d{3(-S*gH%!jt6>wT zSdh$(+8nv9k7fF(^1H5U?mqvB@Y`KGh-9I66m zqc5Wk2Y4}Sk;6utSFtEL8{59GQ}R3E33W+6%!a?=M)+d=Y)b_=y5XO)Bs+bxYo#*S z`_9UBl-S}!@a3a9oHqA#<%zAui{d4!$bDS|lgUlc5u9Y^c=Sz!ufwk)sby`MsI!?v z`m3Gs6a=caW z;+<7^=WB01^__gvrU$nq8bxxswtx6l7%a*`8)y29o!-SK;Wv=Vzdd9}M)+H22PACS zTB7pi^Q>7hJXevKGh>f9CCsTlKs5(YV_hT_<(zX7Uj~5<*o|3!lmr60&CKZ@(CvA_ zsW%|VB)kdgGCcwfG&O4WE`CGrfg+^bD9pHzIU*>H;7nYnvuY4PxXBO0O@6_sfk5bl zoMDr{?*GT+kGK6#vmZ3xJ~sNJVdStqr~hk@_BrJ}P0E{8LvnoHJTDiYmy6GEM;L-G Xzq=^ejV;T9@V^D~<6^#vk{0|MRS&^+ literal 1491 zcmd5)`%ha{96uKa$Yv3stVb=URU{k&SOeeQ+#47hvJu zKGM~QBQfWg#R35K>J@0X7x#0lx06HMyZyYakUBK;TUQ_%A1mAbzAn28ii#kdwWL6S>VW5^5c=I(X9 zE#^D_kca}$h40--({7e{Qi08y9zAx2Tq{V!FO4;N^2Pe-XRsr>Hn})9> z727^reT&O@64GOb+zS9os#=-_ssinFiO=3$jVamv3|GzmW|})6v62TC4t}zFX9K;4 zon0P)B8q;$hc<96kvO}vwtIH;-WfeQ$v{@b(W6y{&9c%kX^#sh-HG&Vosngxh=?Fy zsfhhbF{2oC-}sU+zfuZ-C*4=Y&yJZDM@YCXp6!NIU5ynAdOw%6-X!UYQ;{hwi(>H8 z$k?XsUp*goUH+SNni~;nPGfM=gzt@DG?&q2y(nKL>5^Y$*_5aVTecKP;uF9d&W+S^ zwb)f=t91^uwQ#N8n}?|RG?H8SbYDc4jRoSE_%xXNj zE;R(gMBXz#t_*xb8RT}bwVU%LM>PEq43A(gy>bG*8#qkgo|>Ijhx#6uf`(W@`f$zY zRNH}=+tGOZxee0C}%o#`SKzTKia?M>=xxfA4>Y^R=3Z$Fg!HmzGpI;@4)8nd~% z)8@oQo^O?9|A z=j{IH{NMlmUv@2DD)sgZ^u*zC-Z8QTu{aza#{NW94{T)GzE6+gaI6h63&g8ZUXOR8 zs1SJ(@XoX{hAssGgvEvxQGo;l7kqr-g_0!GVCy@=CxbA!2A}^z)-Vct$K`@Q5?M>` zjKGDtaXG^{7wqte#W2{^LD=14m_YzrBZk&?5A&WUNzx!g(vAp{BwFsiEYQG8GXFrQ z^zmnG5WB@OVHFhq0gs_M+!GmdU?p7@aa@m0NY%P8Uo(K?IH}*j(C5LW#)S3Yq?ew| z;hw070sM?T@Ppwv`7#Lx{}yF6XE@~8Qe(ob>=tF<&4CB-zHvo4|d5V69aZ$bsxoZkSx9B8S zpF|9|lZ)-OPt)XXPVOZ@y0U-xo10wNfug8*LO!pOLb^YPL!60B(bUE9Bp-+iyO_u} z)yLjH6t~EJ!RsHY6ZDX_M%aH{4ff)s8|ke=^S$^^xoTn3{zfn^vu_qnXI3#g1eSs( zBKv``=Ww5liD&B6bI+nBM{(9vQQT>IJ3bU{@cx|5pcCgH6Er7y?O3E%Mogr|ZCa`zMN=>;g7w7`S~n%?DT|fU$e5 zS6qBEFM&#&lC@FeQFK<7Yh&-P`wp&g?6v)1o*p7igY%TZyLdRBU_jbmcR}=vuUUMV z$SN0<%+fAm?g=gbanXc0{P?3+MEf|(j&1yMTyUE}PWE_TWA)ur5Jcb!`sq0+D z0R&_UCSJ_ofF3>Ww}9GotG~1(BX#E^l}^1L(RbmIXo)94xiQ zgt7K^o10A{#iL(sd8fW1jh|#=D)z&;x60kfMvk6+Pnv{L%Cr$MNZmFkw#Ar6q>OS)TvASZ% z>Pm~5^A=Vg0#TlzNkRTbIl#HX6Gl%RNV-B~wasWj;cxY&J4@NG{ez|8Yw9mHTFgNK zQAp(!eykm<3G=2O)H|Gd!-mjUNrq;Db7duAK{7sGr{3(bL_?_AWWJVzu9P|%nWSuo zd0e*DF(oP_cjYN0T~KdVH=uipXTEbISyqHrwBoOC#uK9xt9sLyPs+d}>p@{AHy}AH z#nSn9)hvR<&H)TOuu0LD;$~uIv7OZyxalSeZO(R7ckWa?2ZFTPVq9p~Tx?=v&AjQG zwcQTR1GMEuG-);^?--fpcU#fI?s&lA%_9aO!J4!d_RRCm;Ie69FM?s=ApI%`Sh|FV z?7<(FNK*$mSg|ZPVOc#uZ=9W{;CE9_oR=S)+jU&?WnCwvIEM!FPWnZ%?COZ7UCX{h zUPAr+JPiXJ)=FW83DA^kwEV6lRi4gO_8zDHOeEbb zw8RcvdfS&~BA=(W_dz|Uq0l_`fRXPX)O3FkHd+df8ll-4KrkVANz<_OdPcE-W4-)mQ|wdJ4;WXQ z*e+#OvUHHJTU~@L7P#BPJgj-Zq*A_tEBcAy&?EdBE9CxX;5Dx3vM1x8UMa?kbZIJG zZmbcWZ;quE(&~U>ePh^L5okOI547EEmiGu}q(ZGTS_;0uhv{1gBX3l^?+veLK@}mg zT18v@i@Z@#A9B{})Cbymr=39!17$Ch^G=Y@*TUy;dpYGZN-?bsxHPMG8B#K9)3j=< zlit>+2pXKeS2!yTs#*h^18k}-<8<9@Sf*F2$e%-s(EUs0dE`s8X{+TAJ(0_{CO_5% z6t-=dj!E*tb*@IiwrhkJ4kr*ReZ(^o=ikpkDvMZcU1*MNDuQuSW7`T%LGb|P7ou_|-R%vzbnXV0_pCRN793_ptD#S7%e> zynyFkHaseoyuK)fUn`t0&^I&*LXW&}yp73jVQ^8h*z_tszdHbY9o0i2TPi4|T?rDl z$6K12qOTE{7?5ceuYV(VTNrQw<)xA2vKrwlAz1H0!1|*bIeW3d1Rmi98QRndWoRi2 zt%xHLH{!zyYfo!0_W6K8jUjKcI_`65d2HB(GS6rm92)?eJOT|@>2IOjLX?RW+qz$? zb-8gT-1@G}kd!ebdZXokus#3(!bY0`qep;TxRueWyf=7+-R;Oz6WPx?k>Am-J?lh% zhd=Z*ZKdgk6>cUM9duhe$rp9#=aTGHcWyIwx?MMxG#dJn+sVn@sx{yTX}@Yd)P+&Q zs%^gI0iCj>zVUbPFd?-{Q!CS{_kZmZ>Q)>#y61UJb-k$T^eHZ1&t%Z4}o4aH0 zl=hO!!Pnx`t2^J}f21{Z(X~I3X}c|3KZY5*$N%sZGb6kskq-u<$Q+U+^&zKlpzP`- zVEJA7an?>xm+l=jA}4y`H?D@X?X*p_?LbH7T7vlthQCLm`J)TW#mr@4AG2e|$&JS| zf_3VK20zaAED#Y~;tOR!MUlwBX@Q~TQK-OFT!6I*3n5eLV6p*@c4Hxu z4Uu*r(mHW*g8+hJN(Z%jb(=C^rVFI<(L&io3LS0n6)2B>yuGrI|L5Y)&B@96-JEmp z`Q>-2Q#mZ3&D%Ev0DO?dgmeHP73r~?s3a0v-_cG6s5qS!4~-9m3CLt)eq2f%0Jm$s z9626j{z`6QdI|t#K>!Fp1z?SYgmVC#+XH}+2S7|C0KR9XeQB?g7t~X1Rsta0r&-w| zCK1}%#J7q8@OsJZ5NMM6k)Y@MNXlVPBNPDn(3dOfTLAE2API5DK9rg!)Ja+Se$++Q z)1;p-*tT>tUd=Oc?%7*;9_7$@9%C$f^y8|+`-O^^?`KA5!#n49&}%L|mNG3C`PPdO z=he1g>vvaR@`-1$MXcn(As`8uZPqS1cH3eoek${rClKyUE|IkMK{6fF&>}PNu^6PN z;WPN0`8t2T*B^hPU&?ajShWBOF5oXvmJa4pjG`x;`1S=l-X!lGT=wf9?=w2lA?$*e0TP!h#5XyD37;2#gJBsq>EL*OdRa!z6M7>9GI|am0_m9HXB!FJ3?S|>j0m!vH6Ct-Fq=)6#0^)GU3Bc zgT7BOQ(&&eBdck1Umr7}WtJ3;%O5w?>8YDyk?{RBD@?C~K&&yZjPSpZGmrL=j5BN9 zj6WyqIi0^iQHgf^B^}oz-l>xQLjZi9!=LkAII7YF>5cxe8ds)yNJ>WvizG0sIpfb%M9?GeYo1sKS(v{NBWfNo0<7+=9;Q->_clhu63v|CFGi8CQk= z_`8O#PEs@K(EmjqmPPoGd;wv>~4p>bwaRZt}dQs@XUsj?=S~OXscnq$TtUQ-lpuS5(L|OEonwW^gR;XS<+X!MWJ3_ z!|jgAZ<(|XEMx#aBP%_Ufgh|Sph zx&^=AV}sZueiBwnX73}to8!$y#c7C`ChdkF}d z{+Q#VXq2}_DJVkzx+dStt3)T3(03)IP2Qb9b zuB+4%H#S6|HS7oJ*UXAD&paEa-^wN)zyCstioK~iI+Tqz#LTe6D`t$W&eEJiDDwCz zSKIrVR=U#-TwvKqSj^q0Qf{HBD^an9BUR3~!+T@vq0g38!mJ95IZa#o3izAHqD^&T zqcj%EmXeft{&>&ECREtXDIW@V*auzgXWO9&WS@=Z?L`Y2Tdy2N3tOf8tmF%d20U%P=0&RQK&RSDBs`(L zpAeC0b(e%}-FYA&|7)Z)8Uzi8 z^YgOh*|<)oRx08|!F8moU+K?~)lc~mIabs0vV*kg9&1;XMzLaL#d+u8lbTXOc+fP) z3J=g%cJNX|lNswe%M>-}v$i~Yg~`y{!kUc2D}sMPuY|Z=d>~C^I#GXYIg9#s@AEfj z3$C$J0Wt2HWOA+8dUGsv!(-~J%W9DgFA(;=4i4u7Q^;qpxpnc{%?dZ>+c^iHv!LJn zyY3$q@=Semc77^=J@Y|pCSjV3*eU@^BiuYMEVvvJNQ0ogVCbgp@M)TUPr$w6@DeiE zq;4K<|Kp}ZE%;R7kSc&Y6RhFx-_$Y@3;|QXd#!_{>@=C3qOX@bXEHA$n^rxTZ~MT> z3+`wrnuV||e&6S0sdUI46%ulU=PxP~zF1DsjxT-^N^$>Mwr!ayt?`y*o%t@FG`co@ zMRb{!1xb>%p?E+h;LXv8&Za>H6QQ$^w@&9VyCXnQbJ;n!Oz;p8+`iy#iCgZ@J!w6i z?7!~{MTLx3#)3bay2eqzZ=h@9DfJu25lhG!?2jo5GGL+nJW8NQoKs%D%Sg3D}oXb#p( zBg%JadUuo>UEF^p7whnjMjK*sVDnXd(<#dN zEZzvcI5NWCypv@C-yW|QD#4iIha|?H_FBpV{;?n~tz4?G2kwc*~}I{P*Bw zFpjtOilz=Bq@9DFv-U@qA(DIpQNPuVc>MNTk~PqgF(_+V;W0%PxL3ZWmCbe-A=5@C z758g8rrUB;jz8gQ7rdsOFq#M_iEX#RttP@01YHSTGLe_a&};}a9f~cZEFmngT7@)x zhGs%CjuXk;iHO~GcutK0sMeRFw~-fCv75h=bs=uab=|n9R4dXJ7S*l;?{SY+&hK%Y zz5Eb=>{OH5FeEKWEp)VkdT1}$N&Wyf#|7w>YO_&4Q#HlmPE48Ek_tCE*OPYWwzVf&=3rr2GC^pUbiN3Ujhqr3;$7yXrJ;ki32UW<8eQ5Nx zV|L+fOm94XoEc-M+@rCH>r*ZHf)qxIEGAs&1Acjg>+$SPZSp4*s` z6!<>*KlGR$&}#srPIix=+-|XyMrnEBu)e46S|w5`$uZ!{1gEcPfS1fuiNIIrWkw3E zW{?@?;^cDCsPB;W887b@al=z&*)pU$2=)v8CXaPtku1MYrrg+NelyN&O!8$YW&Wbv zFYuPNh5lnh!|YAbgH^2Nhdj}yvA^|2MKB^%$!6?E0WTzOi2PNV!p}rrJC0cBSTE@u z;*Cb6!gg|e&kroOA{#N{h%2DYc2(Pgt3d&@OTNrM6FdX=w1bvNqH9ov@Q`KjBiwlp zwqp2Opg-0*h!I|fG?GtL5%cYPH|d152UL|P|9Q`nJ6M+Cw!FN?$~Jf{U#;oQ!7aNW zVB+}G$f*o`=csi>x1C5~&jD)xPLB*vFcw$_Tww*`N+wY%^k;a-7Uze1g?gw{3mx#y zvj?FV3$V^f^L*G~>>9W>U@W=Rj%9`7l>)Qiv`U#8u9JSIj zX?n?D%(6!vaye-X>YaJhnqDw-?elT-E@|SDpuR~uu>JhBsBo6>2v01{3Q(8@`M+rj z3mN$QTKTR%Hgj(KeRwuG@ifrz5P{A2ukbPPR=E|RAxX-K&274G1C;S)xQEH}%Slgq z@Rk9UuR)q}-A3J*x^s-@sl(uYE4ciIB$};G)?6`!x}%Z_F@f!TFvzvYS1}`EhI>dw zNFCmxb^PkG0m<<(jtO85<9pLBMUdRqMKtmp@kVzr zUU!giY9q0!^i&VopOc;MXEoJOTXJ3VizMA(C*j?)VLus4$8D+pW$m@_rQIzi3DcZM z+nd+1vH&$dYviw4#ojZFEybXn3-`CcvwCd*&<}o#m}EVjT+~BTBiQR~A7bv?AXxh*&?CB=PtvrY z^W;841a3j(_W63X{Cl3D4JGTv#)Y={AU`yxx$pGV5*<*n99(X5Oq$DJy%5je)qvSU zzY_2QhRZm@I6YTO6R2`s;nOC zypaS_oQ|dDu^QVB?RFzCPSN&xE&Aq0Xb#%Y#~obw<=vn7uDplk^~>;Dr_{4*(PNnp zD*3?^8iKYlWbVo39G$2WwZ2UVnTvEjVop@udqFsAT{#a^bn7;)aqXA&T;;x^Agrk< zbOdr`z#MkOyl#r#L!#ASLa$ej@p6RY5)QGdOHdxek(=0yk^1?H?%U~rO+vlPL^1H^6nMwm|(>+HsK$5yNu%M3Iu=ODM0=8 zrQjn{*beTC?j{KcZtG-?*A^?EfO-ebKw65I(Q*9>$4BkVB0t3!dUNGM5L0BWWy9di zmw77rIJYhPaDE5$)w3)$Nx60_NRHa1+!eWuNsPHoQ`f0);2NE88y=s(=`r0)9gI{X z;q|v&jy#kq4fguOXbobp_N*O6_=4jc)kwq?%qVA`xcCO)!WeaKY@d8_tbg{7*)`eS z8e70cYfFB1ABcvZ-Jq<+vGB_-XztOM+qd9>EctJ|I9Up*M&5^C2Cz2keTKYt#h{9$ zOHQEOYqF@mg-f}wditG_o91$$rH^|jYqM}(CwJ-!Q4g3kAbL8~(7S;W{AC-)^@7`r zi=274=(3j}Vov+X4L^N|x+&DPuVkh$TI_mb*h=M^XTR3l-|uu@IEN8 z9G|2@I@i&Q)~weA$`aBBjDqm$anH76q!KSWIpos0@+AWJGNU*;9z+3D38mA$oKte{QmeLhDC>=?1g@?RQ;nGq+4A4atr zr3VcUq_ElGA(i|gI#ziam5(t<;r0P$Cy_-ujys;-D|k`SO_8Gl##|%jYOvdOU=I2> z*bxdin^KQa@q#z@ert_F4zLps&Tq!Au-I;ff@T#`^Z}PDeL6R}2**Z6Gg(-n0 z0cyBbq_HG{Qs<8k%yAY9X&L+ieg>|E5EUv41@@AsK`T;K_;a35WqWCeBV9kkDb0dq z)nn0$eyyZ|nsEAWITyiPL#;#^VYA_{F=b$fLRrOA$AWdbWZ?r5n~fN0S&i1qOvCUXZfd_7L+0Y7n2t7;;)xTIg->$! zZ%k(Q$=r@#=4Hl~+>*L*OqU6kz{WO-TNT<#p}5`@M7~m8{n)TQdHj<d=l;@#S}C_`FMG!R46({TnoyK@_W6CBPtiL3b}XZs&?#bT9G3?yu89z zk3?3wAotZ85`C&ej>eed!_8TQlD&vBeYCrt#G+cN%<6mTwh;s$yypd4uKW^D&CeG> z?vlc!)&!Q;3;f2Jx4U9B`=zm%QS9buE`rPy0mKr&-dSDAya5aP#aY02Eu%XFpL+w91A>K+o%I%+#yN50df=m%ko=kwWV{t*>4PKt=5 z^H;9cj&2yHTGBMGZIa2a>jgwOMbV$E68Yt9b|qe%1lA1bB~~`^`BrVcwD{z<@Q0JZ z+`Q9r$DzV7v(W3dI^kqUa{X>6VV+FaL5pBxrpkz=kCRY9)xmDeWpE3tAdKlMeTsXq z&Kjvq6Uj;O*5KZQwDXxdrOx8S=CXdUzHKY9KLg@t9UjjZB}{sy^+QCQl)E97cm08BQS=w}w`%Q#VxoLc->FTe6<<})!OJYYK?!d41 znhMWOKWNd%*cR+fBlt@0^?56%D<O+^W;@<6!2M@$L1As!-| zj}LtP7jjSvGXwK4-0}4**!7dSEYeRDNX`K&r+~Lc{uT^Mrjb?gY<1LqQ~wj#M8J0d literal 2600 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 literal 2152 zcmbtUeNJou6YX^Q1 zd?ameBESM~t4hSjkg}o;ro!;-y1CfbE2hrf-D+@PE?JVc-phhQBJLln8>D_~d43}ffTk{&?W2+9mEwi+ zxGSUZ)5niaKdbezO!LVKu@$CH!HND4rs!E%za@?kwB_$|A3H)tY%#-fM#E^Ht$nM| zokfj}Fj%;9mGeQKKl(xg;zgNA=nCG?nYr$;o8@;hy^FoHr?=h`OP&85nw0Q0@3{*T z=DjE?K?*kw=J_o@5Z(V;U#qBJz!!st7sZI6N2 zPrm7$cBJyLm7e-W#H(oIQZ|5h4=p5Uvf?KOn{6@dEyIeo7F$QVBhuAHvz;#gx6vcB z!RE6J2bKmmKQi3Eh;3^_W&WzM;viCfN|6vCtdM>Q<%w8ZG+DSqO-X)?t*nYMV-}rU z7j0)?sihJFX_@A)k5m?ct)}@XBfj9`J@~^mlz!Y-FR9j6smM{geuP>}mF91b`H(~G zJBg^0wAPacoLN)`Tebi(E$76~Y@O|>Pn^eKKl$W*_tnJ$b<9Z)5p;!K8S@3vCN_$+ zzT`0pZThpL+q;k2>A%37;%owhH-~zed0f@lDu|)9-y2}WPatj#Rpu=);-))1BFvBKf3V8~W$lp7rNXjLCR{&r$AIRq*otyrszd zq343zVNK68((&TrEOuWLbY^4#QVsVNs!_zEs1skQ?hvoji_NX?9z3zqb9a%cn+pc{ zlo*+1wS`x|pN+c#LZ;C=v7)-j)E|fJYepcc=DbuQ~0#f=UN59)8sTuvFiXN zbXGw!*FGrLoejk)A?@zkcgBy{Ydy*yQ#jB><=}y2q<1^3a>XhmWy|h`qar96gnWxVW zu>b%7 diff --git a/src/main/resources/player/armor/examplehelmet.png b/src/main/resources/player/armor/examplehelmet.png index 687a019636f5c54c2b18f81ca1356666383610da..bded8d4006e5bc1d8820dcfc7d530fb56e12ecaf 100644 GIT binary patch literal 7592 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 AiU0rr literal 2833 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 Date: Tue, 3 Feb 2026 18:16:43 +0000 Subject: [PATCH 18/28] Add example grass biome, tile, object & seed Introduce a new example grass system and reorganize map/incursion packages: - Add ExampleBiome (examples.maps.biomes) and new ExampleModBiomes loader; ExampleMod now loads biomes. - Implement overworld biome generation, spawn tables, generator stack and region placement logic. - Add ExampleGrassTile (terrain splatter tile) with growth/spread, loot and rendering logic, plus splat texture. - Add ExampleGrassObject (drops ExampleGrassSeed) and ExampleGrassSeedItem (places examplegrasstile). - Register new tile/object/item in ExampleModTiles, ExampleModObjects and ExampleModItems. - Add resource assets: item and object textures and tile splat PNGs. - Update locale/en.lang with names for new tile, object and seed. - Move incursion/map classes into examples.maps.incursion and rename ExampleTile package to examples.tiles; remove old incursion ExampleBiome file. These changes add a complete example grass biome + gameplay loop (tile growth, objects, and seed drops) and tidy up package structure for map/incursion examples. --- src/main/java/examplemod/ExampleMod.java | 3 +- .../examplemod/Loaders/ExampleModBiomes.java | 15 ++ .../Loaders/ExampleModIncursions.java | 10 +- .../examplemod/Loaders/ExampleModItems.java | 1 + .../examplemod/Loaders/ExampleModObjects.java | 3 + .../examplemod/Loaders/ExampleModTiles.java | 4 +- .../examples/incursion/ExampleBiome.java | 36 ---- .../items/materials/ExampleGrassSeedItem.java | 21 +++ .../examples/maps/biomes/ExampleBiome.java | 178 ++++++++++++++++++ .../incursion/ExampleIncursionBiome.java | 2 +- .../incursion/ExampleIncursionLevel.java | 2 +- .../examples/objects/ExampleGrassObject.java | 21 +++ .../examples/tiles/ExampleGrassTile.java | 106 +++++++++++ .../examples/{ => tiles}/ExampleTile.java | 2 +- src/main/resources/items/examplegrass.png | Bin 0 -> 465 bytes src/main/resources/items/examplegrassseed.png | Bin 0 -> 535 bytes src/main/resources/locale/en.lang | 3 + src/main/resources/objects/examplegrass.png | Bin 0 -> 1186 bytes .../tiles/examplegrasstile_splat.png | Bin 0 -> 11459 bytes 19 files changed, 358 insertions(+), 49 deletions(-) create mode 100644 src/main/java/examplemod/Loaders/ExampleModBiomes.java delete mode 100644 src/main/java/examplemod/examples/incursion/ExampleBiome.java create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java create mode 100644 src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java rename src/main/java/examplemod/examples/{ => maps}/incursion/ExampleIncursionBiome.java (98%) rename src/main/java/examplemod/examples/{ => maps}/incursion/ExampleIncursionLevel.java (99%) create mode 100644 src/main/java/examplemod/examples/objects/ExampleGrassObject.java create mode 100644 src/main/java/examplemod/examples/tiles/ExampleGrassTile.java rename src/main/java/examplemod/examples/{ => tiles}/ExampleTile.java (97%) create mode 100644 src/main/resources/items/examplegrass.png create mode 100644 src/main/resources/items/examplegrassseed.png create mode 100644 src/main/resources/objects/examplegrass.png create mode 100644 src/main/resources/tiles/examplegrasstile_splat.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 53a5e32..eb11d66 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,7 +1,7 @@ package examplemod; import examplemod.Loaders.*; -import examplemod.examples.incursion.ExampleBiome; +import examplemod.examples.maps.biomes.ExampleBiome; import necesse.engine.modLoader.annotations.ModEntry; import necesse.engine.sound.SoundSettings; import necesse.engine.sound.gameSound.GameSound; @@ -20,6 +20,7 @@ public void init() { // The examples are split into different classes here for readability, but you can register them directly here in init if you wish ExampleModCategories.load(); + ExampleModBiomes.load(); ExampleModIncursions.load(); ExampleModTiles.load(); ExampleModObjects.load(); diff --git a/src/main/java/examplemod/Loaders/ExampleModBiomes.java b/src/main/java/examplemod/Loaders/ExampleModBiomes.java new file mode 100644 index 0000000..b24b3f8 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBiomes.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.maps.biomes.ExampleBiome; +import necesse.engine.registries.BiomeRegistry; + +public class ExampleModBiomes { + public static void load() { + // Register a simple biome that will not appear in natural world gen. + ExampleMod.EXAMPLE_BIOME = BiomeRegistry.registerBiome("examplebiome", new ExampleBiome(), false); + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModIncursions.java b/src/main/java/examplemod/Loaders/ExampleModIncursions.java index e072748..6f328d7 100644 --- a/src/main/java/examplemod/Loaders/ExampleModIncursions.java +++ b/src/main/java/examplemod/Loaders/ExampleModIncursions.java @@ -1,19 +1,13 @@ package examplemod.Loaders; -import examplemod.ExampleMod; -import examplemod.examples.incursion.ExampleBiome; -import examplemod.examples.incursion.ExampleIncursionBiome; -import examplemod.examples.incursion.ExampleIncursionLevel; -import necesse.engine.registries.BiomeRegistry; +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 a simple biome that will not appear in natural world gen. - ExampleMod.EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); - // Register the incursion biome with tier requirement 1. IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index 198e969..5cff93b 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -20,6 +20,7 @@ public static void load(){ ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); ItemRegistry.registerItem("examplelog", new ExampleLogItem().setItemCategory("materials","logs"),10,true); + ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(),1,true); // Tools ItemRegistry.registerItem("examplesword", new ExampleSwordWeapon(), 20, true); diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 6953650..136cd75 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -35,5 +35,8 @@ public static void load(){ .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD) .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD),50,true); + // Register a grass object + ObjectRegistry.registerObject("examplegrass",new ExampleGrassObject(),1,true); + } } diff --git a/src/main/java/examplemod/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java index 4b5a9d8..f97d9eb 100644 --- a/src/main/java/examplemod/Loaders/ExampleModTiles.java +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -1,6 +1,7 @@ package examplemod.Loaders; -import examplemod.examples.ExampleTile; +import examplemod.examples.tiles.ExampleGrassTile; +import examplemod.examples.tiles.ExampleTile; import necesse.engine.registries.TileRegistry; public class ExampleModTiles { @@ -8,5 +9,6 @@ public class ExampleModTiles { public static void load(){ // Register our tiles TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + TileRegistry.registerTile("examplegrasstile", new ExampleGrassTile(),1,false,false,true); } } diff --git a/src/main/java/examplemod/examples/incursion/ExampleBiome.java b/src/main/java/examplemod/examples/incursion/ExampleBiome.java deleted file mode 100644 index 60d122b..0000000 --- a/src/main/java/examplemod/examples/incursion/ExampleBiome.java +++ /dev/null @@ -1,36 +0,0 @@ -package examplemod.examples.incursion; - -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/items/materials/ExampleGrassSeedItem.java b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java new file mode 100644 index 0000000..7e8164b --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -0,0 +1,21 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.tileItem.GrassSeedItem; + +/** + * A seed item that turns dirt into our custom grass tile when placed. + * + * Vanilla uses GrassSeedItem for grass seeds. It handles: + * Only placing on dirt + * Tile placement + preview + * Consuming the item (unless in god mode) + * "Grass seed" style tooltip and crafting ingredients + */ +public class ExampleGrassSeedItem extends GrassSeedItem { + + public ExampleGrassSeedItem() { + // This must match your TileRegistry stringID + // i.e. TileRegistry.registerTile("examplegrasstile", ...) + super("examplegrasstile"); + } +} diff --git a/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java new file mode 100644 index 0000000..3ff999c --- /dev/null +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -0,0 +1,178 @@ +package examplemod.examples.maps.biomes; + +import necesse.engine.AbstractMusicList; +import necesse.engine.MusicList; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.engine.world.biomeGenerator.BiomeGeneratorStack; +import necesse.entity.mobs.PlayerMob; +import necesse.level.maps.Level; +import necesse.level.maps.biomes.Biome; +import necesse.level.maps.biomes.MobSpawnTable; +import necesse.level.maps.regionSystem.Region; + +import java.awt.Color; + +/** + * Example overworld biome (1.1.x "infinite surface" style). + * + * Key idea for new modders: + * ------------------------ + * In Necesse 1.1.x, the overworld surface is generated in *regions* by the game's SurfaceLevel. + * SurfaceLevel does NOT create a special "ExampleBiomeLevel" per biome anymore. + * + * Instead, SurfaceLevel asks the Biome for: + * - which ground tile to paint (getGenerationTerrainTileID) + * - what "noise/vein" patterns to use for placing objects (initializeGeneratorStack) + * - what objects to place in each region (generateRegionSurfaceTerrain) + * + * So: if you want a biome that changes the overworld terrain + trees, these are the methods to override. + */ +public class ExampleBiome extends Biome { + + // -------------------------- + // Spawn tables + // -------------------------- + // MobSpawnTable controls what can spawn in the biome. + // Vanilla provides some defaults; we "include" them and then add our own entries. + + /** Small critters (e.g. butterflies, squirrels) for surface regions of this biome. */ + public static final MobSpawnTable surfaceCritters = new MobSpawnTable() + .include(Biome.defaultSurfaceCritters); + + /** Critters for cave levels (if the player is in caves). */ + public static final MobSpawnTable caveCritters = new MobSpawnTable() + .include(Biome.defaultCaveCritters); + + /** Hostile/neutral mobs for the surface. We add our custom mob "examplemob". */ + public static final MobSpawnTable surfaceMobs = new MobSpawnTable() + .include(Biome.defaultSurfaceMobs) + .add(30, "examplemob"); // weight: higher = more likely relative to other entries + + /** Hostile/neutral mobs for caves. Usually a different balance than the surface. */ + public static final MobSpawnTable caveMobs = new MobSpawnTable() + .include(Biome.defaultCaveMobs) + .add(100, "examplemob"); + + public ExampleBiome() { + super(); + + // Generation weight decides how often this biome is chosen when the world is generating new regions. + // Vanilla values are typically around ~0.5 to ~1.5. Keep it in that range while testing. + // (Huge numbers can cause problems with spawn finding and biome distribution.) + this.setGenerationWeight(1.0F); + } + + // -------------------------- + // Overworld world generation hooks (SurfaceLevel -> Biome) + // -------------------------- + + /** + * This is the *base ground tile* that SurfaceLevel paints for this biome in new surface regions. + * + * We look up our custom tile by string ID. If it isn't registered (returns -1), + * we fall back to vanilla grass so the game doesn't break and we can still load the world. + */ + @Override + public int getGenerationTerrainTileID() { + int exampleGrass = TileRegistry.getTileID("examplegrasstile"); + if (exampleGrass == -1) { + // -1 means "not found in registry" (usually a typo or missing registration). + return TileRegistry.grassID; + } + return exampleGrass; + } + + /** + * The BiomeGeneratorStack is a helper that stores "noise patterns" used during worldgen. + * + * Think of it like: "make a map of blobs/veins for trees", then when placing objects + * we can say "place trees only on those blobs" to get natural clumps. + * + * This method is called as part of generator setup, not every tick. + */ + @Override + public void initializeGeneratorStack(BiomeGeneratorStack stack) { + super.initializeGeneratorStack(stack); + + // Register a simplex-based "vein" pattern named "exampleTrees". + // We'll reference this by name later when placing our trees. + // + // The parameters control clump size / branching / frequency. + // If you want denser or larger clumps, we can tweak these values. + stack.addRandomSimplexVeinsBranch("exampleTrees", 2.0F, 0.2F, 1.0F, 0); + } + + /** + * Called when SurfaceLevel is generating a specific *region* of the overworld surface. + * + * This is where you place objects like: + * - trees + * - grass tufts + * - flowers + * - rocks, etc. + * + * IMPORTANT: This runs during world generation for new/unexplored regions. + * It does NOT retroactively change already-generated terrain. + */ + @Override + public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionSurfaceTerrain(region, stack, random); + + // Cache our terrain tile ID so we place objects only on the correct ground. + final int grassTile = getGenerationTerrainTileID(); + + // Place our custom tree object using the "exampleTrees" vein pattern. + // This creates forest-like clusters instead of a perfectly even distribution. + stack.startPlaceOnVein(this, region, random, "exampleTrees") + .onlyOnTile(grassTile) // only place on our biome's land tile + .chance(0.10D) // density inside valid vein areas (tweak for more/less) + .placeObject("exampletree"); + + // Place vanilla "grass" decoration objects on top of our tile. + // This is purely visual and helps the biome feel "alive". + stack.startPlace(this, region, random) + .chance(0.40D) // overall density of grass objects + .onlyOnTile(grassTile) + .placeObject("grass"); + } + + /** + * Used by some debug tools/views to show biomes as solid colors. + * Not required for gameplay, but helpful when testing generation. + */ + @Override + public Color getDebugBiomeColor() { + return new Color(128, 0, 128); + } + + // -------------------------- + // Ambient music + spawns + // -------------------------- + + /** + * Music selection for this biome. Here we reuse vanilla forest music. + * You can swap to a different MusicRegistry path if you want. + */ + @Override + public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { + return new MusicList(MusicRegistry.ForestPath); + } + + /** + * Critter spawns depend on whether the current level is a cave level. + */ + @Override + public MobSpawnTable getCritterSpawnTable(Level level) { + return level.isCave ? caveCritters : surfaceCritters; + } + + /** + * Mob spawns depend on whether the current level is a cave level. + */ + @Override + public MobSpawnTable getMobSpawnTable(Level level) { + return level.isCave ? caveMobs : surfaceMobs; + } +} diff --git a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java similarity index 98% rename from src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java index 379782a..5a4d9d6 100644 --- a/src/main/java/examplemod/examples/incursion/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java @@ -1,4 +1,4 @@ -package examplemod.examples.incursion; +package examplemod.examples.maps.incursion; import necesse.engine.network.server.Server; import necesse.engine.registries.ItemRegistry; diff --git a/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java similarity index 99% rename from src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java index e3f7acc..5118631 100644 --- a/src/main/java/examplemod/examples/incursion/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java @@ -1,4 +1,4 @@ -package examplemod.examples.incursion; +package examplemod.examples.maps.incursion; import examplemod.ExampleMod; import examplemod.examples.ExamplePreset; diff --git a/src/main/java/examplemod/examples/objects/ExampleGrassObject.java b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java new file mode 100644 index 0000000..22c1e17 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java @@ -0,0 +1,21 @@ +package examplemod.examples.objects; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GrassObject; +import necesse.level.maps.Level; + +public class ExampleGrassObject extends GrassObject { + + public ExampleGrassObject() { + // "examplegrass" is the texture name, 2 = variants/height setting used by GrassObject + super("examplegrass", 2); + } + + public LootTable getLootTable(Level level, int tileX, int tileY) { + // 4% chance, tweak to match vanilla feel + return new LootTable( + new ChanceLootItem(0.04F, "examplegrassseed") + ); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java new file mode 100644 index 0000000..d7f6c8b --- /dev/null +++ b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java @@ -0,0 +1,106 @@ +package examplemod.examples.tiles; + +import java.awt.Color; +import java.awt.Point; + +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.util.GameMath; +import necesse.engine.util.GameRandom; +import necesse.gfx.gameTexture.GameTextureSection; +import necesse.inventory.lootTable.LootItemInterface; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GameObject; +import necesse.level.gameTile.TerrainSplatterTile; +import necesse.level.maps.Level; +import necesse.level.maps.regionSystem.SimulatePriorityList; + +public class ExampleGrassTile extends TerrainSplatterTile { + // You can tweak these to change growth/spread speeds + public static double growChance = GameMath.getAverageSuccessRuns(7000.0D); + public static double spreadChance = GameMath.getAverageSuccessRuns(850.0D); + + private final GameRandom drawRandom = new GameRandom(); + + public ExampleGrassTile() { + // IMPORTANT: this string must match your texture name in resources/tiles/ + // (e.g. resources/tiles/examplegrass.png) + super(false, "examplegrasstile"); + + this.mapColor = new Color(70, 120, 40); // minimap color + this.canBeMined = true; + this.isOrganic = true; + } + + @Override + public LootTable getLootTable(Level level, int tileX, int tileY) { + // Option A: drop vanilla grassseed + // return new LootTable(new ChanceLootItem(0.04F, "grassseed")); + + // Option B: drop your own seed item (if you register one) + return new LootTable(new LootItemInterface[]{ + new ChanceLootItem(0.04F, "examplegrassseed") + }); + } + + @Override + public void addSimulateLogic(Level level, int x, int y, long ticks, SimulatePriorityList list, boolean sendChanges) { + addSimulateGrow(level, x, y, growChance, ticks, "examplegrass", list, sendChanges); + } + + // Same helper pattern vanilla uses (simplified) + public static void addSimulateGrow(Level level, int tileX, int tileY, double chance, long ticks, + String growObjectID, SimulatePriorityList list, boolean sendChanges) { + if (level.getObjectID(tileX, tileY) == 0) { + double runs = Math.max(1.0D, GameMath.getRunsForSuccess(chance, GameRandom.globalRandom.nextDouble())); + long remainingTicks = (long)(ticks - runs); + if (remainingTicks > 0L) { + GameObject obj = ObjectRegistry.getObject(ObjectRegistry.getObjectID(growObjectID)); + if (obj.canPlace(level, tileX, tileY, 0, false) == null) { + list.add(tileX, tileY, remainingTicks, () -> { + if (obj.canPlace(level, tileX, tileY, 0, false) == null) { + obj.placeObject(level, tileX, tileY, 0, false); + level.objectLayer.setIsPlayerPlaced(tileX, tileY, false); + if (sendChanges) level.sendObjectUpdatePacket(tileX, tileY); + } + }); + } + } + } + } + + @Override + public double spreadToDirtChance() { + // This is what makes dirt convert into your grass when adjacent + return spreadChance; + } + + @Override + public void tick(Level level, int x, int y) { + if (!level.isServer()) return; + + // Grow your grass object on empty tiles + if (level.getObjectID(x, y) == 0 && GameRandom.globalRandom.getChance(growChance)) { + GameObject grassObj = ObjectRegistry.getObject(ObjectRegistry.getObjectID("examplegrass")); + if (grassObj.canPlace(level, x, y, 0, false) == null) { + grassObj.placeObject(level, x, y, 0, false); + level.objectLayer.setIsPlayerPlaced(x, y, false); + level.sendObjectUpdatePacket(x, y); + } + } + } + + @Override + public Point getTerrainSprite(GameTextureSection terrainTexture, Level level, int tileX, int tileY) { + int tile; + synchronized (drawRandom) { + tile = drawRandom.seeded(getTileSeed(tileX, tileY)).nextInt(terrainTexture.getHeight() / 32); + } + return new Point(0, tile); // column 0, random row + } + + @Override + public int getTerrainPriority() { + return 100; // same as vanilla grass + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleTile.java b/src/main/java/examplemod/examples/tiles/ExampleTile.java similarity index 97% rename from src/main/java/examplemod/examples/ExampleTile.java rename to src/main/java/examplemod/examples/tiles/ExampleTile.java index 9189d36..2b981be 100644 --- a/src/main/java/examplemod/examples/ExampleTile.java +++ b/src/main/java/examplemod/examples/tiles/ExampleTile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.tiles; import necesse.engine.util.GameRandom; import necesse.gfx.gameTexture.GameTexture; diff --git a/src/main/resources/items/examplegrass.png b/src/main/resources/items/examplegrass.png new file mode 100644 index 0000000000000000000000000000000000000000..1fba177452e63d5fe4cc4438e34385a4fde7fa38 GIT binary patch literal 465 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*8Lmk8ZS<8K$fg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHibHvw)`hJCWyBKo^L5 zx;TbJw7#8cn0LrQz;(W$>4i3>I}OYV8b#eDD<=fZni5jD@j}2Q7mv)vGRk^Fg$#KP zT}xVH4uovJ(foF+z$D#G>u1UKtN!1vENLTmzFz%8ujpxu%6T73j3%4DpLKe1;W_W= zOEgssNKF>+~tD(ZldESuV3947xQP-cMTK81~>H;Z>+7RWqzTA*W;7!eTFlfxrcghE;*y}y!=f=a?!#v>jx$ZCpW)nuZ>>c z`G{Zm=1;lpOYIi?V|&6UFL|6HHt)@5|Iew}bN8Hxne-}FP;y|S0C zHU73wmc4Mo@6U(Xul9eaN@X*-<#F@KtW&%dHtd&ug3l_x?EwZe1B0ilpUXO@geCyg CL#@OB literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplegrassseed.png b/src/main/resources/items/examplegrassseed.png new file mode 100644 index 0000000000000000000000000000000000000000..35103ba85d2752ebede5af6932fcaca0c98e8502 GIT binary patch literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*8Lmk8ZS<8K$fg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHibHvw*Jkyvn*lpbMfs zT^vIqTHj9F>vhCIq}Bc?`;EAZQ7n!s3f)b=Ma-f(n%4G~cCFmLQIv(dx|W68dgGL% zK^MJRu8K=^e_eO7^w`-aT-jm%Q&0YRGDp8$t#SW4$=6J~OKM;I(@dXwZQfR?4O#2n zxp?k#6qA0GXw&&jutT-}DQ|;a`HFw}o+kYbD_pr77?~d!S1M)}lxLVuKVZwxptDip z&HR7G4k{`xlCR|+%$n`Rk|llPl=NFI=7X|7gU#n8;|Z&DmDdXLmNPb2bOKPOBwPis3k= z=QweV?}E2h&*X0{VDwcA-S}jq(%-%Dhgbc!P@8L0X6G~KK)*;(VAcKZBo}qR2l4Oy Xc4f1tvz@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;%JBegMCkr%giu000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHU?5E~j>4F6gH00ZVpL_t(|ob8)GZ`42##UF@3cSM8+q`-%wfQ~8>N<&En z4KzqJP#`2KIzAH$1&IWT1R~I;I0`blyJyesAMe_ReZMA(Z*OOJ#+i9Lb^sxS5JI%a zCGUfGc)eWz{Sh9!W7o%r#t9+j!{w@U^!!$u|D{(V z1t3JjL@NNxtlru0JAMEFcCZ5gxQ43${GI~C^QLCh?eP6sO9`a*wsP-_p??ptNC61ZFwqJil}|WI8qXQr%jC7Y zsj(H-v+q3>Nrm6HurqgPUEWzZw-VWYtk+|3Pl(5xLhzJDF04q*KgQ5XY4;t!4`Z-@ zElwV}_XGbu)aw060SM7B)f7N3oLOVfbfF5F%2dy7x#HSW6|TVE_qZY(x%*(d_Vgz- zzH~f{P({t{0dVCvKe7_zdD%Z~KNPoEY~Q>d!>;pFxzK$q1t3JjbQHi`-&kU5XIjr5 ze{L1gI}@kE`#8lZ0AO*E(Z^Oop{G9vD}z$;U~0{4*nQ6x)x+~oNGZh<^1ofTb*3sG zDF7iFW_CRQ!#jJ1&bmXf?lr#w$7gfPyyrOI`2JXLq8VHP=0a^hnBPmi|KXkO*-q}h z#=b9Nkpd8+VO#~^WjxD_w{ffYd!@R-X7>;W%k}w=@EHJb6MKW^_pmdd?cbDuUFY=_ zWFP4I_veKoSp?osFltqrP~5-b8-RMNTDmgx zd=cvzT6s3Ux5b}P@hZU9I8=yx61MN!_9Ofo8@D2`63dOh6o3#7)A!7n&l;gjk~)4U zp1soRh@Efj{m;dIEBl^$pG>G1khDH203jOY4+E(O$OPuh-T(jq07*qoM6N<$g2T`m A>MCL4ob9pkVS)K|v!lzuwUd_{YjC=WHw$-YUc@ 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 literal 0 HcmV?d00001 From 7f2bb19cff01b15cc5e9e9585cdd0e164448c6d1 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Wed, 4 Feb 2026 02:32:03 +0000 Subject: [PATCH 19/28] Add example level event, object, and loader Introduce example event plumbing and a placeable object demonstrating LevelEvent and GameEvent usage. Adds ExampleLevelEvent (server-side level event) and ExampleEvent (lightweight game event), an ExampleLevelEventObject that spawns the level event and triggers the game event, and ExampleModEvents to register the level event and a listener. Updates ExampleMod to load the events and registers the new object in ExampleModObjects. Also adds sprites for the object and a locale entry (en.lang). --- src/main/java/examplemod/ExampleMod.java | 1 + .../examplemod/Loaders/ExampleModEvents.java | 33 ++++ .../examplemod/Loaders/ExampleModObjects.java | 3 + .../examples/events/ExampleEvent.java | 39 +++++ .../examples/events/ExampleLevelEvent.java | 65 ++++++++ .../objects/ExampleLevelEventObject.java | 148 ++++++++++++++++++ .../items/exampleleveleventobject.png | Bin 0 -> 289 bytes src/main/resources/locale/en.lang | 1 + .../objects/exampleleveleventobject.png | Bin 0 -> 289 bytes 9 files changed, 290 insertions(+) create mode 100644 src/main/java/examplemod/Loaders/ExampleModEvents.java create mode 100644 src/main/java/examplemod/examples/events/ExampleEvent.java create mode 100644 src/main/java/examplemod/examples/events/ExampleLevelEvent.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java create mode 100644 src/main/resources/items/exampleleveleventobject.png create mode 100644 src/main/resources/objects/exampleleveleventobject.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index eb11d66..7319641 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -20,6 +20,7 @@ public void init() { // The examples are split into different classes here for readability, but you can register them directly here in init if you wish ExampleModCategories.load(); + ExampleModEvents.load();; ExampleModBiomes.load(); ExampleModIncursions.load(); ExampleModTiles.load(); diff --git a/src/main/java/examplemod/Loaders/ExampleModEvents.java b/src/main/java/examplemod/Loaders/ExampleModEvents.java new file mode 100644 index 0000000..e3a3fad --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModEvents.java @@ -0,0 +1,33 @@ +package examplemod.Loaders; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEventListener; +import necesse.engine.GameEvents; +import necesse.engine.network.server.ServerClient; +import necesse.engine.registries.LevelEventRegistry; + +public class ExampleModEvents { + public static void load() { + // Register our Level Event to the registry + LevelEventRegistry.registerEvent("examplelevelevent", ExampleLevelEvent.class); + + // Register our ExampleEvent Listener + GameEvents.addListener(ExampleEvent.class, new GameEventListener() { + @Override + public void onEvent(ExampleEvent event) { + if (event.level == null || !event.level.isServer()) return; + + ServerClient client = event.level.getServer().getClient(event.clientSlot); + if (client != null) { + client.sendChatMessage(event.message); + client.sendChatMessage("PONG: this message was sent from the ExampleEvent Listener "); + } + } + }); + + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 136cd75..4d316d4 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -38,5 +38,8 @@ public static void load(){ // Register a grass object ObjectRegistry.registerObject("examplegrass",new ExampleGrassObject(),1,true); + // Register an object which uses a level event + ObjectRegistry.registerObject("exampleleveleventobject", new ExampleLevelEventObject(),1,true); + } } diff --git a/src/main/java/examplemod/examples/events/ExampleEvent.java b/src/main/java/examplemod/examples/events/ExampleEvent.java new file mode 100644 index 0000000..83e74e5 --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleEvent.java @@ -0,0 +1,39 @@ +package examplemod.examples.events; + +import necesse.engine.events.GameEvent; +import necesse.level.maps.Level; + +/** + * A simple custom "game event" for our mod. + * + * This is NOT a LevelEvent (it does not exist in the world, does not tick, and is not drawn). + * Instead, it's a lightweight message object you pass through your own event system + * (for example: GameEvents.triggerEvent(...)) so other parts of your mod can react. + * + * Think of it like: "something happened" + "here is the data about what happened". + */ +public class ExampleEvent extends GameEvent { + + /** + * The level where the thing happened + */ + public final Level level; + + /** + * Which connected player this event is about. + */ + public final int clientSlot; + + /** + * Example payload data listeners can use. + */ + public final String message; + + public ExampleEvent(Level level, int clientSlot) { + this.level = level; + this.clientSlot = clientSlot; + + // Simple demo message that listeners can read. + this.message = "PING: this message was sent from the ExampleEvent"; + } +} diff --git a/src/main/java/examplemod/examples/events/ExampleLevelEvent.java b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java new file mode 100644 index 0000000..b60321f --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java @@ -0,0 +1,65 @@ +package examplemod.examples.events; + +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.server.ServerClient; +import necesse.entity.levelEvent.LevelEvent; + +/** + * Very simple LevelEvent demo: + * Server-side only: sends a chat message to a specific player, then ends. + * + * Note: If you spawn this with events.addHidden(...), clients will never receive it, + * so the packet methods below won't be used (they're included for when/if you use events.add(...)). + */ +public class ExampleLevelEvent extends LevelEvent { + + // Which ServerClient (player) to message. -1 = invalid/unset. + private int targetSlot = -1; + + // Message to send (kept non-null for safety). + private String message = ""; + + // Required empty constructor for registry/network spawning + public ExampleLevelEvent() { + } + + public ExampleLevelEvent(int targetSlot) { + this.targetSlot = targetSlot; + this.message = "ZING: this message was sent from the ExampleLevelEvent"; + } + + @Override + public void init() { + super.init(); + + // Only the server can access ServerClient and send chat packets. + if (isServer()) { + ServerClient client = level.getServer().getClient(targetSlot); + if (client != null) { + client.sendChatMessage(message); + } + + // We're done immediately (no ticking needed). + over(); + } + } + + @Override + public void setupSpawnPacket(PacketWriter writer) { + super.setupSpawnPacket(writer); + + // Write fields in a fixed order... + writer.putNextInt(targetSlot); + writer.putNextString(message); + } + + @Override + public void applySpawnPacket(PacketReader reader) { + super.applySpawnPacket(reader); + + // ...and read them back in the same order. + targetSlot = reader.getNextInt(); + message = reader.getNextString(); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java new file mode 100644 index 0000000..459dab9 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java @@ -0,0 +1,148 @@ +package examplemod.examples.objects; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEvents; +import necesse.engine.gameLoop.tickManager.TickManager; +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.Rectangle; +import java.util.List; + +/* + * Basic placeable object that: + * draws a single 32x32 sprite in the world + * + * spawns a LevelEvent that sends a chat message (event handles the message, not the object) + * and also fires an ExampleEvent which triggers an event listener to run its code + */ +public class ExampleLevelEventObject extends GameObject { + + // Loaded once from your mod resources in loadTextures() + private GameTexture texture; + + public ExampleLevelEventObject() { + // 32x32 collision/selection box + super(new Rectangle(32, 32)); + this.isSolid = true; + } + + @Override + public void loadTextures() { + super.loadTextures(); + + // Loads: src/main/resources/objects/exampleleveleventobject.png + this.texture = GameTexture.fromFile("objects/exampleleveleventobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + + // Lighting at this tile (so the sprite matches world lighting) + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coords -> screen draw coords + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Build the draw options once, then draw them inside the drawable + final TextureDrawOptionsEnd opts = this.texture.initDraw() + .sprite(0, 0, 32) // first sprite, 32x32 + .light(light) + .pos(drawX, drawY); + + // Add a drawable so the engine draws it in correct Y-sort order + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + return 16; // typical "middle of the tile" sort value for 1-tile objects + } + + @Override + public void draw(TickManager tickManager) { + opts.draw(); + } + }); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + + // This is the translucent "ghost" preview when placing the object + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + this.texture.initDraw() + .sprite(0, 0, 32) + .light(light) + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public boolean canInteract(Level level, int x, int y, PlayerMob player) { + return true; + } + + @Override + public void interact(Level level, int x, int y, PlayerMob player) { + + /* + * interact(...) is called on BOTH sides in multiplayer: + * client: when you click / interact locally + * server: when the server processes the interaction + * + * Anything that changes game state (spawning events, sending chat, triggering mod logic) + * should be done on the SERVER to avoid double-running and desync. + */ + if (level.isServer()) { + + /* + * In multiplayer, a PlayerMob on the server is tied to a ServerClient. + * The "slot" is a simple way to identify which connected client/player we mean. + * We'll pass this into our event so it knows who to target. + */ + int clientSlot = player.getServerClient().slot; + + /* + * Spawn our LevelEvent. + * + * We keep the object simple: it just creates the event. + * The ExampleLevelEvent itself contains the "what happens" logic (like sending a message). + */ + ExampleLevelEvent ev = new ExampleLevelEvent(clientSlot); + + /* + * addHidden(...) means "server-only": + * the event is added to the level's event manager + * it will NOT send a spawn packet to clients + * + * Use addHidden when the event is purely server logic (like chat/logging). + * Use events.add(ev) if you want clients to also receive/tick/draw the event. + */ + level.entityManager.events.addHidden(ev); + + + /* + * This is an example of triggering an event (in this case ExampleEvent) + * which will fire the event listener for ExampleEvent to run its code + */ + GameEvents.triggerEvent(new ExampleEvent(level, clientSlot)); + } + + // Always call super unless you specifically want to block default behavior. + super.interact(level, x, y, player); + } +} diff --git a/src/main/resources/items/exampleleveleventobject.png b/src/main/resources/items/exampleleveleventobject.png new file mode 100644 index 0000000000000000000000000000000000000000..957d455f4644d965454c71f7d510f499443d5232 GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*8Lmk8ZS<8K$fg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHibH3xl3mVbv=>piru( zi(^PcYw{N1ZGX=jG&E!?=*pKKSi$TjdjJHSx*pAVAINf5=V6T7BxW`n7PdQ#Dz1!1 z3Z^Rx8yWc<8Os@N1~zEB_dH_?I+&p+QNZ5wtO+RiIAe3o6~@JU5~T+mtQi=zIr%w% T@UOZ9w2;Bm)z4*}Q$iB}h6GWA literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index ba1e2e8..8e92f3c 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -13,6 +13,7 @@ examplegrass=Example Grass examplewall=Example Wall exampledoor=Example Door examplechair=Example Chair +exampleleveleventobject=Example Level Event Object [item] exampleitem=Example Item diff --git a/src/main/resources/objects/exampleleveleventobject.png b/src/main/resources/objects/exampleleveleventobject.png new file mode 100644 index 0000000000000000000000000000000000000000..957d455f4644d965454c71f7d510f499443d5232 GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*8Lmk8ZS<8K$fg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHibH3xl3mVbv=>piru( zi(^PcYw{N1ZGX=jG&E!?=*pKKSi$TjdjJHSx*pAVAINf5=V6T7BxW`n7PdQ#Dz1!1 z3Z^Rx8yWc<8Os@N1~zEB_dH_?I+&p+QNZ5wtO+RiIAe3o6~@JU5~T+mtQi=zIr%w% T@UOZ9w2;Bm)z4*}Q$iB}h6GWA literal 0 HcmV?d00001 From e3265679ccc7a9243e5e8fb98a58269add3bde5a Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Thu, 5 Feb 2026 02:23:38 +0000 Subject: [PATCH 20/28] Add summon weapon, mob, and level-event object Introduce a summon weapon and associated summon mob plus event-driven demo changes. Added ExampleSummonOrbWeapon, ExampleSummonWeaponMob, ExampleObjectEntity and new item/mob textures; registered the summon mob and item, and added an iron anvil recipe for the summon orb. Expanded ExampleLevelEvent to support server-side damage/health-change logic and client-side particle effects, plus proper spawn packet serialization. Replaced direct object interaction with an ObjectEntity (ExampleObjectEntity) for tile-based triggers and refactored ExampleLevelEventObject to spawn that entity. Renamed projectile/sword classes and assets for clarity (ExampleProjectileWeapon -> ExampleMagicProjectileWeapon, ExampleSwordWeapon -> ExampleMeleeSwordWeapon) and updated item registrations, recipes, and locale keys/tooltips accordingly. Misc: minor formatting and comments updates across examples and resource additions/deletions for new sprites. --- .../examplemod/Loaders/ExampleModItems.java | 10 +- .../examplemod/Loaders/ExampleModMobs.java | 4 + .../examplemod/Loaders/ExampleModRecipes.java | 14 +- .../Loaders/ExampleModResources.java | 2 + .../examples/ExampleConstructorPatch.java | 1 - .../examples/ExampleObjectEntity.java | 97 +++++++++++++ .../examples/events/ExampleEvent.java | 28 ++-- .../examples/events/ExampleLevelEvent.java | 135 ++++++++++++++++-- ...java => ExampleMagicProjectileWeapon.java} | 6 +- ...apon.java => ExampleMeleeSwordWeapon.java} | 4 +- .../items/tools/ExampleSummonOrbWeapon.java | 29 ++++ .../examples/mobs/ExampleSummonWeaponMob.java | 87 +++++++++++ .../objects/ExampleLevelEventObject.java | 103 +++---------- ...examplestaff.png => examplemagicstaff.png} | Bin ...examplesword.png => examplemeleesword.png} | Bin src/main/resources/items/examplesummonorb.png | Bin 0 -> 385 bytes src/main/resources/locale/en.lang | 8 +- src/main/resources/mobs/examplesummonmob.png | Bin 0 -> 4665 bytes .../resources/mobs/icons/examplesummonmob.png | Bin 0 -> 442 bytes .../player/weapons/examplemagicstaff.png | Bin 0 -> 467 bytes .../player/weapons/examplemeleesword.png | Bin 0 -> 451 bytes .../resources/player/weapons/examplestaff.png | Bin 540 -> 0 bytes .../player/weapons/examplesummonorb.png | Bin 0 -> 348 bytes .../resources/player/weapons/examplesword.png | Bin 516 -> 0 bytes 24 files changed, 404 insertions(+), 124 deletions(-) create mode 100644 src/main/java/examplemod/examples/ExampleObjectEntity.java rename src/main/java/examplemod/examples/items/tools/{ExampleProjectileWeapon.java => ExampleMagicProjectileWeapon.java} (96%) rename src/main/java/examplemod/examples/items/tools/{ExampleSwordWeapon.java => ExampleMeleeSwordWeapon.java} (86%) create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java create mode 100644 src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java rename src/main/resources/items/{examplestaff.png => examplemagicstaff.png} (100%) rename src/main/resources/items/{examplesword.png => examplemeleesword.png} (100%) create mode 100644 src/main/resources/items/examplesummonorb.png create mode 100644 src/main/resources/mobs/examplesummonmob.png create mode 100644 src/main/resources/mobs/icons/examplesummonmob.png create mode 100644 src/main/resources/player/weapons/examplemagicstaff.png create mode 100644 src/main/resources/player/weapons/examplemeleesword.png delete mode 100644 src/main/resources/player/weapons/examplestaff.png create mode 100644 src/main/resources/player/weapons/examplesummonorb.png delete mode 100644 src/main/resources/player/weapons/examplesword.png diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index 5cff93b..cf31ab5 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -6,8 +6,9 @@ import examplemod.examples.items.consumable.ExampleFoodItem; import examplemod.examples.items.consumable.ExamplePotionItem; import examplemod.examples.items.materials.*; -import examplemod.examples.items.tools.ExampleProjectileWeapon; -import examplemod.examples.items.tools.ExampleSwordWeapon; +import examplemod.examples.items.tools.ExampleMagicProjectileWeapon; +import examplemod.examples.items.tools.ExampleMeleeSwordWeapon; +import examplemod.examples.items.tools.ExampleSummonOrbWeapon; import necesse.engine.registries.ItemRegistry; public class ExampleModItems { @@ -23,8 +24,9 @@ public static void load(){ ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(),1,true); // Tools - ItemRegistry.registerItem("examplesword", new ExampleSwordWeapon(), 20, true); - ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); + ItemRegistry.registerItem("examplemeleesword", new ExampleMeleeSwordWeapon(), 20, true); + ItemRegistry.registerItem("examplemagicstaff", new ExampleMagicProjectileWeapon(), 30, true); + ItemRegistry.registerItem("examplesummonorb", new ExampleSummonOrbWeapon(),40,true); // Armor ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200f, true); diff --git a/src/main/java/examplemod/Loaders/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java index f8196f4..80acd22 100644 --- a/src/main/java/examplemod/Loaders/ExampleModMobs.java +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -2,6 +2,7 @@ import examplemod.examples.mobs.ExampleBossMob; import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; import necesse.engine.registries.MobRegistry; public class ExampleModMobs { @@ -11,5 +12,8 @@ public static void load(){ // Register boss mob MobRegistry.registerMob("examplebossmob", ExampleBossMob.class,true,true); + + // Register summon mob + MobRegistry.registerMob("examplesummonmob", ExampleSummonWeaponMob.class,true,false); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index b1e9662..deab3f5 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -37,7 +37,7 @@ public static void registerRecipes(){ //IRON ANVIL RECIPES Recipes.registerModRecipe(new Recipe( - "examplesword", + "examplemeleesword", 1, RecipeTechRegistry.IRON_ANVIL, new Ingredient[]{ @@ -47,7 +47,7 @@ public static void registerRecipes(){ )); Recipes.registerModRecipe(new Recipe( - "examplestaff", + "examplemagicstaff", 1, RecipeTechRegistry.IRON_ANVIL, new Ingredient[]{ @@ -56,6 +56,16 @@ public static void registerRecipes(){ } )); + Recipes.registerModRecipe(new Recipe( + "examplesummonorb", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 3), + new Ingredient("examplebar", 2) + } + )); + Recipes.registerModRecipe(new Recipe( "examplehelmet", 1, diff --git a/src/main/java/examplemod/Loaders/ExampleModResources.java b/src/main/java/examplemod/Loaders/ExampleModResources.java index 918470d..0244312 100644 --- a/src/main/java/examplemod/Loaders/ExampleModResources.java +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -3,6 +3,7 @@ import examplemod.ExampleMod; import examplemod.examples.mobs.ExampleBossMob; import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; import necesse.engine.sound.SoundSettings; import necesse.engine.sound.gameSound.GameSound; import necesse.gfx.gameTexture.GameTexture; @@ -16,6 +17,7 @@ public static void load(){ ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); + ExampleSummonWeaponMob.texture = GameTexture.fromFile("mobs/examplesummonmob"); //initialising the sound to be used by our boss mob ExampleMod.EXAMPLESOUND = GameSound.fromFile("examplesound"); diff --git a/src/main/java/examplemod/examples/ExampleConstructorPatch.java b/src/main/java/examplemod/examples/ExampleConstructorPatch.java index 7b48c45..0ad921a 100644 --- a/src/main/java/examplemod/examples/ExampleConstructorPatch.java +++ b/src/main/java/examplemod/examples/ExampleConstructorPatch.java @@ -22,5 +22,4 @@ static void onExit(@Advice.This RabbitMob rabbitMob) { // Debug message to know it's working System.out.println("Exited RabbitMob constructor: " + rabbitMob.getStringID()); } - } diff --git a/src/main/java/examplemod/examples/ExampleObjectEntity.java b/src/main/java/examplemod/examples/ExampleObjectEntity.java new file mode 100644 index 0000000..74eae65 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleObjectEntity.java @@ -0,0 +1,97 @@ +package examplemod.examples; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEvents; +import necesse.engine.network.server.ServerClient; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.level.maps.Level; + +import java.awt.*; + +public class ExampleObjectEntity extends ObjectEntity { + // Tracks whether a player was on it last tick (so we only trigger once per step-on) + private boolean pressed = false; + + // Small cooldown to avoid rapid re-triggers if the player jitters on the edge + private long nextTriggerTime = 0L; + + + public ExampleObjectEntity(Level level, int tileX, int tileY) { + // Create an ObjectEntity instance for the object placed at (tileX, tileY) on this level. + // The string is this ObjectEntity's type/id used by the engine for identification (save/load/sync). + super(level, "examplelevelevent", tileX, tileY); + + // no need to save this is only a demo + this.shouldSave = false; + } + + private Rectangle getWorldTileRect() { + // Full tile area in world pixels + return new Rectangle(tileX * 32, tileY * 32, 32, 32); + } + + @Override + public void serverTick() { + super.serverTick(); + + // Get the level + Level level = getLevel(); + + // Get the current time + long now = level.getTime(); + Rectangle tileRect = getWorldTileRect(); + + boolean hasPlayerOnTile = false; + + + // Check all connected server clients + for (ServerClient client : level.getServer().getClients()) { + if (client == null || client.playerMob == null) continue; + + // we want to specifically target the player rather than any mob + if (client.playerMob.getCollision().intersects(tileRect)) { + hasPlayerOnTile = true; + break; + } + } + + // if there's a player on the tile, and it's not pressed, and it's time to check + if (hasPlayerOnTile && !pressed && now >= nextTriggerTime) { + pressed = true; + nextTriggerTime = now + 300; // 300 time units cooldown (tweak as you like) + + int px = tileX * 32 + 16; + int py = tileY * 32 + 16; + + // If your ExampleLevelEvent targets a player slot, pick the first player standing on it: + int targetSlot = -1; + for (ServerClient client : level.getServer().getClients()) { + if (client != null && client.playerMob != null && client.playerMob.getCollision().intersects(tileRect)) { + targetSlot = client.slot; + + break; + } + } + + /* + * This is an example of triggering a level event (in this case ExampleLevelEvent). + * Use events.add(...) when both client and server need to be sent it. + * Use events.addHidden(...) when only the server needs to be sent it. + */ + level.entityManager.events.add(new ExampleLevelEvent(targetSlot, px, py)); + + /* + * This is an example of triggering an event (in this case ExampleEvent) + * which will fire the event listener for ExampleEvent to run its code + */ + GameEvents.triggerEvent(new ExampleEvent(level, targetSlot)); + } + + // Reset when nobody is standing on it + if (!hasPlayerOnTile) { + pressed = false; + } + } +} + diff --git a/src/main/java/examplemod/examples/events/ExampleEvent.java b/src/main/java/examplemod/examples/events/ExampleEvent.java index 83e74e5..4c4cc7e 100644 --- a/src/main/java/examplemod/examples/events/ExampleEvent.java +++ b/src/main/java/examplemod/examples/events/ExampleEvent.java @@ -3,37 +3,33 @@ import necesse.engine.events.GameEvent; import necesse.level.maps.Level; -/** - * A simple custom "game event" for our mod. +/* + * ExampleEvent is a small "notification" object for our mod. * - * This is NOT a LevelEvent (it does not exist in the world, does not tick, and is not drawn). - * Instead, it's a lightweight message object you pass through your own event system - * (for example: GameEvents.triggerEvent(...)) so other parts of your mod can react. + * Compared to a LevelEvent + * it does not exist in the world + * it does not tick + * it does not draw anything * - * Think of it like: "something happened" + "here is the data about what happened". + * Instead, we create one and pass it through GameEvents.triggerEvent(...) + * so any registered listeners can react. */ public class ExampleEvent extends GameEvent { - /** - * The level where the thing happened - */ + // The level where the event happened public final Level level; - /** - * Which connected player this event is about. - */ + // The slot id of the player this event relates to public final int clientSlot; - /** - * Example payload data listeners can use. - */ + // Simple data payload for the demo public final String message; public ExampleEvent(Level level, int clientSlot) { this.level = level; this.clientSlot = clientSlot; - // Simple demo message that listeners can read. + // For a real mod you would usually pass the message into the constructor. this.message = "PING: this message was sent from the ExampleEvent"; } } diff --git a/src/main/java/examplemod/examples/events/ExampleLevelEvent.java b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java index b60321f..82fefe1 100644 --- a/src/main/java/examplemod/examples/events/ExampleLevelEvent.java +++ b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java @@ -4,28 +4,50 @@ import necesse.engine.network.PacketWriter; import necesse.engine.network.server.ServerClient; import necesse.entity.levelEvent.LevelEvent; +import necesse.entity.levelEvent.mobAbilityLevelEvent.MobHealthChangeEvent; +import necesse.entity.mobs.GameDamage; +import necesse.entity.particle.Particle; + +import java.awt.Color; /** - * Very simple LevelEvent demo: - * Server-side only: sends a chat message to a specific player, then ends. - * - * Note: If you spawn this with events.addHidden(...), clients will never receive it, - * so the packet methods below won't be used (they're included for when/if you use events.add(...)). + * LevelEvent + * Server: sends a chat message to a specific player. + * Client: spawns a quick burst of particles at a world position. + * events.addHidden(ev) server only + * events.add(ev) server + clients */ public class ExampleLevelEvent extends LevelEvent { // Which ServerClient (player) to message. -1 = invalid/unset. private int targetSlot = -1; - // Message to send (kept non-null for safety). + // Message to send. private String message = ""; + // World position (pixels) where the particle effect should appear. + private int worldX; + private int worldY; + + // Simple lifetime for the client effect (in ticks). + private int ticksLeft = 20; + + // Small guard so the server only sends the message once. + private boolean sentServerMessage = false; + // Required empty constructor for registry/network spawning public ExampleLevelEvent() { } - public ExampleLevelEvent(int targetSlot) { + /** + * targetSlot player to message + * worldX particle position X in pixels (tileX * 32 + 16 is tile center) + * worldY particle position Y in pixels + */ + public ExampleLevelEvent(int targetSlot, int worldX, int worldY) { this.targetSlot = targetSlot; + this.worldX = worldX; + this.worldY = worldY; this.message = "ZING: this message was sent from the ExampleLevelEvent"; } @@ -33,33 +55,122 @@ public ExampleLevelEvent(int targetSlot) { public void init() { super.init(); - // Only the server can access ServerClient and send chat packets. - if (isServer()) { + // Server side: send the message once. + if (isServer() && !sentServerMessage) { + sentServerMessage = true; + + //target the specific client that triggered the event and make sure it's not returned null ServerClient client = level.getServer().getClient(targetSlot); if (client != null) { + + GameDamage dmg = new GameDamage(30F); + + //Apply damage + int healthBefore = client.playerMob.getHealth(); + client.playerMob.isServerHit(dmg, worldX, worldY, 0.0F, null); + int healthAfter = client.playerMob.getHealth(); + + //How much damage was actually taken + int damageTaken = healthBefore - healthAfter; + + if (damageTaken > 0) { + //Restore that amount clamped to current max health, and compute the healing applied because this is a demo, and we aren't mean xD + int finalHealth = Math.min(client.playerMob.getMaxHealth(), healthAfter + damageTaken); + int healApplied = finalHealth - healthAfter; + + //send a health change event to apply + level.entityManager.events.add(new MobHealthChangeEvent(client.playerMob, finalHealth, healApplied)); + } + client.sendChatMessage(message); } - // We're done immediately (no ticking needed). + // Do NOT call over() here if you want clients to see the event. + // Let it exist long enough for the spawn packet to be processed. + } + } + + @Override + public void serverTick() { + super.serverTick(); + + // If this event was added as hidden, it will never go to clients, + // so we can end it right away after doing server work. + if (sentServerMessage) { over(); } } + @Override + public void clientTick() { + super.clientTick(); + + // Quick particle burst for a short time, then end. + if (ticksLeft-- <= 0) { + over(); + return; + } + + Color c = new Color(120, 200, 255); + + for (int i = 0; i < 6; i++) { + float ox = (float) (Math.random() * 24.0 - 12.0); // -12..12 px + float oy = (float) (Math.random() * 24.0 - 12.0); + + level.entityManager + .addParticle(worldX + ox, worldY + oy, Particle.GType.COSMETIC) + .color(c) + .sizeFades(30, 60) + .fadesAlphaTime(250, 150) + .lifeTime(350); + } + } + @Override public void setupSpawnPacket(PacketWriter writer) { super.setupSpawnPacket(writer); - // Write fields in a fixed order... + /* + * setupSpawnPacket(...) is called when the server spawns this LevelEvent to clients + * (when you use level.entityManager.events.add/level.entityManager.events.addHidden) + * + * Anything you want the client-side version of this event to know must be written here. + * The client will read these values in applySpawnPacket(...) in the exact same order. + */ + + // Who the event should target (which player/server client slot) writer.putNextInt(targetSlot); + + // Any text payload we want associated with the event writer.putNextString(message); + + // World position for effects (these should be pixel coords, not tile coords) + writer.putNextInt(worldX); + writer.putNextInt(worldY); + + // How long the client-side effect should run + writer.putNextInt(ticksLeft); } @Override public void applySpawnPacket(PacketReader reader) { super.applySpawnPacket(reader); - // ...and read them back in the same order. + /* + * applySpawnPacket(...) is called on the client when it receives the spawn packet + * for this LevelEvent. + * + * Make sure you read values back in the same order you wrote them in setupSpawnPacket(...), + * otherwise you'll desync fields and get confusing bugs. + */ + + // Read target player slot + message payload targetSlot = reader.getNextInt(); message = reader.getNextString(); + + // Read effect position + lifetime + worldX = reader.getNextInt(); + worldY = reader.getNextInt(); + ticksLeft = reader.getNextInt(); } } diff --git a/src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java similarity index 96% rename from src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java index fe57eb1..f2440ae 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java @@ -18,13 +18,13 @@ import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleProjectileWeapon extends MagicProjectileToolItem { +public class ExampleMagicProjectileWeapon extends MagicProjectileToolItem { // This weapon will shoot out some projectiles. // Different classes for specific projectile weapon are already in place that you can use: // GunProjectileToolItem, BowProjectileToolItem, BoomerangToolItem, etc. - public ExampleProjectileWeapon() { + public ExampleMagicProjectileWeapon() { super(400, null); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); @@ -42,7 +42,7 @@ public ExampleProjectileWeapon() { @Override public ListGameTooltips getPreEnchantmentTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { ListGameTooltips tooltips = super.getPreEnchantmentTooltips(item, perspective, blackboard); - tooltips.add(Localization.translate("itemtooltip", "examplestafftip")); + tooltips.add(Localization.translate("itemtooltip", "examplemagicstafftip")); return tooltips; } diff --git a/src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java similarity index 86% rename from src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java index 76fad1e..1e72fee 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleSwordWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java @@ -4,11 +4,11 @@ import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; // Extends SwordToolItem -public class ExampleSwordWeapon extends SwordToolItem { +public class ExampleMeleeSwordWeapon extends SwordToolItem { // Weapon attack textures are loaded from resources/player/weapons/ - public ExampleSwordWeapon() { + public ExampleMeleeSwordWeapon() { super(400, null); rarity = Item.Rarity.UNCOMMON; attackAnimTime.setBaseValue(300); // 300 ms attack time diff --git a/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java new file mode 100644 index 0000000..77d32d7 --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java @@ -0,0 +1,29 @@ +package examplemod.examples.items.tools; + +import necesse.entity.mobs.itemAttacker.FollowPosition; +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.summonToolItem.SummonToolItem; +import necesse.inventory.lootTable.presets.SummonWeaponsLootTable; + +public class ExampleSummonOrbWeapon extends SummonToolItem { + public ExampleSummonOrbWeapon() { + // mobStringID, followPosition, summonSpaceTaken, enchantCost, lootTableCategory + super("examplesummonmob", + FollowPosition.PYRAMID, + 1.0F, + 400, + SummonWeaponsLootTable.summonWeapons); + + this.rarity = Item.Rarity.UNCOMMON; + + // This damage is what gets injected into your minion via mob.updateDamage(getAttackDamage(item)) + this.attackDamage.setBaseValue(50.0F).setUpgradedValue(1.0F, 45.0F); + + // Offset the X location of the attack texture + this.attackXOffset = 15; + // Offset the X location of the attack texture + this.attackYOffset = 10; + + + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java b/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java new file mode 100644 index 0000000..781a1dc --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java @@ -0,0 +1,87 @@ +package examplemod.examples.mobs; + +import java.awt.*; +import java.util.List; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.ai.behaviourTree.trees.PlayerFollowerCollisionChaserAI; +import necesse.entity.mobs.summon.summonFollowingMob.attackingFollowingMob.AttackingFollowingMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptions; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +public class ExampleSummonWeaponMob extends AttackingFollowingMob { + + // Loaded in examplemod.ExampleMod.initResources() + public static GameTexture texture; + + public ExampleSummonWeaponMob() { + super(20); // health + setSpeed(60.0F); + setFriction(2.0F); + this.attackCooldown = 500; + + this.collision = new Rectangle(-10, -7, 20, 14); + this.hitBox = new Rectangle(-12, -14, 24, 24); + this.selectBox = new Rectangle(-13, -14, 26, 24); + } + + @Override + public void init() { + super.init(); + + // Range, damage, knockback, cooldown etc. + // This uses this.summonDamage which SummonToolItem injects at spawn time. + this.ai = new BehaviourTreeAI<>(this, + new PlayerFollowerCollisionChaserAI<>( + 576, // target range + this.summonDamage, + 30, // knockback + 500, // attack windup? + 640, // chase range + 64 // give up / pathing distance + ) + ); + } + + @Override + protected void addDrawables(List list, OrderableDrawables tileList, OrderableDrawables topList, Level level, int x, int y, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + super.addDrawables(list, tileList, topList, level, x, y, tickManager, camera, perspective); + // Tile positions are basically level positions divided by 32. getTileX() does this for us etc. + GameLight light = level.getLightLevel(getTileX(), getTileY()); + int drawX = camera.getDrawX(x) - 32; + int drawY = camera.getDrawY(y) - 51; + + // A helper method to get the sprite of the current animation/direction of this mob + Point sprite = getAnimSprite(x, y, getDir()); + + drawY += getBobbing(x, y); + drawY += getLevel().getTile(getTileX(), getTileY()).getMobSinkingAmount(this); + + DrawOptions drawOptions = texture.initDraw() + .sprite(sprite.x, sprite.y, 64) + .light(light) + .pos(drawX, drawY); + + list.add(new MobDrawable() { + @Override + public void draw(TickManager tickManager) { + drawOptions.draw(); + } + }); + + addShadowDrawables(tileList, level, x, y, light, camera); + } + + @Override + public int getRockSpeed() { + // Change the speed at which this mobs animation plays + return 20; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java index 459dab9..51b5b83 100644 --- a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java @@ -1,10 +1,9 @@ package examplemod.examples.objects; -import examplemod.examples.events.ExampleEvent; -import examplemod.examples.events.ExampleLevelEvent; -import necesse.engine.GameEvents; +import examplemod.examples.ExampleObjectEntity; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; +import necesse.entity.objectEntity.ObjectEntity; import necesse.gfx.camera.GameCamera; import necesse.gfx.drawOptions.texture.TextureDrawOptionsEnd; import necesse.gfx.drawables.LevelSortedDrawable; @@ -18,21 +17,20 @@ import java.util.List; /* - * Basic placeable object that: - * draws a single 32x32 sprite in the world - * - * spawns a LevelEvent that sends a chat message (event handles the message, not the object) - * and also fires an ExampleEvent which triggers an event listener to run its code + * Basic placeable demo object that: + * - draws a 32x32 sprite in the world + * - on interact (server side), spawns our ExampleLevelEvent + * - also triggers our custom ExampleEvent through GameEvents (so listeners can react) */ public class ExampleLevelEventObject extends GameObject { - // Loaded once from your mod resources in loadTextures() + // Loaded once from mod resources in loadTextures() private GameTexture texture; public ExampleLevelEventObject() { - // 32x32 collision/selection box - super(new Rectangle(32, 32)); - this.isSolid = true; + //no physics shape + super(new Rectangle()); + this.isSolid = false; } @Override @@ -40,6 +38,7 @@ public void loadTextures() { super.loadTextures(); // Loads: src/main/resources/objects/exampleleveleventobject.png + // (no ".png" in the string) this.texture = GameTexture.fromFile("objects/exampleleveleventobject"); } @@ -48,38 +47,30 @@ public void addDrawables(List list, OrderableDrawables tile Level level, int tileX, int tileY, TickManager tickManager, GameCamera camera, PlayerMob perspective) { - // Lighting at this tile (so the sprite matches world lighting) + // Match sprite lighting to the level light at this tile GameLight light = level.getLightLevel(tileX, tileY); - // Convert tile coords -> screen draw coords + // Convert tile coordinates to screen draw coordinates int drawX = camera.getTileDrawX(tileX); int drawY = camera.getTileDrawY(tileY); - // Build the draw options once, then draw them inside the drawable + // Build draw options once (sprite + lighting + position) final TextureDrawOptionsEnd opts = this.texture.initDraw() - .sprite(0, 0, 32) // first sprite, 32x32 + .sprite(0, 0, 32) // sprite index (0,0), size 32 .light(light) .pos(drawX, drawY); - // Add a drawable so the engine draws it in correct Y-sort order - list.add(new LevelSortedDrawable(this, tileX, tileY) { - @Override - public int getSortY() { - return 16; // typical "middle of the tile" sort value for 1-tile objects - } - - @Override - public void draw(TickManager tickManager) { - opts.draw(); - } - }); + /* + */ + tileList.add(tm -> opts.draw()); } + @Override public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, PlayerMob player, GameCamera camera) { - // This is the translucent "ghost" preview when placing the object + // Placement preview ("ghost" sprite) while holding the item GameLight light = level.getLightLevel(tileX, tileY); int drawX = camera.getTileDrawX(tileX); int drawY = camera.getTileDrawY(tileY); @@ -92,57 +83,7 @@ public void drawPreview(Level level, int tileX, int tileY, int rotation, float a } @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) { - - /* - * interact(...) is called on BOTH sides in multiplayer: - * client: when you click / interact locally - * server: when the server processes the interaction - * - * Anything that changes game state (spawning events, sending chat, triggering mod logic) - * should be done on the SERVER to avoid double-running and desync. - */ - if (level.isServer()) { - - /* - * In multiplayer, a PlayerMob on the server is tied to a ServerClient. - * The "slot" is a simple way to identify which connected client/player we mean. - * We'll pass this into our event so it knows who to target. - */ - int clientSlot = player.getServerClient().slot; - - /* - * Spawn our LevelEvent. - * - * We keep the object simple: it just creates the event. - * The ExampleLevelEvent itself contains the "what happens" logic (like sending a message). - */ - ExampleLevelEvent ev = new ExampleLevelEvent(clientSlot); - - /* - * addHidden(...) means "server-only": - * the event is added to the level's event manager - * it will NOT send a spawn packet to clients - * - * Use addHidden when the event is purely server logic (like chat/logging). - * Use events.add(ev) if you want clients to also receive/tick/draw the event. - */ - level.entityManager.events.addHidden(ev); - - - /* - * This is an example of triggering an event (in this case ExampleEvent) - * which will fire the event listener for ExampleEvent to run its code - */ - GameEvents.triggerEvent(new ExampleEvent(level, clientSlot)); - } - - // Always call super unless you specifically want to block default behavior. - super.interact(level, x, y, player); + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + return new ExampleObjectEntity(level, x, y); } } diff --git a/src/main/resources/items/examplestaff.png b/src/main/resources/items/examplemagicstaff.png similarity index 100% rename from src/main/resources/items/examplestaff.png rename to src/main/resources/items/examplemagicstaff.png diff --git a/src/main/resources/items/examplesword.png b/src/main/resources/items/examplemeleesword.png similarity index 100% rename from src/main/resources/items/examplesword.png rename to src/main/resources/items/examplemeleesword.png diff --git a/src/main/resources/items/examplesummonorb.png b/src/main/resources/items/examplesummonorb.png new file mode 100644 index 0000000000000000000000000000000000000000..d662c7030376255342fd7aab454e5b3c4e0dfd76 GIT binary patch literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#_D)r)yK4P@j|PZ!6K zh}Pr;307rUgGs9r^OjzyulaQH?RyWS$(4`mQ9y+XR=fvl{BjLSoET&eI6o0@aq{Qhu*F%Um*=iP17n28opgt@vPYU2 z`570zW{Q~e=oIe@yY{1hKdj__u=1z)#8(|G%yLYNbr)R>`=}wo%5d<2TcX!5GeMw- O7(8A5T-G@yGywouZh(CN literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 8e92f3c..7c67aea 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -25,20 +25,22 @@ examplegrassseed=Example Grass Seed examplehuntincursionmaterial=Example Hunt Incursion Material examplepotion=Example Potion examplefood=Example Food -examplesword=Example Sword -examplestaff=Example Staff +examplemeleesword=Example Melee Sword +examplemagicstaff=Example Magic Staff +examplesummonorb=Example Summon Orb examplehelmet=Example Helmet examplechestplate=Example Chestplate exampleboots=Example Boots [itemtooltip] -examplestafftip=Shoots a homing, piercing projectile +examplemagicstafftip=Shoots a homing, piercing projectile examplepotionitemtip= An example potion [mob] examplemob=Example Mob examplebossmob=Example Boss +examplesummonmob=Example Summon Mob [buff] examplebuff=Example Buff diff --git a/src/main/resources/mobs/examplesummonmob.png b/src/main/resources/mobs/examplesummonmob.png new file mode 100644 index 0000000000000000000000000000000000000000..3f85699afdf8ea147cad00e2ea8ac2dd2dcb806a GIT binary patch literal 4665 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 BSz-VH literal 0 HcmV?d00001 diff --git a/src/main/resources/mobs/icons/examplesummonmob.png b/src/main/resources/mobs/icons/examplesummonmob.png new file mode 100644 index 0000000000000000000000000000000000000000..14ad9f5c8bab6b095bc0bfa597eb0ca351469b5a GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8gz!3HE#_5EXk6k~CayA$KhlREW44okYDuOkD) z#(wTUiL5}rLb6AYF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=Ijcz0ZOnX zdAqwX9Al_s*gtEzuQO1Dv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVgee^cX6$ZAW-O^ zr;B5VN9WOLhI~y1B5n0OYI%O|<~fS-9eBZe$8$>S;tdNpcbgY{VbI$lELN_@F2t_P zcb5A@#>yF6>bSSmuCK|I%Vl)7*mUm4j#bBfgxg9}W(kW7G(X++kn(tbvoj6ha^sn=u;}!CA7{C12d1b#uI(6OLySZ=m7+$=det7kg z6RSIKn(BS(vil&$U-`F4WtlO9$D>VV=`wEK>~FT@y$abYUhFVdQ&MBb@0J>nN?f?J) literal 0 HcmV?d00001 diff --git a/src/main/resources/player/weapons/examplemagicstaff.png b/src/main/resources/player/weapons/examplemagicstaff.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2ed0ce28dd1d1544de4fc1d5c614ba1eb9687f GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEjKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e4;GjG>NU|E%S{&Oi~)0*}aI1_o|n5N2eUHAey{$X?><>&pI$iIssxCt=}?k3biQ zd%8G=M6|xWVawNKz~J!k`FTzs6GwCQ{D%wF<~4d3Fqpr8@Z`WbNtrKldOKzoaLO#W zXvPwtA#&O~f@Moxrm4uUFXGGHwRvy#djf(sX*A5z19=vL;u+@+A7=vB<1KsV`KVQC_ShfDw?G>qhQu;H#u2q%% zZ@`#fc|Pt#cc#+rqWP8Ye-_Ub2|4{&e&5`*2D_8{1$4C7YA1zpbZzm7Qq@=$ddbT% zP6L}LOp?~9rp5v|X)w{&ccuo>ox%zg+ z!9#l`dwChm;-=X2FZb`yI>x=tlKrYtxmJ((o7K0t_pWohVLtmnAu!Y!JYD@<);T3K F0RZ=Lrjq~w literal 0 HcmV?d00001 diff --git a/src/main/resources/player/weapons/examplemeleesword.png b/src/main/resources/player/weapons/examplemeleesword.png new file mode 100644 index 0000000000000000000000000000000000000000..be64290018974b32623b47692d777f557d2dbc27 GIT binary patch literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofvPP)Tsw@SkfJR9T^xl z_H+M9WCijSl0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6XxjAYL$MSD+10f-TA0 z-G$*8Lmk8ZS<8K$fg+p*9+AZi4BWyX%*Zfnjs#GUy~NYkmHibHD+9BsclWYyKo_uk zx;TbJw7#8Y%hzNe<39hGu*HlQDtz~x+0H$9_uvVmkBqNP!{p}O=>-?6Emllcn|F|f zJvV9V78i|a{o5Iq`re-Y=`p`(RaXA@h0kKsuKYJ#*{AQE9mdcjvHDlU@l59?jo@8| zD<^V0Go75$&fefJkpAYpMf!@wR|SuBY~SBtdLZ0!$Ls2p;LygE~bs{8ZBfT6_T>FVdQ&MBb@0R7XZN&o-= literal 0 HcmV?d00001 diff --git a/src/main/resources/player/weapons/examplestaff.png b/src/main/resources/player/weapons/examplestaff.png deleted file mode 100644 index b277ac2e9d54f6e1c3cefffe05044f830f899209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 540 zcmV+%0^|LOP)4M~b}9L#~V3`rJB%0W_cKe-`El`@N#A<06GnuL}i zN!3!H79q((&DIkwLXsx^3u=ZW3oY88)U4ZS;y^S{GlI%V$J|#aH4d=>LsJJ56Ox3= zM|y-H*1)>8LJ}+vY)9hlB{DG#5`e2kPM1*vN^&(_qL`Fi4a!h34}tX2b({rXNWxMY zFt?-hZRnaw$SEIL1~8AHl#j69Bf5JA#DS!QeK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e49SyvJQj%)RuPXF>%sRCroVws%A|q;%&Q+&LmuzvIZs zZ5=TOoYpu+*z8TMIDOzigLUX5rsm`B2J#1LTIMk&erh@JK>zgx#x+MIY*J;dB<#Kl gT1gb;wmoEL&?&HgJ$pr1HPEdLp00i_>zopr0N-kL&;S4c literal 0 HcmV?d00001 diff --git a/src/main/resources/player/weapons/examplesword.png b/src/main/resources/player/weapons/examplesword.png deleted file mode 100644 index d2845b07c82a332cbfe3f21d0a5d6a5ff38465ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 516 zcmV+f0{i`mP)4$1pN7Vs+C%IF<#;CFnuLI1nMngv(MMd1eMs$l(e-S~->l-C+B>n7NFl4EW!eJgGFY^LEdTQGFF0h7LJHl@G|idd5Q6!bnDoN0#=-zo zM~jd`@h!fP;#Gpg?iR;aun?n3NU>l`8v_|q1QYe=`+rc1Gl~dKAum9q;k%@*5Vi5jG zy6LnGq5ltG{AHM13F+tEdj@HFDoL>nTnG^pKIlDUn)Do?i5l;)3bt|zlsRachR8{u zv Date: Fri, 6 Feb 2026 05:04:06 +0000 Subject: [PATCH 21/28] Add arrow ammo, bow, projectile & arrow buff Introduce arrow ammo, a ranged bow, projectile, and associated regen buff plus assets and registry updates. Adds ExampleArrowItem (arrow stats and projectile lookup), ExampleRangedBowWeapon (bow stats/behavior), ExampleArrowProjectile (custom draw, target filtering, hit logic that applies a 4s regen buff and 50% drop chance), and ExampleArrowBuff (server-side heal over time: 5 HP per 250ms). Registers new items, ammo and projectiles in ExampleModItems/ExampleModProjectiles and registers the buff in ExampleModBuffs. Also renamed/moved several example classes/packages (ExampleArmorSetBonusBuff -> ExampleArmorSetBuff, ExampleMagicProjectileWeapon -> ExampleMagicStaffWeapon, ExampleProjectile moved into projectiles package) and updated localization entries and sprite resources. --- .../examplemod/Loaders/ExampleModBuffs.java | 3 +- .../examplemod/Loaders/ExampleModItems.java | 10 +- .../Loaders/ExampleModProjectiles.java | 6 +- ...onusBuff.java => ExampleArmorSetBuff.java} | 4 +- .../examples/buffs/ExampleArrowBuff.java | 52 ++++++ .../examples/items/ammo/ExampleArrowItem.java | 37 +++++ ...apon.java => ExampleMagicStaffWeapon.java} | 6 +- .../items/tools/ExampleRangedBowWeapon.java | 31 ++++ .../projectiles/ExampleArrowProjectile.java | 157 ++++++++++++++++++ .../{ => projectiles}/ExampleProjectile.java | 2 +- src/main/resources/items/examplearrow.png | Bin 0 -> 464 bytes src/main/resources/items/examplerangedbow.png | Bin 0 -> 418 bytes src/main/resources/locale/en.lang | 8 +- .../player/weapons/examplerangedbow.png | Bin 0 -> 423 bytes .../projectiles/examplearrowprojectile.png | Bin 0 -> 448 bytes 15 files changed, 303 insertions(+), 13 deletions(-) rename src/main/java/examplemod/examples/buffs/{ExampleArmorSetBonusBuff.java => ExampleArmorSetBuff.java} (87%) create mode 100644 src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java create mode 100644 src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java rename src/main/java/examplemod/examples/items/tools/{ExampleMagicProjectileWeapon.java => ExampleMagicStaffWeapon.java} (96%) create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java create mode 100644 src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java rename src/main/java/examplemod/examples/{ => projectiles}/ExampleProjectile.java (98%) create mode 100644 src/main/resources/items/examplearrow.png create mode 100644 src/main/resources/items/examplerangedbow.png create mode 100644 src/main/resources/player/weapons/examplerangedbow.png create mode 100644 src/main/resources/projectiles/examplearrowprojectile.png diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java index 45d4f3b..f7a5ce7 100644 --- a/src/main/java/examplemod/Loaders/ExampleModBuffs.java +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -7,6 +7,7 @@ public class ExampleModBuffs { public static void load(){ // Register our buff BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); - BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBonusBuff()); + BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBuff()); + BuffRegistry.registerBuff("examplearrowbuff", new ExampleArrowBuff()); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index cf31ab5..2b37fda 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -1,13 +1,15 @@ package examplemod.Loaders; +import examplemod.examples.items.ammo.ExampleArrowItem; import examplemod.examples.items.armor.ExampleBootsArmorItem; import examplemod.examples.items.armor.ExampleChestArmorItem; import examplemod.examples.items.armor.ExampleHelmetArmorItem; import examplemod.examples.items.consumable.ExampleFoodItem; import examplemod.examples.items.consumable.ExamplePotionItem; import examplemod.examples.items.materials.*; -import examplemod.examples.items.tools.ExampleMagicProjectileWeapon; +import examplemod.examples.items.tools.ExampleMagicStaffWeapon; import examplemod.examples.items.tools.ExampleMeleeSwordWeapon; +import examplemod.examples.items.tools.ExampleRangedBowWeapon; import examplemod.examples.items.tools.ExampleSummonOrbWeapon; import necesse.engine.registries.ItemRegistry; @@ -25,8 +27,9 @@ public static void load(){ // Tools ItemRegistry.registerItem("examplemeleesword", new ExampleMeleeSwordWeapon(), 20, true); - ItemRegistry.registerItem("examplemagicstaff", new ExampleMagicProjectileWeapon(), 30, true); + ItemRegistry.registerItem("examplemagicstaff", new ExampleMagicStaffWeapon(), 30, true); ItemRegistry.registerItem("examplesummonorb", new ExampleSummonOrbWeapon(),40,true); + ItemRegistry.registerItem("examplerangedbow", new ExampleRangedBowWeapon(),10,true); // Armor ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200f, true); @@ -36,5 +39,8 @@ public static void load(){ // Consumables ItemRegistry.registerItem("examplepotion", new ExamplePotionItem(), 10, true); ItemRegistry.registerItem("examplefood", new ExampleFoodItem(), 15, true); + + // Ammo + ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(),5,true); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModProjectiles.java b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java index c910ab9..a6c190d 100644 --- a/src/main/java/examplemod/Loaders/ExampleModProjectiles.java +++ b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java @@ -1,11 +1,15 @@ package examplemod.Loaders; -import examplemod.examples.ExampleProjectile; +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/examples/buffs/ExampleArmorSetBonusBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java similarity index 87% rename from src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java rename to src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java index 65df04f..ec17866 100644 --- a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBonusBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java @@ -6,8 +6,8 @@ import necesse.entity.mobs.buffs.BuffModifiers; import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.setBonusBuffs.SimpleSetBonusBuff; -public class ExampleArmorSetBonusBuff extends SimpleSetBonusBuff { - public ExampleArmorSetBonusBuff() { +public class ExampleArmorSetBuff extends SimpleSetBonusBuff { + public ExampleArmorSetBuff() { super( new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), new ModifierValue<>(BuffModifiers.SPEED, 0.10f) diff --git a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java new file mode 100644 index 0000000..cedb0e2 --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -0,0 +1,52 @@ +package examplemod.examples.buffs; + +import necesse.entity.levelEvent.mobAbilityLevelEvent.MobHealthChangeEvent; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.staticBuffs.Buff; + +public class ExampleArrowBuff extends Buff { + + // Heal interval in milliseconds + private static final int HEAL_INTERVAL_MS = 250; + + public ExampleArrowBuff() { + this.canCancel = false; + this.isVisible = false; + this.isPassive = false; + this.shouldSave = false; + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + buff.getGndData().setInt("timePassed", 0); + } + @Override + public void serverTick(ActiveBuff buff) { + Mob m = buff.owner; + if (m == null) return; + + int heal = buff.getGndData().getInt("healPerTick"); + if (heal <= 0) return; + + int accum = buff.getGndData().getInt("timePassed"); + accum += 50; + + if (accum < HEAL_INTERVAL_MS) { + buff.getGndData().setInt("timePassed", accum); + return; + } + + accum -= HEAL_INTERVAL_MS; + buff.getGndData().setInt("timePassed", accum); + + int before = m.getHealth(); + int finalHealth = Math.min(m.getMaxHealth(), before + heal); + int applied = finalHealth - before; + + if (applied > 0) { + m.getLevel().entityManager.events.add(new MobHealthChangeEvent(m, finalHealth, applied)); + } + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java new file mode 100644 index 0000000..058b331 --- /dev/null +++ b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java @@ -0,0 +1,37 @@ +package examplemod.examples.items.ammo; + +import necesse.engine.registries.ProjectileRegistry; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.itemAttacker.ItemAttackerMob; +import necesse.entity.projectile.Projectile; +import necesse.inventory.item.arrowItem.ArrowItem; + +public class ExampleArrowItem extends ArrowItem { + + public ExampleArrowItem() { + super(5000); // stack size like vanilla arrows + + // These fields are on ArrowItem (public), and BowProjectileToolItem uses them via modDamage/modVelocity. + this.damage = 8; // adds +8 damage to the bow’s base damage + this.armorPen = 2; // adds armor pen + this.critChance = 0.05f; // +5% base crit + this.speedMod = 1.10f; // 10% faster arrow velocity + } + + @Override + public Projectile getProjectile(float x, float y, float targetX, float targetY, + float velocity, int range, GameDamage damage, int knockback, + ItemAttackerMob owner) { + + // Same exact pattern as StoneArrowItem / IronArrowItem, etc. + return ProjectileRegistry.getProjectile( + "examplearrowprojectile", // your projectile stringID + owner.getLevel(), + x, y, targetX, targetY, + velocity, range, + damage, knockback, + (Mob) owner + ); + } +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java similarity index 96% rename from src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java index f2440ae..b604b4e 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleMagicProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java @@ -1,6 +1,6 @@ package examplemod.examples.items.tools; -import examplemod.examples.ExampleProjectile; +import examplemod.examples.projectiles.ExampleProjectile; import necesse.engine.localization.Localization; import necesse.engine.network.gameNetworkData.GNDItemMap; import necesse.engine.sound.SoundEffect; @@ -18,13 +18,13 @@ import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleMagicProjectileWeapon extends MagicProjectileToolItem { +public class ExampleMagicStaffWeapon extends MagicProjectileToolItem { // This weapon will shoot out some projectiles. // Different classes for specific projectile weapon are already in place that you can use: // GunProjectileToolItem, BowProjectileToolItem, BoomerangToolItem, etc. - public ExampleMagicProjectileWeapon() { + public ExampleMagicStaffWeapon() { super(400, null); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); diff --git a/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java new file mode 100644 index 0000000..16740cd --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java @@ -0,0 +1,31 @@ +package examplemod.examples.items.tools; + +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.projectileToolItem.bowProjectileToolItem.BowProjectileToolItem; +import necesse.inventory.lootTable.presets.BowWeaponsLootTable; + +public class ExampleRangedBowWeapon extends BowProjectileToolItem { + public ExampleRangedBowWeapon() { + // (enchantCost, lootTableCategory) + super(100, BowWeaponsLootTable.bowWeapons); + + this.rarity = Item.Rarity.NORMAL; + + // Core stats + this.attackAnimTime.setBaseValue(800); // ms per shot + this.attackDamage.setBaseValue(12.0F); // base bow damage (arrows further modify) + this.attackRange.setBaseValue(600); // tiles-ish range value used by bows + this.velocity.setBaseValue(100); // base projectile velocity (arrows further modify) + this.knockback.setBaseValue(25); + + // Sprite offsets (tune until it looks right in-hand) + this.attackXOffset = 8; + this.attackYOffset = 20; + + // How much the bow sprite “stretches” while charging + this.attackSpriteStretch = 4; + + // Optional + this.canBeUsedForRaids = true; + } +} diff --git a/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java new file mode 100644 index 0000000..852c929 --- /dev/null +++ b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java @@ -0,0 +1,157 @@ +package examplemod.examples.projectiles; + +import java.awt.*; +import java.util.List; +import java.util.stream.Stream; + +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.network.NetworkClient; +import necesse.engine.util.GameRandom; +import necesse.engine.util.GameUtils; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.projectile.Projectile; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.texture.TextureDrawOptionsEnd; +import necesse.gfx.drawables.EntityDrawable; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.inventory.InventoryItem; +import necesse.level.maps.Level; +import necesse.level.maps.LevelObjectHit; +import necesse.level.maps.light.GameLight; + +public class ExampleArrowProjectile extends Projectile { + + + @Override + public void init() { + super.init(); + this.height = 18.0F; + this.heightBasedOnDistance = true; + setWidth(8.0F); + this.doesImpactDamage = false; + } + + @Override + public void addDrawables(List list, + OrderableDrawables tileList, OrderableDrawables topList, OrderableDrawables overlayList, + Level level, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + if (removed()) return; + + GameLight light = level.getLightLevel(this); + int drawX = camera.getDrawX(this.x) - this.texture.getWidth() / 2; + int drawY = camera.getDrawY(this.y); + + final TextureDrawOptionsEnd options = this.texture.initDraw() + .light(light) + .rotate(getAngle(), this.texture.getWidth() / 2, 0) + .pos(drawX, drawY - (int)getHeight()); + + list.add(new EntityDrawable(this) { + @Override + public void draw(TickManager tickManager) { + options.draw(); + } + }); + + // Shadow + addShadowDrawables(tileList, drawX, drawY, light, getAngle(), 0); + } + @Override + protected Stream streamTargets(Mob owner, Shape hitBounds) { + // The projectile calls this to ask: “Which mobs should I check collisions against this tick?” + + Level level = getLevel(); + // If we don’t have a level there’s nothing to test. + if (level == null || hitBounds == null) return Stream.empty(); + + // collect non player mobs + NetworkClient attackerClient = (owner == null) ? null : GameUtils.getAttackerClient(owner); + + // enemies/settlers/animals/etc) that are inside the projectile_attach hit area. + Stream mobs = level.entityManager.mobs + .streamInRegionsShape(hitBounds, 1) //query mobs in nearby regions + + // Is valid target logic + .filter(m -> owner == null || m.canBeTargeted(owner, attackerClient)); + + + // Collect players even if pvp is off + Stream players = level.entityManager.players + .streamInRegionsShape(hitBounds, 1) + + // Ignore null, the shooter, and players that were removed from the world. + .filter(p -> p != null && p != owner && !p.removed()) + + /* this makes sure the player has a network client + * the client has spawned and is not dead + * the playerMob exists and has a valid Level reference + */ + .filter(p -> { + NetworkClient c = p.getNetworkClient(); + return (c != null + && !c.isDead() + && c.hasSpawned() + && c.playerMob != null + && c.playerMob.getLevel() != null); + }); + + // Combine the two streams into one “things we can collide with” stream. + // PlayerMob is a subclass of Mob + return Stream.concat(mobs, players.map(p -> (Mob) p)); + } + + @Override + public boolean canHit(Mob mob) { + Mob owner = getOwner(); + + // Allow hitting allies + if (owner != null && (mob == owner || mob.isSameTeam(owner))) { + return true; + } + + // Otherwise use normal rules (enemies, etc.) + return super.canHit(mob); + } + + @Override + public void doHitLogic(Mob mob, LevelObjectHit object, float x, float y) { + super.doHitLogic(mob, object, x, y); + + if (!isServer() || mob == null) return; + + int durationMs = 4000; // 4 seconds regen + int healPerTick = 5; // healed every 250ms in the buff = 20 HP/sec + + // Try get existing buff + ActiveBuff existing = mob.buffManager.getBuff("examplearrowbuff"); + if (existing != null) { + // refresh duration + existing.setDurationLeft(durationMs); + + // optionally stack strength + int current = existing.getGndData().getInt("healPerTick"); + existing.getGndData().setInt("healPerTick", Math.max(current, healPerTick)); + + return; + } + + // create new + ActiveBuff regen = new ActiveBuff("examplearrowbuff", mob, durationMs, getOwner()); + regen.getGndData().setInt("healPerTick", healPerTick); + + // add the buff, send an update packet to clients and force update the buff + mob.buffManager.addBuff(regen, true,true,true); + } + + @Override + public void dropItem() { + // Optional: drop your arrow item sometimes, like vanilla StoneArrowProjectile does. + if (GameRandom.globalRandom.getChance(0.5F)) { + getLevel().entityManager.pickups.add(new InventoryItem("examplearrow").getPickupEntity(getLevel(), this.x, this.y) + ); + } + } +} diff --git a/src/main/java/examplemod/examples/ExampleProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleProjectile.java rename to src/main/java/examplemod/examples/projectiles/ExampleProjectile.java index 8e46ce8..3b247c0 100644 --- a/src/main/java/examplemod/examples/ExampleProjectile.java +++ b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.projectiles; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.GameDamage; diff --git a/src/main/resources/items/examplearrow.png b/src/main/resources/items/examplearrow.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad20938cd07694ec04cfa6e517c5f43bd89b54f GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#;|wtgh}pb6*#5lPl*w+Q4Xrpq>|M_gce zYmzH5+vd*P(vD5qvt$3BntA-)pZnFu1<$WlUJ#Emxw`TYXK;M3#FBL%FM3y)qTS-+Vb_U`siBqiPMq zt^FstcbYJK-oB^l0Z?VZ$Cs)*XK+{ve>7HTNmbg%#xl9H_40=+U)#Q&tar^j@Plto z_m>#+z*9Di0h-sBgf!lF`NPX`sn~Pg-Mvf?YD5`d?6L7)dEBLl2PC+!{yjrN_I4+a uKE~@M+7{npCaHgyy>QX@kLwe*cQOoNjnV9lIq{%yWbkzLb6Mw<&;$TcFs|eP literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplerangedbow.png b/src/main/resources/items/examplerangedbow.png new file mode 100644 index 0000000000000000000000000000000000000000..0310d0fcbdd2373d3bf3b6477afd1708fcba6803 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eVq#;oG~l)Oya^O~=IP=X z649ERAn~$cVi5ByA=dwoC+E-lFSlOSXNpxr&;h4)ZZpoy_C`ob072ceb50ZOr_Jz~ zV#zGWq_Ui`Wd3{m@6xNSoC^7wfg&!;8E2eNzGDeeD>3cOpMwVuG`tLU+Q}r#GgqLY zv0Cu5B9BtArJ}(PX9neWk_vX}W>bZkn(bK<8g4;MSj?wj#jCOQL`nh>SZtfTi%;T@ zV#A5|6Fwct`d7mS1Z}&H9QnWE)04)7@=PxpZe=FEXkn;|YS`M5P*ZfUp`GET=8PL* pA3wYmU$C6BDElNMe`6p6L+T&r^N;uQ%mezE!PC{xWt~$(69B{*jUE61 literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 7c67aea..57bb308 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -25,9 +25,11 @@ examplegrassseed=Example Grass Seed examplehuntincursionmaterial=Example Hunt Incursion Material examplepotion=Example Potion examplefood=Example Food -examplemeleesword=Example Melee Sword -examplemagicstaff=Example Magic Staff -examplesummonorb=Example Summon Orb +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 diff --git a/src/main/resources/player/weapons/examplerangedbow.png b/src/main/resources/player/weapons/examplerangedbow.png new file mode 100644 index 0000000000000000000000000000000000000000..09a87a4401178895117fe0a374dddb366c98edb0 GIT binary patch literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Ld!3HEdKT9nDQjEnx?oNz1PwLbIIV|apzK#qG z8~eHcB(ehe3dtTpz6=aiY77hwEes65fI|H(?D8gCb5n0T@z%2~Ij105pNB{-dOFVsD*A3cdDp zaSZY3JUdO1?~s8=+kQch4Ka<%3eN)~RG5N0A8-lBG_eFXJ0vYHm@Lz%ouJsEI88ib z`^6<|&emOB7W?_>>Fn#%Uj#`VQc%bgWx zpLb)h+0NeVbI0?M<(89r*TY?p7%`qOn0riFWYgCqo&$#UAt}>VZ#z<45&d>`V`%!i z7UeZN7wWGgtNdSvY3H^TNs2H8D`Cq01C2~c>21szhYuzR8zUNf3g+O1uUK} zjv*eMZ?CNPVhI#E{;^-_2V2uo1-4e!!`mEPn)?-gGxa69ZkTaKt@+Ro8ln}vUc)5Kc;PCdQz`Mvvp^6_AbU7dAu)3CMn%->BnAq2+o`*^Vp65D{ znXJ?HGeK<5-1z3BUzgj3>#qNO`$f20s~~R=o15#4;QKlGYM(Qf8L?_Du2!hJ%V2&{ z_QoyEYPqE<4_|RryeOG;BzcpMk6-i71jW5V=_gOU|99=8VZ^$!YUHJO_ jL2XdoRQ)+G-oNBciuCqB8aF!>7(@)7u6{1-oD!M Date: Wed, 11 Feb 2026 02:44:28 +0000 Subject: [PATCH 22/28] Add settler jobs, settings, objects, and packets Introduce mod settings and a small settler/job system plus related objects, packets and patches. - Add ExampleModSettings (persistent mod config) and expose it as ExampleMod.settings via initSettings(). - Register new job type and level job (ExampleLevelJob) and load it from ExampleModJobs; add safe job-finder patch to avoid null handlers. - Add settler/settler-mob classes (ExampleSettler, ExampleSettlerMob) and register them via ExampleModSettlers and ExampleModMobs. - Add job-triggering world object and entity (ExampleJobObject, ExampleJobObjectEntity) that scans nearby tiles and enqueues ExampleLevelJob jobs. - Add a config-driven object (ExampleConfigObject) and a client->server packet (ExampleConfigInteractPacket) that reads/updates ExampleModSettings and saves server settings. - Enhance ExampleMod to load the new loaders (jobs, settlers, packets, objects) and small fixes/renames of package layouts for patches/presets/objectentity. - Add ExampleArrowBuff improvements (server-side heal logic using gndData) and several resource images and localization entries for new items/objects/jobs. These changes demonstrate using persistent settings, custom objects/object entities, jobs/settlers integration, packet handling, and a defensive patch for job finding. --- src/main/java/examplemod/ExampleMod.java | 12 ++- .../java/examplemod/ExampleModSettings.java | 39 ++++++++ .../Loaders/ExampleModCommands.java | 1 + .../examplemod/Loaders/ExampleModJobs.java | 26 +++++ .../examplemod/Loaders/ExampleModMobs.java | 4 + .../examplemod/Loaders/ExampleModObjects.java | 6 ++ .../examplemod/Loaders/ExampleModPackets.java | 5 + .../Loaders/ExampleModSettlers.java | 14 +++ .../examples/buffs/ExampleArrowBuff.java | 51 +++++++++- .../items/materials/ExampleGrassSeedItem.java | 2 +- .../maps/incursion/ExampleIncursionLevel.java | 2 +- .../examples/mobs/ExampleSettlerMob.java | 46 +++++++++ .../objectentity/ExampleJobObjectEntity.java | 79 +++++++++++++++ .../ExampleObjectEntity.java | 2 +- .../examples/objects/ExampleConfigObject.java | 87 +++++++++++++++++ .../examples/objects/ExampleJobObject.java | 79 +++++++++++++++ .../objects/ExampleLevelEventObject.java | 2 +- .../packets/ExampleConfigInteractPacket.java | 64 +++++++++++++ .../ExampleConstructorPatch.java | 2 +- .../{ => patches}/ExampleMethodPatch.java | 2 +- .../examples/patches/JobFinderSafe.java | 36 +++++++ ...r_StreamFoundJobs_NoNullHandlersPatch.java | 30 ++++++ .../examples/{ => presets}/ExamplePreset.java | 3 +- .../{ => presets}/ExamplePresetCode.java | 3 +- .../settlement/jobs/ExampleLevelJob.java | 90 ++++++++++++++++++ .../settlement/settlers/ExampleSettler.java | 41 ++++++++ .../resources/items/exampleconfigobject.png | Bin 0 -> 1271 bytes src/main/resources/items/examplejobobject.png | Bin 0 -> 291 bytes src/main/resources/locale/en.lang | 15 +++ .../resources/objects/exampleconfigobject.png | Bin 0 -> 1271 bytes .../resources/objects/examplejobobject.png | Bin 0 -> 291 bytes 31 files changed, 732 insertions(+), 11 deletions(-) create mode 100644 src/main/java/examplemod/ExampleModSettings.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModJobs.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModSettlers.java create mode 100644 src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java create mode 100644 src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java rename src/main/java/examplemod/examples/{ => objectentity}/ExampleObjectEntity.java (98%) create mode 100644 src/main/java/examplemod/examples/objects/ExampleConfigObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleJobObject.java create mode 100644 src/main/java/examplemod/examples/packets/ExampleConfigInteractPacket.java rename src/main/java/examplemod/examples/{ => patches}/ExampleConstructorPatch.java (95%) rename src/main/java/examplemod/examples/{ => patches}/ExampleMethodPatch.java (99%) create mode 100644 src/main/java/examplemod/examples/patches/JobFinderSafe.java create mode 100644 src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java rename src/main/java/examplemod/examples/{ => presets}/ExamplePreset.java (98%) rename src/main/java/examplemod/examples/{ => presets}/ExamplePresetCode.java (98%) create mode 100644 src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java create mode 100644 src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java create mode 100644 src/main/resources/items/exampleconfigobject.png create mode 100644 src/main/resources/items/examplejobobject.png create mode 100644 src/main/resources/objects/exampleconfigobject.png create mode 100644 src/main/resources/objects/examplejobobject.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 7319641..d7338b4 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -9,24 +9,34 @@ @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 EXAMPLESOUND; public static SoundSettings EXAMPLESOUNDSETTINGS; + // 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!"); // The examples are split into different classes here for readability, but you can register them directly here in init if you wish ExampleModCategories.load(); - ExampleModEvents.load();; + ExampleModEvents.load(); + ExampleModJobs.load(); ExampleModBiomes.load(); ExampleModIncursions.load(); ExampleModTiles.load(); ExampleModObjects.load(); ExampleModItems.load(); ExampleModMobs.load(); + ExampleModSettlers.load(); ExampleModProjectiles.load(); ExampleModBuffs.load(); ExampleModPackets.load(); diff --git a/src/main/java/examplemod/ExampleModSettings.java b/src/main/java/examplemod/ExampleModSettings.java new file mode 100644 index 0000000..a551c07 --- /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); + exampleString = data.getSafeString("exampleString", exampleString, false); + + //log the settings to test + logLoadedSettings(); + } + + private 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/ExampleModCommands.java b/src/main/java/examplemod/Loaders/ExampleModCommands.java index e60f03b..3754142 100644 --- a/src/main/java/examplemod/Loaders/ExampleModCommands.java +++ b/src/main/java/examplemod/Loaders/ExampleModCommands.java @@ -8,5 +8,6 @@ public static void load(){ // Register our server chat command CommandsManager.registerServerCommand(new ExampleChatCommand()); + } } diff --git a/src/main/java/examplemod/Loaders/ExampleModJobs.java b/src/main/java/examplemod/Loaders/ExampleModJobs.java new file mode 100644 index 0000000..8bda7bc --- /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("weeding", + new JobType( + true, // canChangePriority (shows in settlement UI) + true, // defaultDisabledBySettler (locked for normal settlers) + new LocalMessage("jobs", "weedingname"), + new LocalMessage("jobs", "weedingtip") + ) + ); + + // 2) Register our ExampleLevelJob //DEBUG + LevelJobRegistry.registerJob("weedgrass", ExampleLevelJob .class, "weeding"); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java index 80acd22..a9cb57e 100644 --- a/src/main/java/examplemod/Loaders/ExampleModMobs.java +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -3,6 +3,7 @@ import examplemod.examples.mobs.ExampleBossMob; import examplemod.examples.mobs.ExampleMob; import examplemod.examples.mobs.ExampleSummonWeaponMob; +import examplemod.examples.mobs.ExampleSettlerMob; import necesse.engine.registries.MobRegistry; public class ExampleModMobs { @@ -15,5 +16,8 @@ public static void load(){ // Register summon mob MobRegistry.registerMob("examplesummonmob", ExampleSummonWeaponMob.class,true,false); + + // Register a example mob (ExampleSettlerMob that uses ExampleSettler for settler settings and is capable of our ExampleLevelJob //DEBUG + MobRegistry.registerMob("examplesettlermob", ExampleSettlerMob.class, false, false,true); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 4d316d4..dda5e77 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -41,5 +41,11 @@ public static void load(){ // Register an object which uses a level event ObjectRegistry.registerObject("exampleleveleventobject", new ExampleLevelEventObject(),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); + } } diff --git a/src/main/java/examplemod/Loaders/ExampleModPackets.java b/src/main/java/examplemod/Loaders/ExampleModPackets.java index 8f09fd9..1a6aabf 100644 --- a/src/main/java/examplemod/Loaders/ExampleModPackets.java +++ b/src/main/java/examplemod/Loaders/ExampleModPackets.java @@ -1,5 +1,6 @@ package examplemod.Loaders; +import examplemod.examples.packets.ExampleConfigInteractPacket; import examplemod.examples.packets.ExamplePacket; import examplemod.examples.packets.ExamplePlaySoundPacket; import necesse.engine.registries.PacketRegistry; @@ -10,6 +11,10 @@ public static void load() { // Register our packets PacketRegistry.registerPacket(ExamplePacket.class); + // Register our sound playing packet PacketRegistry.registerPacket(ExamplePlaySoundPacket.class); + + // Register our packet to read config from an object + PacketRegistry.registerPacket(ExampleConfigInteractPacket.class); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModSettlers.java b/src/main/java/examplemod/Loaders/ExampleModSettlers.java new file mode 100644 index 0000000..67431c8 --- /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 ExampleSettler used by ExampleSettlerMob //DEBUG + SettlerRegistry.registerSettler("examplesettler", new ExampleSettler()); + } + +} diff --git a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java index cedb0e2..7074946 100644 --- a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -6,47 +6,94 @@ import necesse.entity.mobs.buffs.BuffEventSubscriber; import necesse.entity.mobs.buffs.staticBuffs.Buff; +/** + * ExampleArrowBuff + * + * A server-side buff that periodically heals the mob it is applied to. + * + * Key points: + * - The healing is done on the SERVER in {@link #serverTick(ActiveBuff)} so it is authoritative in multiplayer. + * - The buff uses "gndData" (Game Network Data) to store per-instance state: + * - "healPerTick": how much health to add each heal tick (must be set by whoever applies the buff) + * - "timePassed": internal timer accumulator to control the heal interval + * - A {@link MobHealthChangeEvent} is spawned so the game can display/hear the heal change properly + * (numbers, effects, syncing, etc.), rather than silently changing health. + */ public class ExampleArrowBuff extends Buff { - // Heal interval in milliseconds + /** + * How often we apply healing (in milliseconds). + * We simulate this by accumulating time in server ticks (see {@link #serverTick(ActiveBuff)}). + */ private static final int HEAL_INTERVAL_MS = 250; public ExampleArrowBuff() { + // Player can't right-click/cancel the buff this.canCancel = false; + + // Hide from the UI (no buff icon) this.isVisible = false; + + // Not a passive stat modifier style buff this.isPassive = false; + + // Don't save this buff to disk (intended as temporary / combat effect) this.shouldSave = false; } @Override public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + // Initialize our per-buff timer accumulator. + // This value lives in the buff instance's gndData so each mob/buff has its own timer. buff.getGndData().setInt("timePassed", 0); + + // NOTE: We intentionally do NOT set "healPerTick" here. + // Whoever applies the buff should set it, e.g.: + // buff.getGndData().setInt("healPerTick", 2); } + @Override public void serverTick(ActiveBuff buff) { + // The mob that currently has this buff. Mob m = buff.owner; if (m == null) return; + // How much to heal each time we trigger a heal tick. + // If not set, it will default to 0, meaning "do nothing". int heal = buff.getGndData().getInt("healPerTick"); if (heal <= 0) return; + /* + * Necesse serverTick runs at a fixed tick rate. + * In this example we assume 1 server tick ≈ 50ms and accumulate time ourselves. + * + * (If tick rate changes, you'd want to accumulate using the actual delta time if available, + * but for many simple buffs, a fixed step like this is fine.) + */ int accum = buff.getGndData().getInt("timePassed"); accum += 50; + // Not enough time has passed yet: store accumulator and wait for the next tick. if (accum < HEAL_INTERVAL_MS) { buff.getGndData().setInt("timePassed", accum); return; } + // Enough time passed: "spend" one interval worth of time. + // We subtract rather than zeroing so leftover time isn't lost. accum -= HEAL_INTERVAL_MS; buff.getGndData().setInt("timePassed", accum); + // Compute how much healing we can actually apply without exceeding max health. int before = m.getHealth(); int finalHealth = Math.min(m.getMaxHealth(), before + heal); int applied = finalHealth - before; + // If healing would have no effect (already full), do nothing. if (applied > 0) { + // Spawn a health change event instead of directly forcing health, + // so the engine can properly sync and display the heal. m.getLevel().entityManager.events.add(new MobHealthChangeEvent(m, finalHealth, applied)); } } -} \ No newline at end of file +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java index 7e8164b..204e7ea 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -5,7 +5,7 @@ /** * A seed item that turns dirt into our custom grass tile when placed. * - * Vanilla uses GrassSeedItem for grass seeds. It handles: + * uses GrassSeedItem for grass seeds. It handles: * Only placing on dirt * Tile placement + preview * Consuming the item (unless in god mode) diff --git a/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java index 5118631..ea8c555 100644 --- a/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java @@ -1,7 +1,7 @@ package examplemod.examples.maps.incursion; import examplemod.ExampleMod; -import examplemod.examples.ExamplePreset; +import examplemod.examples.presets.ExamplePreset; import necesse.engine.GameEvents; import necesse.engine.events.worldGeneration.GenerateCaveLayoutEvent; import necesse.engine.events.worldGeneration.GeneratedCaveOresEvent; diff --git a/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java b/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java new file mode 100644 index 0000000..b6b1b8f --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java @@ -0,0 +1,46 @@ +package examplemod.examples.mobs; + +import examplemod.examples.settlement.jobs.ExampleLevelJob; +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 ExampleSettlerMob extends HumanShop { + + public ExampleSettlerMob() { + // MUST pass 3 args (same pattern as FarmerHumanMob) + super(500, 200, "examplesettler"); + + // Unlock the job type for THIS settler only + this.jobTypeHandler.getPriority("weeding").disabledBySettler = false; + + // Give them a tool to clear grass (optional, but nice) + this.equipmentInventory.setItem(6, new necesse.inventory.InventoryItem("farmingscythe")); + + // Register handler so they can actually perform the job + this.jobTypeHandler.setJobHandler( + ExampleLevelJob.class, + 0, 0, // cooldown min/max + 0, 4000, // work-break buffer usage min/max (matches Forestry) + (handler, worker) -> + !isOnWorkBreak() + && !isOnStrike() + && !hasCompletedMission() + && (!isSettler() || isSettlerWithinSettlement()) + && !isInventoryFull(true), + foundJob -> ExampleLevelJob.getJobSequence(this, isSettler(), foundJob) + ); + } + + @Override + public List getRecruitItems(ServerClient client) { + // Optional: if trapped, don’t allow recruiting + 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/objectentity/ExampleJobObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java new file mode 100644 index 0000000..d68130b --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java @@ -0,0 +1,79 @@ +package examplemod.examples.objectentity; + +import necesse.entity.objectEntity.ObjectEntity; +import necesse.level.maps.Level; +import necesse.level.maps.LevelObject; + +import examplemod.examples.settlement.jobs.ExampleLevelJob; + +public class ExampleJobObjectEntity extends ObjectEntity { + + // Config + private final int radiusTiles = 25; + + // State + private long lastScanTime; + private int scanDX; + private int scanDY; + + public ExampleJobObjectEntity(Level level, int tileX, int tileY) { + // NOTE: ObjectEntity constructor needs (level, type, x, y) + super(level, "exampleweedingpost", tileX, tileY); + this.shouldSave = true; + + this.scanDX = -radiusTiles; + this.scanDY = -radiusTiles; + this.lastScanTime = 0L; + } + + @Override + public void init() { + super.init(); + if (this.lastScanTime <= 0L) { + this.lastScanTime = getWorldEntity().getWorldTime(); + } + } + + @Override + public void serverTick() { + super.serverTick(); + + long now = getWorldEntity().getWorldTime(); + // run once per second (worldTime is ms) + long scanIntervalMs = 1000; + if (now < this.lastScanTime + scanIntervalMs) { + return; + } + this.lastScanTime = now; + + Level level = getLevel(); + + // how many tiles we check each scan burst + int tilesPerRun = 120; + for (int i = 0; i < tilesPerRun; i++) { + int x = this.tileX + scanDX; + int y = this.tileY + scanDY; + + // advance scan cursor (square spiral-ish over the area) + scanDX++; + if (scanDX > radiusTiles) { + scanDX = -radiusTiles; + scanDY++; + if (scanDY > radiusTiles) { + scanDY = -radiusTiles; + } + } + + if (!level.isTileWithinBounds(x, y)) continue; + + // Don’t clear decorative / player-placed grass + if (level.objectLayer.isPlayerPlaced(x, y)) continue; + + LevelObject lo = level.getLevelObject(x, y); + if (lo.object == null || !lo.object.isGrass) continue; + + // Add your weeding job + level.jobsLayer.addJob(new ExampleLevelJob(x,y)); + } + } +} diff --git a/src/main/java/examplemod/examples/ExampleObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleObjectEntity.java rename to src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java index 74eae65..96768ec 100644 --- a/src/main/java/examplemod/examples/ExampleObjectEntity.java +++ b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.objectentity; import examplemod.examples.events.ExampleEvent; import examplemod.examples.events.ExampleLevelEvent; 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..b2bbfb9 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleConfigObject.java @@ -0,0 +1,87 @@ +package examplemod.examples.objects; + +import examplemod.ExampleMod; +import examplemod.examples.packets.ExampleConfigInteractPacket; +import necesse.engine.gameLoop.tickManager.TickManager; +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.Rectangle; +import java.util.List; + +public class ExampleConfigObject extends GameObject { + // Loaded once from mod resources in loadTextures() + private GameTexture texture; + + public ExampleConfigObject() { + super(new Rectangle(32, 32)); + this.isSolid = true; + } + + @Override + public void loadTextures() { + super.loadTextures(); + + // Loads: src/main/resources/objects/exampleleveleventobject.png + // (no ".png" in the string) + this.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) { + + // Match sprite lighting to the level light at this tile + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coordinates to screen draw coordinates + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Build draw options once (sprite + lighting + position) + final TextureDrawOptionsEnd opts = this.texture.initDraw() + .sprite(0, 0, 32) // sprite index (0,0), size 32 + .light(light) + .pos(drawX, drawY); + + /* + */ + tileList.add(tm -> opts.draw()); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + + // Placement preview ("ghost" sprite) while holding the item + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + this.texture.initDraw() + .sprite(0, 0, 32) + .light(light) + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public boolean canInteract(Level level, int x, int y, PlayerMob player) { + return true; + } + + @Override + public void interact(Level level, int x, int y, PlayerMob player) { + if (level.isClient() && level.getClient() != null) { + level.getClient().network.sendPacket(new ExampleConfigInteractPacket(x, y)); + } + } +} \ 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..8558d8a --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleJobObject.java @@ -0,0 +1,79 @@ +package examplemod.examples.objects; + +import java.awt.Rectangle; +import java.util.List; + +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; + +public class ExampleJobObject extends GameObject { + // Loaded once from mod resources in loadTextures() + private GameTexture texture; + + public ExampleJobObject() { + super(new Rectangle(32, 32)); + this.isSolid = true; + this.mapColor = new java.awt.Color(120, 170, 120); + } + @Override + public void loadTextures() { + super.loadTextures(); + + // Loads: src/main/resources/objects/exampleleveleventobject.png + // (no ".png" in the string) + this.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) { + + // Match sprite lighting to the level light at this tile + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coordinates to screen draw coordinates + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Build draw options once (sprite + lighting + position) + final TextureDrawOptionsEnd opts = this.texture.initDraw() + .sprite(0, 0, 32) // sprite index (0,0), size 32 + .light(light) + .pos(drawX, drawY); + + /* + */ + tileList.add(tm -> opts.draw()); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + + // Placement preview ("ghost" sprite) while holding the item + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + this.texture.initDraw() + .sprite(0, 0, 32) + .light(light) + .alpha(alpha) + .draw(drawX, drawY); + } + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + return new ExampleJobObjectEntity(level, x, y); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java index 51b5b83..cf624fd 100644 --- a/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java @@ -1,6 +1,6 @@ package examplemod.examples.objects; -import examplemod.examples.ExampleObjectEntity; +import examplemod.examples.objectentity.ExampleObjectEntity; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; import necesse.entity.objectEntity.ObjectEntity; diff --git a/src/main/java/examplemod/examples/packets/ExampleConfigInteractPacket.java b/src/main/java/examplemod/examples/packets/ExampleConfigInteractPacket.java new file mode 100644 index 0000000..4c1d5c3 --- /dev/null +++ b/src/main/java/examplemod/examples/packets/ExampleConfigInteractPacket.java @@ -0,0 +1,64 @@ +package examplemod.examples.packets; + +import examplemod.ExampleMod; +import necesse.engine.Settings; +import necesse.engine.network.NetworkPacket; +import necesse.engine.network.Packet; +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.client.Client; +import necesse.engine.network.server.Server; +import necesse.engine.network.server.ServerClient; + +public class ExampleConfigInteractPacket extends Packet { + + public final int tileX; + public final int tileY; + + // requred + public ExampleConfigInteractPacket(byte[] data) { + super(data); + PacketReader r = new PacketReader(this); + this.tileX = r.getNextInt(); + this.tileY = r.getNextInt(); + } + + // Convenience constructor when sending + public ExampleConfigInteractPacket(int tileX, int tileY) { + this.tileX = tileX; + this.tileY = tileY; + + PacketWriter w = new PacketWriter(this); + w.putNextInt(tileX); + w.putNextInt(tileY); + } + + @Override + public void processServer(NetworkPacket packet, Server server, ServerClient client) { + if (ExampleMod.settings == null) { + client.sendChatMessage("[ExampleMod] Settings missing on server."); + return; + } + + // 1) Increment server value + ExampleMod.settings.exampleInt += 1; + + // 2) Save server config back to disk (server.cfg + cfg/mods/.cfg) + Settings.saveServerSettings(); + + // 3) Display AFTER increment+save + boolean enabled = ExampleMod.settings.exampleBoolean; + int amount = ExampleMod.settings.exampleInt; + String msg = ExampleMod.settings.exampleString; + + client.sendChatMessage("[ExampleMod] Server config updated (saved):"); + client.sendChatMessage("exampleBoolean: " + enabled); + client.sendChatMessage("exampleInt: " + amount); + client.sendChatMessage("exampleString: " + msg); + } + + @Override + public void processClient(NetworkPacket packet, Client client) { + // Nothing needed. Server sends chat messages to the 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 0ad921a..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; 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/patches/JobFinderSafe.java b/src/main/java/examplemod/examples/patches/JobFinderSafe.java new file mode 100644 index 0000000..6419196 --- /dev/null +++ b/src/main/java/examplemod/examples/patches/JobFinderSafe.java @@ -0,0 +1,36 @@ +package examplemod.examples.patches; + +import java.util.Collection; +import java.util.Iterator; +import java.util.stream.Stream; +import java.util.stream.Stream.Builder; + +import necesse.entity.mobs.job.EntityJobWorker; +import necesse.entity.mobs.job.FoundJob; +import necesse.entity.mobs.job.JobTypeHandler; + +public final class JobFinderSafe { + private JobFinderSafe() {} + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Stream safeStreamFoundJobs(JobTypeHandler handler, EntityJobWorker worker) { + if (handler == null || worker == null) return Stream.empty(); + + Builder b = Stream.builder(); + + Collection subs = handler.getJobHandlers(); + + for (Object o : subs) { + if (!(o instanceof JobTypeHandler.SubHandler)) continue; + + JobTypeHandler.SubHandler sub = (JobTypeHandler.SubHandler) o; + + Iterator it = sub.streamFoundJobsFiltered(worker).iterator(); + while (it.hasNext()) { + b.add((FoundJob) it.next()); + } + } + + return b.build(); + } +} diff --git a/src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java b/src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java new file mode 100644 index 0000000..3c5a81a --- /dev/null +++ b/src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java @@ -0,0 +1,30 @@ +package examplemod.examples.patches; + +import java.util.stream.Stream; + +import necesse.engine.modLoader.annotations.ModMethodPatch; +import necesse.entity.mobs.job.EntityJobWorker; +import necesse.entity.mobs.job.FoundJob; +import necesse.entity.mobs.job.JobFinder; +import necesse.entity.mobs.job.JobTypeHandler; +import net.bytebuddy.asm.Advice; + +@ModMethodPatch(target = JobFinder.class, name = "streamFoundJobs", arguments = {}) +public class JobFinder_StreamFoundJobs_NoNullHandlersPatch { + + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean onEnter( + @Advice.FieldValue("handler") JobTypeHandler handler, + @Advice.FieldValue("worker") EntityJobWorker worker, + @Advice.Local("out") Stream out + ) { + out = JobFinderSafe.safeStreamFoundJobs(handler, worker); + return true; // skip vanilla method body + } + + @Advice.OnMethodExit + static void onExit(@Advice.Local("out") Stream out, + @Advice.Return(readOnly = false) Stream ret) { + ret = out; + } +} diff --git a/src/main/java/examplemod/examples/ExamplePreset.java b/src/main/java/examplemod/examples/presets/ExamplePreset.java similarity index 98% rename from src/main/java/examplemod/examples/ExamplePreset.java rename to src/main/java/examplemod/examples/presets/ExamplePreset.java index 708721b..3f814d3 100644 --- a/src/main/java/examplemod/examples/ExamplePreset.java +++ b/src/main/java/examplemod/examples/presets/ExamplePreset.java @@ -1,5 +1,6 @@ -package examplemod.examples; +package examplemod.examples.presets; +import examplemod.examples.ExampleLootTable; import necesse.engine.util.GameRandom; import necesse.level.maps.presets.Preset; diff --git a/src/main/java/examplemod/examples/ExamplePresetCode.java b/src/main/java/examplemod/examples/presets/ExamplePresetCode.java similarity index 98% rename from src/main/java/examplemod/examples/ExamplePresetCode.java rename to src/main/java/examplemod/examples/presets/ExamplePresetCode.java index e4c644b..dd8909a 100644 --- a/src/main/java/examplemod/examples/ExamplePresetCode.java +++ b/src/main/java/examplemod/examples/presets/ExamplePresetCode.java @@ -1,5 +1,6 @@ -package examplemod.examples; +package examplemod.examples.presets; +import examplemod.examples.ExampleLootTable; import necesse.engine.registries.ObjectRegistry; import necesse.engine.registries.TileRegistry; import necesse.engine.util.GameRandom; 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..953966e --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java @@ -0,0 +1,90 @@ +package examplemod.examples.settlement.jobs; + +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.save.LoadData; +import necesse.entity.ObjectDamageResult; +import necesse.entity.mobs.job.EntityJobWorker; +import necesse.entity.mobs.job.FoundJob; +import necesse.entity.mobs.job.GameLinkedListJobSequence; +import necesse.entity.mobs.job.JobSequence; +import necesse.entity.mobs.job.activeJob.MineObjectActiveJob; +import necesse.level.maps.LevelObject; +import necesse.level.maps.levelData.jobs.LevelJob; +import necesse.level.maps.levelData.jobs.MineObjectLevelJob; + +public class ExampleLevelJob extends MineObjectLevelJob { + + public ExampleLevelJob(int tileX, int tileY) { + super(tileX, tileY); + } + + public ExampleLevelJob(LoadData save) { + super(save); + } + + @Override + public boolean isValid() { + // Basic job validity + level presence, etc. + return super.isValid(); + } + + @Override + public boolean isValidObject(LevelObject object) { + // Don't clear decorative/player-placed grass + if (getLevel().objectLayer.isPlayerPlaced(this.tileX, this.tileY)) return false; + + // Only target grass objects + return object.object != null && object.object.isGrass; + } + + @Override + public boolean isSameJob(LevelJob other) { + // Helps jobsLayer dedupe so your post doesn't spam the same job repeatedly + return other instanceof ExampleLevelJob + && other.tileX == this.tileX + && other.tileY == this.tileY; + } + + @Override + public boolean shouldSave() { + // Post will recreate jobs as needed + return false; + } + + public static JobSequence getJobSequence( + EntityJobWorker worker, final boolean useItem, final FoundJob foundJob + ) { + LevelObject target = foundJob.job.getObject(); + + // target/object can be null if the job got invalidated between pickup and execution + LocalMessage msg = new LocalMessage( + "activities", + "examplejob", + "target", + target != null && target.object != null ? target.object.getLocalization() : new LocalMessage("ui", "unknown") + ); + + final GameLinkedListJobSequence seq = new GameLinkedListJobSequence(msg); + + seq.add(new MineObjectActiveJob( + worker, + foundJob.priority, + foundJob.job.tileX, + foundJob.job.tileY, + lo -> (!foundJob.job.isRemoved() && foundJob.job.isValidObject(lo)), + foundJob.job.reservable, + "farmingscythe", + 5, + 250, + 0 + ) { + @Override + public void onObjectDestroyed(ObjectDamageResult result) { + addItemPickupJobs(foundJob.priority, result, seq); + foundJob.job.remove(); + } + }); + + return seq; + } +} 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..e48dab5 --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java @@ -0,0 +1,41 @@ +package examplemod.examples.settlement.settlers; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import necesse.engine.localization.message.GameMessage; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.network.server.ServerClient; +import necesse.engine.util.TicketSystemList; +import necesse.entity.mobs.friendly.human.HumanMob; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.InventoryItem; +import necesse.level.maps.levelData.settlementData.ServerSettlementData; +import necesse.level.maps.levelData.settlementData.settler.Settler; + +public class ExampleSettler extends Settler { + + public ExampleSettler() { + // MUST match your registered mob stringID + super("examplesettlermob"); + } + + @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/resources/items/exampleconfigobject.png b/src/main/resources/items/exampleconfigobject.png new file mode 100644 index 0000000000000000000000000000000000000000..24285343a1f7a6bfe7608b95d3676beb2ade6b61 GIT binary patch literal 1271 zcmVz@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHU~0s${)gq~9X00cWpL_t(oh0T}EOB{C?$3HW(yR$FI>TF@5FOgQ0*r2^= zTLo7Wf;pF7L_)|vAP5mD7BNkbO0cGtmeOO>&{~a%hrWOkv~?lDmy?p1V9ddjhg}uj zml=0vdf4u; zxjUJUMadW$=3`MhyzTe|pC#)Pd~|r*Z8BZ%PFkfFuF4%HK?ud#Fz38Mtj90-0a({x zFr*IC=jla}6aqrPrjsxe#Pv7>+8{l0H`P)V07)~+lCjLs>S)n)6adJDu2pKmD|juw zfkDbh5fB3fMr=Tg1U}J+m>1l%ndUG3Zzi>=0uTj77zs5q8JprA_uE`>U$FQhqRsiV zI>VB_3_yca&lR~1peVqN%m$MOh0Xz_N`K`ShnbcBGmP&~@V;0>xmb>XU`63+W{s~7 zzQ$eVCKLz}^aqK%w;4-5;4R)HB!+DAFZ2!WY9o%_;8=uB6^#t3!=#LqWo%tvXI7r0 zuDljmk}X+TlBp}NWmcYJU0=t*03cE+S(V@DlZ4dk~=hqoO?P^|Mk{XTV`T8hc%hd1a6axZzG zJFYJQP$Y#8Z#$#0F-s<2zMu#Py#qkDw{yKdt}-Ma+wx5S`eV0jM$eUnFA;8Cc#0TCwfP#rxkODA6y~;k(-u8hlC2KOeN4{o zVLI_`SO6ey#EBbmQfA8HHAwZSx@w&xp=SEkTWslDZ0TF{tM(MAx=MpoZ=F+S%CdWy zY?JVC5q>B%SR?l{^>gaVUtNR%D?6*)k+VgZFcU1AON^$EP=xs}Mq^`CdEO)7$^y79 zU1L_8v$bKd;j*jka$UM+o#T3(QElvagJ?h-Bw-`~sBxXAO}oslw(F$AuC_~?c9|O2 zc_6zN2DHKB527Bq8=vU=S5Tx{s^*er;0YrK-<{5NT#$lDsF_J^iUnhlq?xq%8~RIbqNSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eV&Y~rV4mpi9Ssyp_jGX# ziD*sUBE0SId4q The Example Settler [buff] examplebuff=Example Buff @@ -58,3 +62,14 @@ exampleincursion=Example Incursion examplemodrootcat=ExampleMod examplemodobjectsubcat=ExampleMod Objects examplemodfurnaturesubcat=ExampleMod Furnature + +[jobs] +weedingname=Weeding +weedingtip=Keeps grass cleared in assigned zones. + +[ui] +examplejobzone=Example Job Zone +examplejobzonedefname=Weeding Zone {number} + +[activities] +examplejob=Weeding {target} diff --git a/src/main/resources/objects/exampleconfigobject.png b/src/main/resources/objects/exampleconfigobject.png new file mode 100644 index 0000000000000000000000000000000000000000..24285343a1f7a6bfe7608b95d3676beb2ade6b61 GIT binary patch literal 1271 zcmVz@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHU~0s${)gq~9X00cWpL_t(oh0T}EOB{C?$3HW(yR$FI>TF@5FOgQ0*r2^= zTLo7Wf;pF7L_)|vAP5mD7BNkbO0cGtmeOO>&{~a%hrWOkv~?lDmy?p1V9ddjhg}uj zml=0vdf4u; zxjUJUMadW$=3`MhyzTe|pC#)Pd~|r*Z8BZ%PFkfFuF4%HK?ud#Fz38Mtj90-0a({x zFr*IC=jla}6aqrPrjsxe#Pv7>+8{l0H`P)V07)~+lCjLs>S)n)6adJDu2pKmD|juw zfkDbh5fB3fMr=Tg1U}J+m>1l%ndUG3Zzi>=0uTj77zs5q8JprA_uE`>U$FQhqRsiV zI>VB_3_yca&lR~1peVqN%m$MOh0Xz_N`K`ShnbcBGmP&~@V;0>xmb>XU`63+W{s~7 zzQ$eVCKLz}^aqK%w;4-5;4R)HB!+DAFZ2!WY9o%_;8=uB6^#t3!=#LqWo%tvXI7r0 zuDljmk}X+TlBp}NWmcYJU0=t*03cE+S(V@DlZ4dk~=hqoO?P^|Mk{XTV`T8hc%hd1a6axZzG zJFYJQP$Y#8Z#$#0F-s<2zMu#Py#qkDw{yKdt}-Ma+wx5S`eV0jM$eUnFA;8Cc#0TCwfP#rxkODA6y~;k(-u8hlC2KOeN4{o zVLI_`SO6ey#EBbmQfA8HHAwZSx@w&xp=SEkTWslDZ0TF{tM(MAx=MpoZ=F+S%CdWy zY?JVC5q>B%SR?l{^>gaVUtNR%D?6*)k+VgZFcU1AON^$EP=xs}Mq^`CdEO)7$^y79 zU1L_8v$bKd;j*jka$UM+o#T3(QElvagJ?h-Bw-`~sBxXAO}oslw(F$AuC_~?c9|O2 zc_6zN2DHKB527Bq8=vU=S5Tx{s^*er;0YrK-<{5NT#$lDsF_J^iUnhlq?xq%8~RIbqNSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eV&Y~rV4mpi9Ssyp_jGX# ziD*sUBE0SId4q Date: Wed, 11 Feb 2026 03:09:53 +0000 Subject: [PATCH 23/28] Rename JobFinder stream patch Rename the patch from JobFinder_StreamFoundJobs_NoNullHandlersPatch to JobFinderStreamFoundJobslHandlersPatch --- ...rsPatch.java => JobFinderStreamFoundJobslHandlersPatch.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/examplemod/examples/patches/{JobFinder_StreamFoundJobs_NoNullHandlersPatch.java => JobFinderStreamFoundJobslHandlersPatch.java} (94%) diff --git a/src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java similarity index 94% rename from src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java rename to src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java index 3c5a81a..ab90209 100644 --- a/src/main/java/examplemod/examples/patches/JobFinder_StreamFoundJobs_NoNullHandlersPatch.java +++ b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java @@ -10,7 +10,7 @@ import net.bytebuddy.asm.Advice; @ModMethodPatch(target = JobFinder.class, name = "streamFoundJobs", arguments = {}) -public class JobFinder_StreamFoundJobs_NoNullHandlersPatch { +public class JobFinderStreamFoundJobslHandlersPatch { @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) static boolean onEnter( From b857df3a684abf8ab10145c36108698510c760cc Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Sun, 15 Feb 2026 05:42:15 +0000 Subject: [PATCH 24/28] Add boss summon item, journal, traps, and biome Introduce boss-summoning and related world features: add ExampleBossSummonItem (+sprite), ExampleModJournal, trap object entity, wall-trap and pressure-plate objects, and a wall-trap sprite. Expose and register tile/object IDs in ExampleModTiles and ExampleModObjects, register the new consumable in ExampleModItems, and update locale entries. Enhance biome support and loot: extend ExampleBiome with cave/deep-cave tiles/rock IDs, generator veins, chest-room presets that use rotating loot, and extra mob drops (including the boss summon item) for cave levels. Update ExampleLootTable with rotation and new loot entries. Behaviour and misc fixes: simplify AI leaf to play a sound packet once, override boss mob collision filter, adjust ExampleBaseRockObject tool tier, add ExampleTrapObjectEntity and ExampleWallTrapObject logic, and reorder registration calls in ExampleMod.init to ensure correct load order. Also rename a patch class for JobFinder stream handlers. --- src/main/java/examplemod/ExampleMod.java | 31 +- .../examplemod/Loaders/ExampleModItems.java | 2 + .../examplemod/Loaders/ExampleModJournal.java | 40 +++ .../examplemod/Loaders/ExampleModObjects.java | 21 +- .../examplemod/Loaders/ExampleModTiles.java | 4 +- .../examplemod/examples/ExampleLootTable.java | 18 +- .../examplemod/examples/ai/ExampleAI.java | 15 +- .../examplemod/examples/ai/ExampleAILeaf.java | 191 +---------- .../consumable/ExampleBossSummonItem.java | 189 +++++++++++ .../examples/maps/biomes/ExampleBiome.java | 308 ++++++++++++------ .../examples/mobs/ExampleBossMob.java | 7 + .../objectentity/ExampleTrapObjectEntity.java | 77 +++++ .../objects/ExampleBaseRockObject.java | 3 +- .../objects/ExamplePressurePlateObject.java | 16 + .../objects/ExampleWallTrapObject.java | 25 ++ ...obFinderStreamFoundJobsHandlersPatch.java} | 2 +- .../resources/items/examplebosssummonitem.png | Bin 0 -> 407 bytes src/main/resources/locale/en.lang | 13 +- .../resources/objects/examplewalltrap.png | Bin 0 -> 1101 bytes 19 files changed, 671 insertions(+), 291 deletions(-) create mode 100644 src/main/java/examplemod/Loaders/ExampleModJournal.java create mode 100644 src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java create mode 100644 src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java create mode 100644 src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java rename src/main/java/examplemod/examples/patches/{JobFinderStreamFoundJobslHandlersPatch.java => JobFinderStreamFoundJobsHandlersPatch.java} (95%) create mode 100644 src/main/resources/items/examplebosssummonitem.png create mode 100644 src/main/resources/objects/examplewalltrap.png diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index d7338b4..85a14b2 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -26,20 +26,35 @@ public ExampleModSettings initSettings() { public void init() { System.out.println("Hello world from my example mod!"); - // The examples are split into different classes here for readability, but you can register them directly here in init if you wish + // Register categories first: Used by Items/Objects to appear correctly in Creative/crafting trees ExampleModCategories.load(); - ExampleModEvents.load(); - ExampleModJobs.load(); - ExampleModBiomes.load(); - ExampleModIncursions.load(); + + // Register packets early: Anything networked (mobs, settlers, job UIs, events) can safely reference packet IDs + ExampleModPackets.load(); + + // Core content building blocks first: Tiles/Objects/Items are referenced by biomes, incursions, mobs, projectiles, buffs, etc. ExampleModTiles.load(); ExampleModObjects.load(); ExampleModItems.load(); - ExampleModMobs.load(); - ExampleModSettlers.load(); + + // Combat + entity registries next: Projectiles and buffs often reference items/mobs, and mobs can reference buffs/projectiles. ExampleModProjectiles.load(); ExampleModBuffs.load(); - ExampleModPackets.load(); + ExampleModMobs.load(); + + // Settlement systems after mobs/items exist: Settlers are mobs; jobs can reference settlers, items, and packets/UI. + ExampleModSettlers.load(); + ExampleModJobs.load(); + + // World generation last-ish: Biomes/incursions can safely reference all registered tiles/objects/mobs/items now. + ExampleModBiomes.load(); + ExampleModIncursions.load(); + + // Events after everything is registered: Lets event listeners safely reference IDs and content without ordering surprises. + ExampleModEvents.load(); + + // Journal last: JournalEntry.addMobEntries() resolves MobRegistry immediately at registration time. + ExampleModJournal.load(); } public void initResources() { diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index 2b37fda..3fd7a0c 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -4,6 +4,7 @@ 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.*; @@ -39,6 +40,7 @@ public static void load(){ // 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/ExampleModJournal.java b/src/main/java/examplemod/Loaders/ExampleModJournal.java new file mode 100644 index 0000000..cf69481 --- /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/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index dda5e77..a86c68e 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -2,10 +2,15 @@ import examplemod.examples.objects.*; import necesse.engine.registries.ObjectRegistry; +import necesse.level.gameObject.WallObject; //NOTE item and crafting categories subject to change 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; + public static void load(){ // Register our objects @@ -16,10 +21,10 @@ public static void load(){ // Register a rock object ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); - ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); + EXAMPLE_BASE_ROCK_ID = ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); // Register an ore rock object that overlays onto our incursion rock - ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + EXAMPLE_ORE_ROCK_ID = ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); // Register a wall object, window object and door object ExampleWallWindowDoorObject.registerWallsDoorsWindows(); @@ -47,5 +52,17 @@ public static void load(){ // Register an object that uses the mods config file ObjectRegistry.registerObject("exampleconfigobject", new ExampleConfigObject(),1,true); + // Register an example pressure plate object + ObjectRegistry.registerObject("examplepressureplateobject",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 "examplewalltrapobject" + WallObject exampleWall = (WallObject) ObjectRegistry.getObject("examplewall"); + ObjectRegistry.registerObject("examplewalltrapobject",new ExampleWallTrapObject(exampleWall),1,true); + + + } } diff --git a/src/main/java/examplemod/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java index f97d9eb..b4d5769 100644 --- a/src/main/java/examplemod/Loaders/ExampleModTiles.java +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -6,9 +6,11 @@ public class ExampleModTiles { + public static int EXAMPLE_TILE_ID = -1; + public static void load(){ // Register our tiles - TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + EXAMPLE_TILE_ID = TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); TileRegistry.registerTile("examplegrasstile", new ExampleGrassTile(),1,false,false,true); } } diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java index 8ad567a..e5ae9f1 100644 --- a/src/main/java/examplemod/examples/ExampleLootTable.java +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -4,6 +4,7 @@ import necesse.inventory.lootTable.lootItem.LootItem; import necesse.inventory.lootTable.lootItem.ChanceLootItem; import necesse.inventory.lootTable.lootItem.OneOfLootItems; +import necesse.inventory.lootTable.lootItem.RotationLootItem; /** * This loot table can be referenced from presets, object entities (like storage boxes), @@ -20,7 +21,16 @@ public class ExampleLootTable { * - groups like "pick one of these" (OneOfLootItems) */ public static final LootTable exampleloottable = new LootTable( - + // Rotating entries: + // This uses the (level + AtomicInteger lootRotation) arguments that chest rooms pass in. + // Position 0 = first item, position 1 = second item, etc. + //TODO make a trinket item + RotationLootItem.presetRotation( + new LootItem("exampleboots"), // position 0 + new LootItem("examplehelmet"), // position 1 + new LootItem("examplechestplate"),// position 2 (example) + new LootItem("examplefood") // position 3 (example) + ), // Guaranteed drops: // LootItem(String itemStringID, int amount) // These are always added when the table is rolled. @@ -38,8 +48,10 @@ public class ExampleLootTable { // 0.60f = 60% chance for this item to be granted IF this option is selected. // Because these are inside OneOfLootItems, the group will choose a single option, // then that option rolls its chance. - new ChanceLootItem(0.60f, "examplesword"), - new ChanceLootItem(0.60f, "examplestaff") + new ChanceLootItem(0.60f, "examplemeleesword"), + new ChanceLootItem(0.60f, "examplemagicstaff"), + new ChanceLootItem(0.60f, "examplesummonorb"), + new ChanceLootItem(0.60f, "examplerangedbow") ) ); diff --git a/src/main/java/examplemod/examples/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java index 22e4b2b..ff6d759 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAI.java +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -9,15 +9,14 @@ public class ExampleAI extends SelectorAINode { - // My custom “fix spawn position” leaf. - // This runs first so the mob gets put somewhere sensible before doing anything else. - public final ExampleAILeaf teleporter; + // Plays a sound when then boss appears + public final ExampleAILeaf soundPlay; - // Vanilla AI that does: find player -> chase -> when close enough, call attackTarget(). + // AI that does: find player -> chase -> when close enough, call attackTarget(). // We keep it as a field so we can reuse the damage/knockback values from it. public final CollisionPlayerChaserAI chaser; - // Vanilla “walk around randomly” node. This is what happens when there’s no player to chase. + // “walk around randomly” node. This is what happens when there’s no player to chase. public final WandererAINode wanderer; public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { @@ -29,12 +28,14 @@ public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wande // 1) Teleport / reposition leaf (highest priority). // (In my leaf: 8 tiles = how far to check for open space, 10 tiles = how far to search for a valid spot) - this.teleporter = new ExampleAILeaf<>(8, 10); - addChild(this.teleporter); + this.soundPlay = new ExampleAILeaf<>(); + addChild(this.soundPlay); // 2) Chase + attack (second priority). this.chaser = new CollisionPlayerChaserAI(searchDistance, damage, knockback) { + + // The chaser decides WHEN it should attack, but it asks us HOW to attack. // So we override this and forward it to our own method below. @Override diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java index 30d128a..4a56704 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -1,204 +1,47 @@ package examplemod.examples.ai; -import java.awt.Point; -import java.awt.geom.Point2D; -import java.util.ArrayList; - import examplemod.examples.packets.ExamplePlaySoundPacket; -import necesse.engine.util.GameMath; -import necesse.engine.util.GameRandom; import necesse.entity.mobs.Mob; import necesse.entity.mobs.ai.behaviourTree.AINode; import necesse.entity.mobs.ai.behaviourTree.AINodeResult; import necesse.entity.mobs.ai.behaviourTree.Blackboard; -import necesse.level.maps.IncursionLevel; -import necesse.level.maps.Level; - -/* - This is a "run once after spawn" AI leaf. - - The problem it solves: - • Sometimes a mob spawns in a bad spot in an Incursion (not in the cleared entrance area) - • If that happens, we move it into the entrance area so the fight / behaviour starts properly - What we consider the "entrance area": - • The centre is the Incursion return portal position - • The biome generator clears open space around that portal -*/ +/** + * Runs exactly once, server-side, and tells nearby clients to play a sound + * at the mob's current position via ExamplePlaySoundPacket. + * Returns FAILURE so the parent tree continues normally (this leaf never "takes over"). + */ public class ExampleAILeaf extends AINode { - // We only want to do the check once per mob. - private boolean didCheck = false; - - // How close you need to be to the portal to count as "in the entrance area" - // (in tiles, converted to pixels later). - private final int openRadiusTiles; - - // How far we search around the portal to find a safe landing spot. - private final int searchRadiusTiles; - - public ExampleAILeaf(int openRadiusTiles, int searchRadiusTiles) { - // Just making sure we don't get silly values. - this.openRadiusTiles = Math.max(1, openRadiusTiles); - this.searchRadiusTiles = Math.max(this.openRadiusTiles, searchRadiusTiles); - } + // Ensure this only fires once per mob instance. + private boolean didRun = false; @Override - protected void onRootSet(AINode aiNode, T t, Blackboard blackboard) { - // Nothing needed here for this leaf. + protected void onRootSet(AINode root, T mob, Blackboard blackboard) { + // No setup needed. } @Override public void init(T mob, Blackboard blackboard) { - // Nothing to init. We just do everything in tick() one time. + // No init needed. } @Override public AINodeResult tick(T mob, Blackboard blackboard) { + // Run once. + if (didRun) return AINodeResult.FAILURE; + didRun = true; - // Don’t keep doing this every tick. We only run it once. - if (didCheck) return AINodeResult.FAILURE; - didCheck = true; - - // Only do this on the server. - // The server is the “real” source of truth for mob position. - if (!mob.isServer()) return AINodeResult.FAILURE; - - Level level = mob.getLevel(); - - // This only applies to Incursion levels. - if (!(level instanceof IncursionLevel)) return AINodeResult.FAILURE; - - IncursionLevel incursion = (IncursionLevel) level; - - // The "entrance centre" is the return portal position. - // If this is null, something is wrong / not generated yet, so bail. - Point portalPos = incursion.getReturnPortalPosition(); - if (portalPos == null) return AINodeResult.FAILURE; - - float centerX = portalPos.x; - float centerY = portalPos.y; - - // If we're already close enough to the portal, we count as “in the entrance area”. - // (Tiles -> pixels. Tiles are 32x32 in Necesse.) - float openRadiusPx = this.openRadiusTiles * 32.0f; - if (mob.getDistance(centerX, centerY) <= openRadiusPx) { - - // Sounds must be played on clients, not on the server. - // So we send a packet to nearby clients telling them to play the sound here if teleport doesnt happen . - mob.getLevel().getServer().network.sendToClientsWithEntity( - new ExamplePlaySoundPacket(mob.x, mob.y), - mob - ); - // Already fine, do nothing. - return AINodeResult.FAILURE; - } - - // We’re NOT in the entrance area, so we need to move the mob. - // Look for a valid spot near the portal. - Point2D.Float dest = findValidTeleportPos(incursion, mob, centerX, centerY, this.searchRadiusTiles); - - // If we found somewhere safe, teleport the mob there. - if (dest != null) { - - // Stop any current movement so we don't fight with pathing. - mob.stopMoving(); - - // Move instantly. - // The "true" here is important: it makes sure the move is synced properly. - mob.setPos(dest.x, dest.y, true); + // Only the server should broadcast packets to clients. + if (mob == null || !mob.isServer()) return AINodeResult.FAILURE; - // Sounds must be played on clients, not on the server. - // So we send a packet to nearby clients telling them to play the sound here if teleport happens . + if (mob.getLevel() != null && mob.getLevel().getServer() != null) { mob.getLevel().getServer().network.sendToClientsWithEntity( new ExamplePlaySoundPacket(mob.x, mob.y), mob ); } - // We always return FAILURE here because this leaf isn't meant to "take over" the AI. - // It's just a one-time fix, then the parent Selector can move on to chase/wander normally. return AINodeResult.FAILURE; } - - /* - Find a safe tile near the portal to teleport to. - - We search in "rings" around the centre: - • r = 0 is the centre tile - • r = 1 is the tiles touching it - • r = 2 is the next ring out, etc - - For each tile we check: - • Not liquid - • Not shore - • Mob doesn't collide with the map - • Mob doesn't collide with another mob/player - */ - private static Point2D.Float findValidTeleportPos(IncursionLevel level, Mob mob, float centerX, float centerY, int searchRadiusTiles) { - int centerTileX = GameMath.getTileCoordinate(centerX); - int centerTileY = GameMath.getTileCoordinate(centerY); - - // Try rings from centre outward until we find something. - for (int r = 0; r <= searchRadiusTiles; r++) { - ArrayList ring = buildRing(centerTileX, centerTileY, r); - - // Shuffle-ish: pick random points so we don't always choose the same spot every time. - while (!ring.isEmpty()) { - Point p = ring.remove(GameRandom.globalRandom.nextInt(ring.size())); - - // Don't teleport into water/shore. - if (level.isLiquidTile(p.x, p.y)) continue; - if (level.isShore(p.x, p.y)) continue; - - // Convert tile coords to world pixel coords. - // +16 puts us in the centre of the tile. - int px = p.x * 32 + 16; - int py = p.y * 32 + 16; - - // Make sure the mob can actually stand there. - if (mob.collidesWith(level, px, py)) continue; - - // Also don't land inside another mob/player. - if (mob.collidesWithAnyMob(level, px, py)) continue; - - // This spot looks good. - return new Point2D.Float(px, py); - } - } - - // Couldn't find a valid spot. - return null; - } - - /* - Build a list of tile points that make up a square "ring" around (cx, cy). - - r = 0: just the centre - r = 1: the outer edge of a 3x3 square - r = 2: the outer edge of a 5x5 square - etc - */ - private static ArrayList buildRing(int cx, int cy, int r) { - ArrayList points = new ArrayList<>(); - - if (r == 0) { - points.add(new Point(cx, cy)); - return points; - } - - // Top + bottom edges - for (int dx = -r; dx <= r; dx++) { - points.add(new Point(cx + dx, cy - r)); - points.add(new Point(cx + dx, cy + r)); - } - - // Left + right edges (skip corners because top/bottom already added them) - for (int dy = -r + 1; dy <= r - 1; dy++) { - points.add(new Point(cx - r, cy + dy)); - points.add(new Point(cx + r, cy + dy)); - } - - return points; - } -} +} \ No newline at end of file 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..bb0f2d4 --- /dev/null +++ b/src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java @@ -0,0 +1,189 @@ +package examplemod.examples.items.consumable; + +import java.awt.geom.Line2D; + +import examplemod.examples.maps.biomes.ExampleBiome; +import necesse.engine.localization.Localization; +import necesse.engine.localization.message.GameMessage; +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.engine.util.LevelIdentifier; +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.placeableItem.consumableItem.ConsumableItem; +import necesse.level.maps.IncursionLevel; +import necesse.level.maps.Level; + +/** + * A consumable item that summons our boss mob. + */ +public class ExampleBossSummonItem extends ConsumableItem { + + public ExampleBossSummonItem() { + // Stack size 1, is "single use" consumable behaviour + super(1, true); + + // Cooldown (ms) before you can use it again + this.itemCooldownTime.setBaseValue(2000); + + // Where it appears in the creative menu + setItemCategory("consumable", "bossitems"); + + // If the player dies, drop this like a material (depending on death penalty rules) + this.dropsAsMatDeathPenalty = true; + + // Search keywords (helps with the in-game search) + this.keyWords.add("boss"); + + // Item rarity colour / tier + this.rarity = Item.Rarity.LEGENDARY; + + // How big the item sprite is when dropped in the world + this.worldDrawSize = 32; + + // How long before the item burns up in fire/lava (30 seconds) + this.incinerationTimeMillis = 30000; + } + + /** + * 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) { + + int tileX; + int tileY; + + // Don't allow boss summoning inside an incursion (special dungeon-like levels) + if (level instanceof IncursionLevel) + return "inincursion"; + + // Only allow use in caves (not surface) + if (!level.isCave) + 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. + if (player == null) { + tileX = GameMath.getTileCoordinate(x); + tileY = GameMath.getTileCoordinate(y); + } else { + tileX = player.getTileX(); + tileY = player.getTileY(); + } + + // Only allow in *cave identifier* AND only if the biome at that tile is our ExampleBiome. + // This prevents using the item in other cave biomes. + if (!level.getIdentifier().equals(LevelIdentifier.CAVE_IDENTIFIER) + || !(level.getBiome(tileX, tileY) instanceof ExampleBiome)) + return "notexamplebiome"; + + // Allowed + 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 we have a real server client player + if (level.isServer() && player != null && player.isServerClient() && error.equals("inincursion")) + player.getServerClient().sendChatMessage(new LocalMessage("misc", "cannotsummoninincursion")); + + // Let vanilla handle the rest (cooldowns, failure behaviour, etc.) + 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()) { + + // If we ARE in an incursion, ask the incursion system if boss summoning is allowed. + // (This also supports the game’s built-in "one boss at a time" rules for incursions.) + if (level instanceof IncursionLevel) { + GameMessage summonError = ((IncursionLevel) level).canSummonBoss("examplebossmob"); + if (summonError != null) { + // Tell the player why it failed + if (player != null && player.isServerClient()) + player.getServerClient().sendChatMessage(summonError); + + // Do NOT consume the item if summoning failed + return item; + } + } + + // Simple debug log + System.out.println("Example Boss Mob has been summoned at " + level.getIdentifier() + "."); + + // Pick a random direction (angle 0-359 degrees) + float 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 = 460F; + + // Create the boss mob instance + Mob mob = MobRegistry.getMob("examplebossmob", 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 + ); + + // Let the incursion know a boss was summoned (so it can track / handle rules) + if (level instanceof IncursionLevel) + ((IncursionLevel) level).onBossSummoned(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 UI (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/maps/biomes/ExampleBiome.java b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java index 3ff999c..64356d0 100644 --- a/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -1,178 +1,302 @@ 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.LootItemInterface; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.inventory.lootTable.lootItem.LootItemList; 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.presets.set.ChestRoomSet; +import necesse.level.maps.presets.set.ColumnSet; +import necesse.level.maps.presets.set.WallSet; import necesse.level.maps.regionSystem.Region; import java.awt.Color; +import java.util.concurrent.atomic.AtomicInteger; /** - * Example overworld biome (1.1.x "infinite surface" style). - * - * Key idea for new modders: - * ------------------------ - * In Necesse 1.1.x, the overworld surface is generated in *regions* by the game's SurfaceLevel. - * SurfaceLevel does NOT create a special "ExampleBiomeLevel" per biome anymore. - * - * Instead, SurfaceLevel asks the Biome for: - * - which ground tile to paint (getGenerationTerrainTileID) - * - what "noise/vein" patterns to use for placing objects (initializeGeneratorStack) - * - what objects to place in each region (generateRegionSurfaceTerrain) - * - * So: if you want a biome that changes the overworld terrain + trees, these are the methods to override. + * Controls: + * - surface ground tile + * - base cave/deep cave tiles + rock objects + * - region decoration passes (surface / cave / deep cave) + * - spawns, music, and structure hooks */ public class ExampleBiome extends Biome { - // -------------------------- - // Spawn tables - // -------------------------- - // MobSpawnTable controls what can spawn in the biome. - // Vanilla provides some defaults; we "include" them and then add our own entries. + // ========================================================================= + // Spawns + // ========================================================================= - /** Small critters (e.g. butterflies, squirrels) for surface regions of this biome. */ public static final MobSpawnTable surfaceCritters = new MobSpawnTable() .include(Biome.defaultSurfaceCritters); - /** Critters for cave levels (if the player is in caves). */ public static final MobSpawnTable caveCritters = new MobSpawnTable() .include(Biome.defaultCaveCritters); - /** Hostile/neutral mobs for the surface. We add our custom mob "examplemob". */ public static final MobSpawnTable surfaceMobs = new MobSpawnTable() - .include(Biome.defaultSurfaceMobs) - .add(30, "examplemob"); // weight: higher = more likely relative to other entries + .add(30, "examplemob"); - /** Hostile/neutral mobs for caves. Usually a different balance than the surface. */ public static final MobSpawnTable caveMobs = new MobSpawnTable() - .include(Biome.defaultCaveMobs) + .add(100, "examplemob"); + + public static final MobSpawnTable deepCaveMobs = new MobSpawnTable() .add(100, "examplemob"); public ExampleBiome() { super(); - - // Generation weight decides how often this biome is chosen when the world is generating new regions. - // Vanilla values are typically around ~0.5 to ~1.5. Keep it in that range while testing. - // (Huge numbers can cause problems with spawn finding and biome distribution.) this.setGenerationWeight(1.0F); } - // -------------------------- - // Overworld world generation hooks (SurfaceLevel -> Biome) - // -------------------------- + // ========================================================================= + // Base tiles / rocks + // ========================================================================= /** - * This is the *base ground tile* that SurfaceLevel paints for this biome in new surface regions. - * - * We look up our custom tile by string ID. If it isn't registered (returns -1), - * we fall back to vanilla grass so the game doesn't break and we can still load the world. + * Surface ground tile. + * If it can't be found, we fall back to grass so worlds still load. */ @Override public int getGenerationTerrainTileID() { int exampleGrass = TileRegistry.getTileID("examplegrasstile"); - if (exampleGrass == -1) { - // -1 means "not found in registry" (usually a typo or missing registration). - return TileRegistry.grassID; - } - return exampleGrass; + return (exampleGrass == -1) ? TileRegistry.grassID : exampleGrass; + } + + /** + * Cave floor tile used in this biome. + */ + @Override + public int getGenerationCaveTileID() { + return ExampleModTiles.EXAMPLE_TILE_ID; + } + + /** + * Cave rock object used in this biome. + */ + @Override + public int getGenerationCaveRockObjectID() { + return ExampleModObjects.EXAMPLE_BASE_ROCK_ID; + } + + /** + * Deep cave floor tile used in this biome. + */ + @Override + public int getGenerationDeepCaveTileID() { + return ExampleModTiles.EXAMPLE_TILE_ID; } /** - * The BiomeGeneratorStack is a helper that stores "noise patterns" used during worldgen. - * - * Think of it like: "make a map of blobs/veins for trees", then when placing objects - * we can say "place trees only on those blobs" to get natural clumps. - * - * This method is called as part of generator setup, not every tick. + * Deep cave rock object used in this biome. + * If you ever add a separate deep version, swap it in here. */ + @Override + public int getGenerationDeepCaveRockObjectID() { + return ExampleModObjects.EXAMPLE_BASE_ROCK_ID; + } + + // Set up the loot interface for our boss summon extra drop + public static LootItemInterface randomExampleBossSummonDrop = new LootItemList(new ChanceLootItem(1.00F, "examplebosssummonitem")); + + // ========================================================================= + // Generator setup (veins) + // ========================================================================= + @Override public void initializeGeneratorStack(BiomeGeneratorStack stack) { super.initializeGeneratorStack(stack); - // Register a simplex-based "vein" pattern named "exampleTrees". - // We'll reference this by name later when placing our trees. - // - // The parameters control clump size / branching / frequency. - // If you want denser or larger clumps, we can tweak these values. + // Trees on the surface stack.addRandomSimplexVeinsBranch("exampleTrees", 2.0F, 0.2F, 1.0F, 0); + + // Ore veins underground + stack.addRandomVeinsBranch("exampleCaveOre", 0.60F, 3, 6, 0.4F, 2, false); + stack.addRandomVeinsBranch("exampleDeepCaveOre", 0.60F, 3, 6, 0.4F, 2, false); } - /** - * Called when SurfaceLevel is generating a specific *region* of the overworld surface. - * - * This is where you place objects like: - * - trees - * - grass tufts - * - flowers - * - rocks, etc. - * - * IMPORTANT: This runs during world generation for new/unexplored regions. - * It does NOT retroactively change already-generated terrain. - */ + // ========================================================================= + // Region passes + // ========================================================================= + @Override public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { super.generateRegionSurfaceTerrain(region, stack, random); - // Cache our terrain tile ID so we place objects only on the correct ground. final int grassTile = getGenerationTerrainTileID(); - // Place our custom tree object using the "exampleTrees" vein pattern. - // This creates forest-like clusters instead of a perfectly even distribution. stack.startPlaceOnVein(this, region, random, "exampleTrees") - .onlyOnTile(grassTile) // only place on our biome's land tile - .chance(0.10D) // density inside valid vein areas (tweak for more/less) + .onlyOnTile(grassTile) + .chance(0.10D) .placeObject("exampletree"); - // Place vanilla "grass" decoration objects on top of our tile. - // This is purely visual and helps the biome feel "alive". stack.startPlace(this, region, random) - .chance(0.40D) // overall density of grass objects + .chance(0.40D) .onlyOnTile(grassTile) - .placeObject("grass"); + .placeObject("examplegrass"); + } + + @Override + public void generateRegionCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionCaveTerrain(region, stack, random); + + // Ore veins: place on our cave rock object + stack.startPlaceOnVein(this, region, random, "exampleCaveOre") + .onlyOnObject(ExampleModObjects.EXAMPLE_BASE_ROCK_ID) + .placeObjectForced("exampleorerock"); + + // If you want crates / small rocks etc, add them here. + // region.updateLiquidManager(); // only needed if you place/edit liquids } - /** - * Used by some debug tools/views to show biomes as solid colors. - * Not required for gameplay, but helpful when testing generation. - */ + @Override + public void generateRegionDeepCaveTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionDeepCaveTerrain(region, stack, random); + + // Ore veins: place on our deep cave rock object (same as base for now) + stack.startPlaceOnVein(this, region, random, "exampleDeepCaveOre") + .onlyOnObject(ExampleModObjects.EXAMPLE_BASE_ROCK_ID) + .placeObjectForced("exampleorerock"); + + // region.updateLiquidManager(); // only needed if you place/edit liquids + } + + // ========================================================================= + // Debug + music + // ========================================================================= + @Override public Color getDebugBiomeColor() { return new Color(128, 0, 128); } - // -------------------------- - // Ambient music + spawns - // -------------------------- - - /** - * Music selection for this biome. Here we reuse vanilla forest music. - * You can swap to a different MusicRegistry path if you want. - */ @Override public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { return new MusicList(MusicRegistry.ForestPath); } + // ========================================================================= + // Boss Summon Drop + // ========================================================================= + + // Add drop to journal + @Override + public LootTable getExtraBiomeMobDrops(LevelIdentifier levelIdentifier) { + if (levelIdentifier == null) + return new LootTable(); + if (levelIdentifier.equals(LevelIdentifier.CAVE_IDENTIFIER)) + return new LootTable(randomExampleBossSummonDrop); + return new LootTable(); + } + + // Add Example Boss Summon Item + @Override + public LootTable getExtraMobDrops(Mob mob) { + if (mob == null || mob.getLevel() == null) return super.getExtraMobDrops(mob); + + // Only in caves (or include deep caves too) + LevelIdentifier id = mob.getLevel().getIdentifier(); + boolean isCave = LevelIdentifier.CAVE_IDENTIFIER.equals(id); + // boolean isDeepCave = LevelIdentifier.DEEP_CAVE_IDENTIFIER.equals(id); + + if (isCave) { + if (mob.isHostile && !mob.isSummoned) return new LootTable(randomExampleBossSummonDrop); + } + return super.getExtraMobDrops(mob); + } + + // ========================================================================= + // Spawn selection + // ========================================================================= - /** - * Critter spawns depend on whether the current level is a cave level. - */ @Override public MobSpawnTable getCritterSpawnTable(Level level) { return level.isCave ? caveCritters : surfaceCritters; } - /** - * Mob spawns depend on whether the current level is a cave level. - */ @Override public MobSpawnTable getMobSpawnTable(Level level) { - return level.isCave ? caveMobs : surfaceMobs; + if (!level.isCave) return surfaceMobs; + if (LevelIdentifier.DEEP_CAVE_IDENTIFIER.equals(level.getIdentifier())) return deepCaveMobs; + return caveMobs; + } + + // ========================================================================= + // Structures / presets + // ========================================================================= + + public RandomCaveChestRoom getNewCaveChestRoomPreset(GameRandom random, AtomicInteger lootRotation) { + + // WallSet("example") will look for: + // examplewall, exampledoor, examplearrowtrap, exampleflametrap, etc. + WallSet exampleWalls = new WallSet("example"); + + // Keep columns the same as stone (optional) + ColumnSet columns = ColumnSet.stone; + +// This "set" is just a bundle of IDs that tells the chest-room preset +// which tiles/objects to use when it builds the room. + ChestRoomSet exampleSet = new ChestRoomSet( + "exampletile", // The floor tile name the preset should use + "examplepressureplateobject", // The pressure plate object to place in the room + exampleWalls, // WallSet we made earlier. It supplies the wall + door + trap object by looking up IDs that start with "example" + columns, // Just the column style (visual decoration) + "storagebox", // The chest object that will be placed in the room + "examplewalltrapobject" // A trap object ID to use (this must be a real registered object ID) + ); + +// Now we build the actual room preset using that set. + return getRandomCaveChestRoom(random, lootRotation, exampleSet); + } + + private static RandomCaveChestRoom getRandomCaveChestRoom( + GameRandom random, + AtomicInteger lootRotation, + ChestRoomSet exampleSet + ) { + // Create the chest room preset. + // - ExampleLootTable.exampleloottable = what items go in the chest + // - lootRotation = the counter used for "rotating" loot between different chest rooms + // - exampleSet = which walls/door/plate/traps/chest to place + RandomCaveChestRoom chestRoom = new RandomCaveChestRoom( + random, + ExampleLootTable.exampleloottable, + lootRotation, + exampleSet + ); + + // The base preset normally uses stone floor tiles. + // This line swaps any stone floor the preset would place into our custom cave tile instead. + chestRoom.replaceTile(TileRegistry.stoneFloorID, ExampleModTiles.EXAMPLE_TILE_ID); + + // Give the preset back to world gen. It will be placed later when the generator picks a location. + return chestRoom; + } + + + public RandomCaveChestRoom getNewDeepCaveChestRoomPreset(GameRandom random, AtomicInteger unique) { + return null; + } + + + public CaveRuins getNewCaveRuinsPreset(GameRandom random, AtomicInteger unique) { + return null; + } + + + public CaveRuins getNewDeepCaveRuinsPreset(GameRandom random, AtomicInteger unique) { + return null; } } diff --git a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java index cab411e..480c49c 100644 --- a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -19,6 +19,7 @@ import necesse.gfx.gameTexture.GameTexture; import necesse.inventory.lootTable.LootTable; import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.maps.CollisionFilter; import necesse.level.maps.Level; import necesse.level.maps.light.GameLight; @@ -48,6 +49,12 @@ public ExampleBossMob() { selectBox = new Rectangle(-14, -7 - 34, 28, 48); } + //ignore level collisions (this will still collide with the player + @Override + public CollisionFilter getLevelCollisionFilter() { + return null; // Level.collides(..., null) returns false in source + } + @Override public void init() { super.init(); 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 index 9379e67..fcb2985 100644 --- a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -7,6 +7,7 @@ public class ExampleBaseRockObject extends RockObject { public ExampleBaseRockObject() { super("examplebaserock", new Color(92, 37, 23), "examplestone", "objects", "landscaping"); - this.toolTier = 5.0F; + // Tier of pickaxe required to mine this rock + this.toolTier = 0.0F; } } 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..42de0f3 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java @@ -0,0 +1,16 @@ +package examplemod.examples.objects; + +import java.awt.Color; + +import necesse.level.gameObject.MaskedPressurePlateObject; + +public class ExamplePressurePlateObject extends MaskedPressurePlateObject { + + public ExamplePressurePlateObject() { + // Map color used on the minimap. + super("pressureplatemask","exampletile",new Color(120, 80, 200)); + + // 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/ExampleWallTrapObject.java b/src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java new file mode 100644 index 0000000..be57596 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWallTrapObject.java @@ -0,0 +1,25 @@ +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 behaviour. + return new ExampleTrapObjectEntity(level, x, y); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java similarity index 95% rename from src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java rename to src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java index ab90209..80085bc 100644 --- a/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobslHandlersPatch.java +++ b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java @@ -10,7 +10,7 @@ import net.bytebuddy.asm.Advice; @ModMethodPatch(target = JobFinder.class, name = "streamFoundJobs", arguments = {}) -public class JobFinderStreamFoundJobslHandlersPatch { +public class JobFinderStreamFoundJobsHandlersPatch { @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) static boolean onEnter( diff --git a/src/main/resources/items/examplebosssummonitem.png b/src/main/resources/items/examplebosssummonitem.png new file mode 100644 index 0000000000000000000000000000000000000000..99c9dd650a3a480f68f1c76d9c4250816b243548 GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-G$*l2rk&Wd@@jkv%n*=n1O*?7=#%aX3dcR3bL1Y`ns~eV&WB-Vn5P&xCAJ4+tbA{ zB%(DrL4tL0!V+e;AX68EmOtulce&b3U1pwjS;f49H=-kyv_xFBMFGXwS_2 zaDv1K0R;*F7k2y0zO#juMxBz1II8?!t;NXT0bl1qX{IT^WMm(3aV`>8w8}_(!I7E( z1UtI5{7RHmuQR4go}0X?br+ul`;uQXUhy>?)#X{RT+%5s|P?jHitUh?is?<~+g2sLxj36=cs7uvX83eZv*|lGjWn4q4R=6V4|l kyl7$I;n{8WP(y;1VdDg^s3|_CAfGdMy85}Sb4q9e0D;?r;s5{u literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 7ded848..44a534e 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -4,7 +4,6 @@ examplegrasstile=Example Grass Tile [object] exampleobject=Example Object -exampleobject2 = Example Object Mod Category examplebaserock=Example Rock exampleore=Example Ore examplesapling=Example Sapling @@ -16,6 +15,8 @@ examplechair=Example Chair exampleleveleventobject=Example Level Event Object examplejobobject=Example Job Object exampleconfigobject=Example Config Object +examplepressureplateobject=Example Pressure Plate +examplewalltrap=Example Wall Trap [item] exampleitem=Example Item @@ -35,11 +36,13 @@ examplearrow=Example Arrow examplehelmet=Example Helmet examplechestplate=Example Chestplate exampleboots=Example Boots +examplebosssummonitem= Example Boss Summon Item [itemtooltip] examplemagicstafftip=Shoots a homing, piercing projectile examplepotionitemtip= An example potion +examplebosssummontip= Use in the Example Biome Cave to summon the Example Boss Mob [mob] examplemob=Example Mob @@ -72,4 +75,10 @@ examplejobzone=Example Job Zone examplejobzonedefname=Weeding Zone {number} [activities] -examplejob=Weeding {target} +examplejob=Weeding + +[journal] +examplebiomesurface=Example Biome Surface +examplebiomecave=Example Biome Cave +examplebiomedeepcave=Example Biome Deep Cave + diff --git a/src/main/resources/objects/examplewalltrap.png b/src/main/resources/objects/examplewalltrap.png new file mode 100644 index 0000000000000000000000000000000000000000..c99f9b676b806fc5ceb82b3e06f2852be27412ec GIT binary patch literal 1101 zcmV-T1hV^yP)002M;1^@s6or`?000001b5ch_0olnc ze*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHV31Ozo~et3xh00WUpL_t(|obBA-Pt-sV2k_ZmJ$^g^C1Swf|7T1vlBh8} zKq5qm7)T8NpN1ovT!9CVBhc>pB8fJ)v$O5(?OpHtd4t~Ww8t{rcDf}300000005X5 zD)tV*?Ah(?=oe~Rt*RC3sxIA|d%89^L`0OT)VzGyNu+&Y0D{PIdRQ*dLPA9JEZJZ` z`&0iSBBE=$(fh^64)<9MKydodd0ReDwvtS(d9};U6%vD2}5u7>$^o}TKhWV3ertz?r<(ofp99Rm=e!W0aw0o|&M zGVAuo>6W8OTdy^c0fDuy?1OI{eINq@Ye_A!+eclHRI?)22QnZ;g$xL+p&8P$TDESN zoC6Nc(7Fv75LkVa2kiqH5LmxyV2G)hNzMTiN{p?&_W&GcA-WF4pKSbt;xPG|el~S) zM)&k3?H2E?`y?8GTp_xvy0oF5KK`iA(l3TcR*05D{J8N$az2a@vJfqS_;KTh-Unv} ztQMlBB!y^+kcIF%@uk{w{6s`Y<#A_U?UfJa!TWJ6*FON80ngJ@oh4bLN473>&v54e zzVW4hfKc&$&GPYE_v^`-E=)1F+r|&9a)oFY#E%<)64(qFx)2?*ZK(Lt86Z^r7=47q4nS8STB1oG zTwB|e<5LK45`PkXjB4vU)sEvBLP9AHqGB_ks}L>06{00=ZSkci`Jv+b^fAu(z84`) z;!grQ0Q)AV?aCK0>Oi?^p-C}&RQx)9ApZ1Xr{KSZ=!OC8g=iPVj~jmyoR{cL3(;?i zca3fXRO9>Hhz$|nM;nemG+cy0RL)9B-{gAHEY>PS_f2kH4=PoSFTMB(6{W4Xg8?WI>h-jM z0q8UhtJ2yGZ$@y4y(+HlkO3Lfx+<+r@Wnt>n1UJH>uIGA1x)*@wAhdV8S}6zEjDC8 zM(kC2^8gtTqCy5_#9oy*50C*FptgK3l4R`4U+Po?)HVi;dSd|6<00000FvI=;Z%y7f TMwDIW00000NkvXXu0mjf Date: Sun, 15 Feb 2026 23:56:24 +0000 Subject: [PATCH 25/28] Add example trinket item and buff Introduce an example trinket and wire it into the mod: add ExampleTrinketItem and ExampleTrinketBuff, register the trinket item and its buff in ExampleModItems and ExampleModBuffs, and include a sprite and localization tooltip. Other changes: - Add locale entries for the trinket and clean up a few tooltip/localization strings. - Improve ExampleArrowBuff: clarify comments, rename variables for clarity, and simplify the heal timing/logic while preserving behavior (server-side heal event usage remains). - Minor cleanup in ExampleAI (remove stray comment line) and add necessary imports. Files added: ExampleTrinketItem.java, ExampleTrinketBuff.java, exampletrinketitem.png Files modified: ExampleModItems.java, ExampleModBuffs.java, ExampleArrowBuff.java, ExampleAI.java, en.lang --- .../examplemod/Loaders/ExampleModBuffs.java | 8 ++ .../examplemod/Loaders/ExampleModItems.java | 4 + .../examplemod/examples/ai/ExampleAI.java | 1 - .../examples/buffs/ExampleArrowBuff.java | 88 ++++++------------ .../examples/buffs/ExampleTrinketBuff.java | 29 ++++++ .../items/trinkets/ExampleTrinketItem.java | 36 +++++++ .../resources/items/exampletrinketitem.png | Bin 0 -> 545 bytes src/main/resources/locale/en.lang | 8 +- 8 files changed, 108 insertions(+), 66 deletions(-) create mode 100644 src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java create mode 100644 src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java create mode 100644 src/main/resources/items/exampletrinketitem.png diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java index f7a5ce7..b09d149 100644 --- a/src/main/java/examplemod/Loaders/ExampleModBuffs.java +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -2,12 +2,20 @@ import examplemod.examples.buffs.*; import necesse.engine.registries.BuffRegistry; +import necesse.entity.mobs.buffs.staticBuffs.Buff; public class ExampleModBuffs { public static void load(){ // Register our buff BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + + // Register our Armor Set Bonus BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBuff()); + + // Register our Arrow Buff BuffRegistry.registerBuff("examplearrowbuff", new ExampleArrowBuff()); + + // Register our Trinket Buff + BuffRegistry.registerBuff("exampletrinketbuff",new ExampleTrinketBuff()); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index 3fd7a0c..cb5e3cd 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -12,6 +12,7 @@ import examplemod.examples.items.tools.ExampleMeleeSwordWeapon; import examplemod.examples.items.tools.ExampleRangedBowWeapon; import examplemod.examples.items.tools.ExampleSummonOrbWeapon; +import examplemod.examples.items.trinkets.ExampleTrinketItem; import necesse.engine.registries.ItemRegistry; public class ExampleModItems { @@ -44,5 +45,8 @@ public static void load(){ // Ammo ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(),5,true); + + // Trinkets + ItemRegistry.registerItem("exampletrinketitem",new ExampleTrinketItem(),1,true); } } diff --git a/src/main/java/examplemod/examples/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java index ff6d759..f078f6f 100644 --- a/src/main/java/examplemod/examples/ai/ExampleAI.java +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -23,7 +23,6 @@ public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wande // A Selector is basically: "try child #1, if it can run then use it, // otherwise try child #2, otherwise child #3..." - // // So the ORDER we add children is the ORDER of priority. // 1) Teleport / reposition leaf (highest priority). diff --git a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java index 7074946..5203dbd 100644 --- a/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -6,94 +6,58 @@ import necesse.entity.mobs.buffs.BuffEventSubscriber; import necesse.entity.mobs.buffs.staticBuffs.Buff; -/** - * ExampleArrowBuff - * - * A server-side buff that periodically heals the mob it is applied to. - * - * Key points: - * - The healing is done on the SERVER in {@link #serverTick(ActiveBuff)} so it is authoritative in multiplayer. - * - The buff uses "gndData" (Game Network Data) to store per-instance state: - * - "healPerTick": how much health to add each heal tick (must be set by whoever applies the buff) - * - "timePassed": internal timer accumulator to control the heal interval - * - A {@link MobHealthChangeEvent} is spawned so the game can display/hear the heal change properly - * (numbers, effects, syncing, etc.), rather than silently changing health. - */ public class ExampleArrowBuff extends Buff { - /** - * How often we apply healing (in milliseconds). - * We simulate this by accumulating time in server ticks (see {@link #serverTick(ActiveBuff)}). - */ + // Heal every 250ms (about 1/4 second) private static final int HEAL_INTERVAL_MS = 250; public ExampleArrowBuff() { - // Player can't right-click/cancel the buff + // Keep this buff hidden + temporary this.canCancel = false; - - // Hide from the UI (no buff icon) this.isVisible = false; - - // Not a passive stat modifier style buff this.isPassive = false; - - // Don't save this buff to disk (intended as temporary / combat effect) this.shouldSave = false; } @Override public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { - // Initialize our per-buff timer accumulator. - // This value lives in the buff instance's gndData so each mob/buff has its own timer. + // Timer for this buff instance buff.getGndData().setInt("timePassed", 0); - // NOTE: We intentionally do NOT set "healPerTick" here. - // Whoever applies the buff should set it, e.g.: - // buff.getGndData().setInt("healPerTick", 2); + // "healPerTick" is set by whoever applies the buff + // buff.getGndData().setInt("healPerTick", 2); } @Override public void serverTick(ActiveBuff buff) { - // The mob that currently has this buff. - Mob m = buff.owner; - if (m == null) return; + Mob mob = buff.owner; + if (mob == null) return; - // How much to heal each time we trigger a heal tick. - // If not set, it will default to 0, meaning "do nothing". - int heal = buff.getGndData().getInt("healPerTick"); - if (heal <= 0) return; + // How much to heal each time (0 = no healing) + int healPerTick = buff.getGndData().getInt("healPerTick"); + if (healPerTick <= 0) return; - /* - * Necesse serverTick runs at a fixed tick rate. - * In this example we assume 1 server tick ≈ 50ms and accumulate time ourselves. - * - * (If tick rate changes, you'd want to accumulate using the actual delta time if available, - * but for many simple buffs, a fixed step like this is fine.) - */ - int accum = buff.getGndData().getInt("timePassed"); - accum += 50; + // Add ~50ms per server tick + int time = buff.getGndData().getInt("timePassed") + 50; - // Not enough time has passed yet: store accumulator and wait for the next tick. - if (accum < HEAL_INTERVAL_MS) { - buff.getGndData().setInt("timePassed", accum); + // Not ready to heal yet + if (time < HEAL_INTERVAL_MS) { + buff.getGndData().setInt("timePassed", time); return; } - // Enough time passed: "spend" one interval worth of time. - // We subtract rather than zeroing so leftover time isn't lost. - accum -= HEAL_INTERVAL_MS; - buff.getGndData().setInt("timePassed", accum); + // Ready: keep leftover time and heal once + buff.getGndData().setInt("timePassed", time - HEAL_INTERVAL_MS); - // Compute how much healing we can actually apply without exceeding max health. - int before = m.getHealth(); - int finalHealth = Math.min(m.getMaxHealth(), before + heal); - int applied = finalHealth - before; + // Heal, but don't go past max health + int newHealth = Math.min(mob.getMaxHealth(), mob.getHealth() + healPerTick); - // If healing would have no effect (already full), do nothing. - if (applied > 0) { - // Spawn a health change event instead of directly forcing health, - // so the engine can properly sync and display the heal. - m.getLevel().entityManager.events.add(new MobHealthChangeEvent(m, finalHealth, applied)); + // If health actually changed, tell the game with an event (sync + floating numbers) + if (newHealth != mob.getHealth()) { + int amountHealed = newHealth - mob.getHealth(); + mob.getLevel().entityManager.events.add( + new MobHealthChangeEvent(mob, newHealth, amountHealed) + ); } } -} +} \ No newline at end of file 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/items/trinkets/ExampleTrinketItem.java b/src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java new file mode 100644 index 0000000..a8314e4 --- /dev/null +++ b/src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java @@ -0,0 +1,36 @@ +package examplemod.examples.items.trinkets; + +import necesse.engine.localization.Localization; +import necesse.engine.registries.BuffRegistry; +import necesse.engine.util.GameBlackboard; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.trinketBuffs.TrinketBuff; +import necesse.gfx.gameTooltips.ListGameTooltips; +import necesse.inventory.InventoryItem; +import necesse.inventory.item.trinketItem.TrinketItem; +import necesse.inventory.lootTable.presets.TrinketsLootTable; + +public class ExampleTrinketItem extends TrinketItem { + + // What buff this trinket gives when equipped + private static final String BUFF_ID = "exampletrinketbuff"; + + public ExampleTrinketItem() { + // Basic trinket settings (rarity, enchant cost, loot group) + super(Rarity.UNCOMMON, 400, TrinketsLootTable.trinkets); + } + + @Override + public TrinketBuff[] getBuffs(InventoryItem inventoryItem) { + // Give the player our buff while the trinket is equipped + return new TrinketBuff[] { (TrinketBuff) BuffRegistry.getBuff(BUFF_ID) }; + } + + @Override + public ListGameTooltips getPreEnchantmentTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { + // Start with normal tooltip, then add 1 extra line + ListGameTooltips t = super.getPreEnchantmentTooltips(item, perspective, blackboard); + t.add(Localization.translate("itemtooltip", "exampletrinkettip")); + return t; + } +} \ No newline at end of file diff --git a/src/main/resources/items/exampletrinketitem.png b/src/main/resources/items/exampletrinketitem.png new file mode 100644 index 0000000000000000000000000000000000000000..27a5131eac54e7f7bf313edb7b3592446df2c543 GIT binary patch literal 545 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%C&rs6b?Si}mUKs7M+SzC z{oH>NSwSk3J%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f-TA0 z-33Sk!B6MiAk#PtJR*x37`TN&n2}-D90{Nxdx@v7EBh-ZesNt*rO0J0Ko_KXx;TbJ zw7#8g=*?^>(qQkpy=ymLUXk9SJGxyRJ6`GwgvgvbnAc?SYQbWTywVKEgZD2)x|FwKgDUq z6`$?>YTGJ(Vhz*nxC@~Pn=Z@?4ti#)$2foW0hQB74BHuG3Rq5L$3115knhtfEwFk) z_S#=o@+Qw_rQa3(Yr0^f&z?_dvifh!IREoz_z2Hx@MJLSJX=@`wDeBB+pjqncBHM- cymP>Q{`{HsxvI}|Pk literal 0 HcmV?d00001 diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 44a534e..3b6a78f 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -36,13 +36,15 @@ examplearrow=Example Arrow examplehelmet=Example Helmet examplechestplate=Example Chestplate exampleboots=Example Boots -examplebosssummonitem= Example Boss Summon Item +examplebosssummonitem=Example Boss Summon Item +exampletrinketitem=Example Trinket Item [itemtooltip] examplemagicstafftip=Shoots a homing, piercing projectile -examplepotionitemtip= An example potion -examplebosssummontip= Use in the Example Biome Cave to summon the Example Boss Mob +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 From 17bb4bd7cc8d95fd2a3e9d74795c3fe7a5a62ed9 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Mon, 16 Feb 2026 23:19:18 +0000 Subject: [PATCH 26/28] Log settings; add boss private loot rotation, add trinket to loot table rotation Call settings.logLoadedSettings() during mod init and make the settings logger public so loaded settings are printed for debugging (added GlobalData import and exposed logLoadedSettings()). Update example loot table to use a trinket item in the rotation (replace exampleboots with exampletrinketitem). Enhance ExampleBossMob loot: add imports for LootItem and RotationLootItem, define a regular lootTable and a private rotating uniqueDrops with a privateLootTable, and implement getPrivateLootTable() so each player can receive a unique boss drop rotation. Add exampletrinketitem to ExampleLootTable rotation --- src/main/java/examplemod/ExampleMod.java | 1 + .../java/examplemod/ExampleModSettings.java | 7 +++--- .../examplemod/examples/ExampleLootTable.java | 4 ++-- .../examples/mobs/ExampleBossMob.java | 24 +++++++++++++++++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 85a14b2..920d719 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -25,6 +25,7 @@ public ExampleModSettings initSettings() { public void init() { System.out.println("Hello world from my example mod!"); + settings.logLoadedSettings(); // log the loaded settings for debug // Register categories first: Used by Items/Objects to appear correctly in Creative/crafting trees ExampleModCategories.load(); diff --git a/src/main/java/examplemod/ExampleModSettings.java b/src/main/java/examplemod/ExampleModSettings.java index a551c07..d7a2f2c 100644 --- a/src/main/java/examplemod/ExampleModSettings.java +++ b/src/main/java/examplemod/ExampleModSettings.java @@ -1,6 +1,7 @@ package examplemod; import necesse.engine.GameLog; +import necesse.engine.GlobalData; import necesse.engine.modLoader.ModSettings; import necesse.engine.save.LoadData; import necesse.engine.save.SaveData; @@ -25,12 +26,10 @@ public void applyLoadData(LoadData data) { exampleBoolean = data.getBoolean("exampleBoolean", exampleBoolean); exampleInt = data.getInt("exampleInt", exampleInt); exampleString = data.getSafeString("exampleString", exampleString, false); - - //log the settings to test - logLoadedSettings(); } - private void logLoadedSettings() { + public void logLoadedSettings() { + GameLog.out.println("[ExampleMod] Settings loaded:"); GameLog.out.println(" exampleBoolean = " + exampleBoolean); GameLog.out.println(" exampleInt = " + exampleInt); diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java index e5ae9f1..2119de5 100644 --- a/src/main/java/examplemod/examples/ExampleLootTable.java +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -21,12 +21,12 @@ public class ExampleLootTable { * - groups like "pick one of these" (OneOfLootItems) */ public static final LootTable exampleloottable = new LootTable( + // Rotating entries: // This uses the (level + AtomicInteger lootRotation) arguments that chest rooms pass in. // Position 0 = first item, position 1 = second item, etc. - //TODO make a trinket item RotationLootItem.presetRotation( - new LootItem("exampleboots"), // position 0 + new LootItem("exampletrinketitem"), // position 0 new LootItem("examplehelmet"), // position 1 new LootItem("examplechestplate"),// position 2 (example) new LootItem("examplefood") // position 3 (example) diff --git a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java index 480c49c..8f0455d 100644 --- a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -19,6 +19,8 @@ import necesse.gfx.gameTexture.GameTexture; import necesse.inventory.lootTable.LootTable; import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.inventory.lootTable.lootItem.LootItem; +import necesse.inventory.lootTable.lootItem.RotationLootItem; import necesse.level.maps.CollisionFilter; import necesse.level.maps.Level; import necesse.level.maps.light.GameLight; @@ -31,10 +33,22 @@ public class ExampleBossMob extends BossMob { // Loaded in examplemod.ExampleMod.initResources() public static GameTexture texture; + // Our regular loot table with a chance item public static LootTable lootTable = new LootTable( - ChanceLootItem.between(0.5f, "exampleitem", 1, 3) // 50% chance to drop between 1-3 example items + ChanceLootItem.between(0.5f, "exampleitem", 1, 3) ); + // Indivitual boss unique loot rotation + public static RotationLootItem uniqueDrops = RotationLootItem.privateLootRotation( + new LootItem("examplemeleesword"), + new LootItem("examplemagicstaff"), + new LootItem("examplesummonorb"), + new LootItem("examplerangedbow")); + + // LootTable for unique drops + public static LootTable privateLootTable = new LootTable( + uniqueDrops); + // MUST HAVE an empty constructor public ExampleBossMob() { super(200); @@ -69,12 +83,18 @@ public void init() { ) ); } - + // Our regular LootTable @Override public LootTable getLootTable() { return lootTable; } + // The boss's Rotating loot table unique to each player + @Override + public LootTable getPrivateLootTable() { + return privateLootTable; + } + @Override public void spawnDeathParticles(float knockbackX, float knockbackY) { // Spawn flesh debris particles From 7d748e291c36bc7098d35edfe688c30a213ceb5c Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Tue, 17 Feb 2026 01:13:24 +0000 Subject: [PATCH 27/28] Refactor example mod IDs, tools, and job logic Renamed and reorganized many example classes/IDs and improved example job logic and items for clarity. Key changes: - Renamed item/object/buff IDs and usages for consistency (e.g. examplearmorsetbonus -> examplearmorsetbonusbuff, exampletrinketitem -> exampletrinket, pressure plate/trap IDs shortened). - Renamed several tool classes/files and updated imports/registrations (ExampleRangedBowWeapon -> ExampleBowRangedWeapon, ExampleMagicStaffWeapon -> ExampleStaffMagicWeapon, ExampleSummonOrbWeapon -> ExampleOrbSummonWeapon); removed old ExampleMeleeSwordWeapon and added ExampleSwordMeleeWeapon. - Job system updates: job type "weeding" -> "examplejobtype", level job registration updated, ExampleJobObjectEntity and ExampleSettlerMob updated to use new IDs. - Reworked ExampleLevelJob: added Javadoc, saving behavior, comprehensive job sequence (MineObjectActiveJob) with reservation, pickup handling and removal on completion. - Buff and armor set: adjusted registration ID and ExampleArmorSetBuff made passive/un-cancellable and documented; ExampleHelmetArmorItem updated to new buff ID. - ExampleGrassTile: clarified behavior, added JavaDoc, cleaned loot logic and off-screen grow scheduling helper; several minor fixes and comments. - Small cleanups: removed unused imports, updated comments/formatting, minor API casts/usage fixes, resource filename tweaks, and several class renames (presets/patches) for clarity. Overall this commit standardizes naming, fixes registrations across loaders/usage sites, and expands example job/item implementations to be more complete and robust. --- .../java/examplemod/ExampleModSettings.java | 1 - .../examplemod/Loaders/ExampleModBuffs.java | 2 +- .../examplemod/Loaders/ExampleModItems.java | 18 ++-- .../examplemod/Loaders/ExampleModJobs.java | 8 +- .../examplemod/Loaders/ExampleModObjects.java | 6 +- .../Loaders/ExampleModResources.java | 2 +- .../examplemod/examples/ExampleLootTable.java | 2 +- .../examples/buffs/ExampleArmorSetBuff.java | 20 +++- .../examples/items/ammo/ExampleArrowItem.java | 4 +- .../items/armor/ExampleHelmetArmorItem.java | 2 +- .../items/consumable/ExamplePotionItem.java | 6 +- .../items/materials/ExampleBarItem.java | 3 +- .../items/materials/ExampleGrassSeedItem.java | 2 - .../ExampleHuntIncursionMaterialItem.java | 3 +- .../items/materials/ExampleLogItem.java | 4 +- .../items/materials/ExampleMaterialItem.java | 3 +- .../items/materials/ExampleOreItem.java | 3 +- .../items/materials/ExampleStoneItem.java | 2 +- ...eapon.java => ExampleBowRangedWeapon.java} | 17 ++-- .../items/tools/ExampleMeleeSwordWeapon.java | 22 ---- ...eapon.java => ExampleOrbSummonWeapon.java} | 16 +-- ...apon.java => ExampleStaffMagicWeapon.java} | 6 +- .../items/tools/ExampleSwordMeleeWeapon.java | 23 +++++ .../examples/maps/biomes/ExampleBiome.java | 4 +- .../maps/incursion/ExampleIncursionBiome.java | 6 +- .../examples/mobs/ExampleSettlerMob.java | 2 +- .../objectentity/ExampleJobObjectEntity.java | 4 +- .../objects/ExampleBaseRockObject.java | 9 +- .../examples/objects/ExampleConfigObject.java | 1 - .../objects/ExampleOreRockObject.java | 22 ++-- .../objects/ExamplePressurePlateObject.java | 6 +- .../objects/ExampleTreeSaplingObject.java | 7 +- ...inderSafe.java => JobFinderSafePatch.java} | 6 +- ...JobFinderStreamFoundJobsHandlersPatch.java | 2 +- ...PresetCode.java => ExampleCodePreset.java} | 12 +-- .../examples/presets/ExamplePreset.java | 22 ++-- .../projectiles/ExampleProjectile.java | 2 +- .../settlement/jobs/ExampleLevelJob.java | 49 +++++++-- .../settlement/settlers/ExampleSettler.java | 5 - .../examples/tiles/ExampleGrassTile.java | 95 +++++++++++------- ...bonus.png => examplearmorsetbonusbuff.png} | Bin ...mpletrinketitem.png => exampletrinket.png} | Bin src/main/resources/locale/en.lang | 14 +-- 43 files changed, 256 insertions(+), 187 deletions(-) rename src/main/java/examplemod/examples/items/tools/{ExampleRangedBowWeapon.java => ExampleBowRangedWeapon.java} (68%) delete mode 100644 src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java rename src/main/java/examplemod/examples/items/tools/{ExampleSummonOrbWeapon.java => ExampleOrbSummonWeapon.java} (55%) rename src/main/java/examplemod/examples/items/tools/{ExampleMagicStaffWeapon.java => ExampleStaffMagicWeapon.java} (96%) create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeapon.java rename src/main/java/examplemod/examples/patches/{JobFinderSafe.java => JobFinderSafePatch.java} (89%) rename src/main/java/examplemod/examples/presets/{ExamplePresetCode.java => ExampleCodePreset.java} (94%) rename src/main/resources/buffs/{examplearmorsetbonus.png => examplearmorsetbonusbuff.png} (100%) rename src/main/resources/items/{exampletrinketitem.png => exampletrinket.png} (100%) diff --git a/src/main/java/examplemod/ExampleModSettings.java b/src/main/java/examplemod/ExampleModSettings.java index d7a2f2c..9536e18 100644 --- a/src/main/java/examplemod/ExampleModSettings.java +++ b/src/main/java/examplemod/ExampleModSettings.java @@ -1,7 +1,6 @@ package examplemod; import necesse.engine.GameLog; -import necesse.engine.GlobalData; import necesse.engine.modLoader.ModSettings; import necesse.engine.save.LoadData; import necesse.engine.save.SaveData; diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java index b09d149..fc48389 100644 --- a/src/main/java/examplemod/Loaders/ExampleModBuffs.java +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -10,7 +10,7 @@ public static void load(){ BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); // Register our Armor Set Bonus - BuffRegistry.registerBuff("examplearmorsetbonus", new ExampleArmorSetBuff()); + BuffRegistry.registerBuff("examplearmorsetbonusbuff", new ExampleArmorSetBuff()); // Register our Arrow Buff BuffRegistry.registerBuff("examplearrowbuff", new ExampleArrowBuff()); diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java index cb5e3cd..2d25289 100644 --- a/src/main/java/examplemod/Loaders/ExampleModItems.java +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -8,10 +8,10 @@ import examplemod.examples.items.consumable.ExampleFoodItem; import examplemod.examples.items.consumable.ExamplePotionItem; import examplemod.examples.items.materials.*; -import examplemod.examples.items.tools.ExampleMagicStaffWeapon; -import examplemod.examples.items.tools.ExampleMeleeSwordWeapon; -import examplemod.examples.items.tools.ExampleRangedBowWeapon; -import examplemod.examples.items.tools.ExampleSummonOrbWeapon; +import examplemod.examples.items.tools.ExampleStaffMagicWeapon; +import examplemod.examples.items.tools.ExampleSwordMeleeWeapon; +import examplemod.examples.items.tools.ExampleBowRangedWeapon; +import examplemod.examples.items.tools.ExampleOrbSummonWeapon; import examplemod.examples.items.trinkets.ExampleTrinketItem; import necesse.engine.registries.ItemRegistry; @@ -28,10 +28,10 @@ public static void load(){ ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(),1,true); // Tools - ItemRegistry.registerItem("examplemeleesword", new ExampleMeleeSwordWeapon(), 20, true); - ItemRegistry.registerItem("examplemagicstaff", new ExampleMagicStaffWeapon(), 30, true); - ItemRegistry.registerItem("examplesummonorb", new ExampleSummonOrbWeapon(),40,true); - ItemRegistry.registerItem("examplerangedbow", new ExampleRangedBowWeapon(),10,true); + ItemRegistry.registerItem("examplemeleesword", new ExampleSwordMeleeWeapon(), 20, true); + ItemRegistry.registerItem("examplemagicstaff", new ExampleStaffMagicWeapon(), 30, true); + ItemRegistry.registerItem("examplesummonorb", new ExampleOrbSummonWeapon(),40,true); + ItemRegistry.registerItem("examplerangedbow", new ExampleBowRangedWeapon(),10,true); // Armor ItemRegistry.registerItem("examplehelmet", new ExampleHelmetArmorItem(), 200f, true); @@ -47,6 +47,6 @@ public static void load(){ ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(),5,true); // Trinkets - ItemRegistry.registerItem("exampletrinketitem",new ExampleTrinketItem(),1,true); + ItemRegistry.registerItem("exampletrinket",new ExampleTrinketItem(),1,true); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModJobs.java b/src/main/java/examplemod/Loaders/ExampleModJobs.java index 8bda7bc..54b6e15 100644 --- a/src/main/java/examplemod/Loaders/ExampleModJobs.java +++ b/src/main/java/examplemod/Loaders/ExampleModJobs.java @@ -10,17 +10,17 @@ public class ExampleModJobs { public static void load(){ // 1) Register the job type - JobTypeRegistry.registerType("weeding", + JobTypeRegistry.registerType("examplejobtype", new JobType( true, // canChangePriority (shows in settlement UI) true, // defaultDisabledBySettler (locked for normal settlers) - new LocalMessage("jobs", "weedingname"), - new LocalMessage("jobs", "weedingtip") + new LocalMessage("jobs", "examplejobname"), + new LocalMessage("jobs", "examplejobtip") ) ); // 2) Register our ExampleLevelJob //DEBUG - LevelJobRegistry.registerJob("weedgrass", ExampleLevelJob .class, "weeding"); + LevelJobRegistry.registerJob("examplejob", ExampleLevelJob .class, "examplejobtype"); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index a86c68e..830fcc3 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -53,14 +53,14 @@ public static void load(){ ObjectRegistry.registerObject("exampleconfigobject", new ExampleConfigObject(),1,true); // Register an example pressure plate object - ObjectRegistry.registerObject("examplepressureplateobject",new ExamplePressurePlateObject(),1,true); + 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 "examplewalltrapobject" + // Takes the texture of the wall object and overlays our "examplewalltrap" WallObject exampleWall = (WallObject) ObjectRegistry.getObject("examplewall"); - ObjectRegistry.registerObject("examplewalltrapobject",new ExampleWallTrapObject(exampleWall),1,true); + ObjectRegistry.registerObject("examplewalltrap",new ExampleWallTrapObject(exampleWall),1,true); diff --git a/src/main/java/examplemod/Loaders/ExampleModResources.java b/src/main/java/examplemod/Loaders/ExampleModResources.java index 0244312..668a259 100644 --- a/src/main/java/examplemod/Loaders/ExampleModResources.java +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -19,7 +19,7 @@ public static void load(){ ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); ExampleSummonWeaponMob.texture = GameTexture.fromFile("mobs/examplesummonmob"); - //initialising the sound to be used by our boss mob + //initializing the sound to be used by our boss mob ExampleMod.EXAMPLESOUND = GameSound.fromFile("examplesound"); // Optional settings (volume/pitch/falloff) – used when playing via SoundSettings diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java index 2119de5..9f397d2 100644 --- a/src/main/java/examplemod/examples/ExampleLootTable.java +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -26,7 +26,7 @@ public class ExampleLootTable { // This uses the (level + AtomicInteger lootRotation) arguments that chest rooms pass in. // Position 0 = first item, position 1 = second item, etc. RotationLootItem.presetRotation( - new LootItem("exampletrinketitem"), // position 0 + new LootItem("exampletrinket"), // position 0 new LootItem("examplehelmet"), // position 1 new LootItem("examplechestplate"),// position 2 (example) new LootItem("examplefood") // position 3 (example) diff --git a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java index ec17866..d5d3548 100644 --- a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java @@ -6,19 +6,33 @@ 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), - new ModifierValue<>(BuffModifiers.SPEED, 0.10f) + new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), // +10% damage + new ModifierValue<>(BuffModifiers.SPEED, 0.10f) // +10% speed ); } @Override public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + // Set bonuses should not be removable by the player. this.canCancel = false; + + // Mark it as a passive buff (always active while wearing the set). this.isPassive = true; + + // Show it in the buff UI. this.isVisible = true; + + // Let the parent class finish setup (applies the modifiers). super.init(buff, eventSubscriber); } -} +} \ 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 index 058b331..3d8d6b0 100644 --- a/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java +++ b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java @@ -2,7 +2,6 @@ import necesse.engine.registries.ProjectileRegistry; import necesse.entity.mobs.GameDamage; -import necesse.entity.mobs.Mob; import necesse.entity.mobs.itemAttacker.ItemAttackerMob; import necesse.entity.projectile.Projectile; import necesse.inventory.item.arrowItem.ArrowItem; @@ -24,14 +23,13 @@ public Projectile getProjectile(float x, float y, float targetX, float targetY, float velocity, int range, GameDamage damage, int knockback, ItemAttackerMob owner) { - // Same exact pattern as StoneArrowItem / IronArrowItem, etc. return ProjectileRegistry.getProjectile( "examplearrowprojectile", // your projectile stringID owner.getLevel(), x, y, targetX, targetY, velocity, range, damage, knockback, - (Mob) owner + owner ); } } diff --git a/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java index 965fe75..1ce4bb1 100644 --- a/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java +++ b/src/main/java/examplemod/examples/items/armor/ExampleHelmetArmorItem.java @@ -18,7 +18,7 @@ public ExampleHelmetArmorItem() { "examplehelmet", //helmet texture name "examplechestplate", //chest item STRING ID "exampleboots", //boots item STRING ID - "examplearmorsetbonus" //buff STRING ID + "examplearmorsetbonusbuff" //buff STRING ID ); } } diff --git a/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java index a2293fa..d9fe2b7 100644 --- a/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java @@ -5,7 +5,11 @@ public class ExamplePotionItem extends SimplePotionItem { public ExamplePotionItem() { - super(100,Rarity.COMMON,"examplebuff",100, "examplepotionitemtip"); + super(100, // Max Stack Size + Rarity.COMMON, // Item Rarity + "examplebuff", // Buff to apply + 100, // Buff Duration in seconds + "examplepotionitemtip"); // Localization text to load and display } } \ 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 index 7251215..83e47a3 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java @@ -6,7 +6,8 @@ public class ExampleBarItem extends MatItem { public ExampleBarItem() { - super(500, Item.Rarity.UNCOMMON); + 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 index 204e7ea..d2e9b63 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -4,7 +4,6 @@ /** * 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 @@ -15,7 +14,6 @@ public class ExampleGrassSeedItem extends GrassSeedItem { public ExampleGrassSeedItem() { // This must match your TileRegistry stringID - // i.e. TileRegistry.registerTile("examplegrasstile", ...) super("examplegrasstile"); } } diff --git a/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java index f54d3fb..7796e8d 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java @@ -5,7 +5,8 @@ public class ExampleHuntIncursionMaterialItem extends MatItem { public ExampleHuntIncursionMaterialItem() { - super(100, Rarity.RARE); + super(100, // Max Stack Size + Rarity.RARE); // Rarity } } diff --git a/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java index 5d2a978..ed190f3 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java @@ -5,7 +5,9 @@ public class ExampleLogItem extends MatItem { public ExampleLogItem() { - super(500,Rarity.UNCOMMON, new String[]{"anylog"}); + super(500, // Max Stack Size + Rarity.UNCOMMON, // Rarity + new String[]{"anylog"}); // Global Ingrediants } } diff --git a/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java index 12e9220..558a20a 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java @@ -5,7 +5,8 @@ public class ExampleMaterialItem extends MatItem { public ExampleMaterialItem() { - super(100, Rarity.UNCOMMON); + 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 index 6a04fcb..4b1ff07 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java @@ -6,7 +6,8 @@ public class ExampleOreItem extends MatItem { public ExampleOreItem() { - super(500, Item.Rarity.UNCOMMON); + 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 index faeff08..720f8c8 100644 --- a/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java @@ -4,6 +4,6 @@ public class ExampleStoneItem extends StonePlaceableItem { public ExampleStoneItem(){ - super(100); + super(100); // Max Stack Size } } diff --git a/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java similarity index 68% rename from src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java index 16740cd..46c8574 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleRangedBowWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java @@ -4,19 +4,20 @@ import necesse.inventory.item.toolItem.projectileToolItem.bowProjectileToolItem.BowProjectileToolItem; import necesse.inventory.lootTable.presets.BowWeaponsLootTable; -public class ExampleRangedBowWeapon extends BowProjectileToolItem { - public ExampleRangedBowWeapon() { +public class ExampleBowRangedWeapon extends BowProjectileToolItem { + public ExampleBowRangedWeapon() { // (enchantCost, lootTableCategory) - super(100, BowWeaponsLootTable.bowWeapons); + super(100, // Enchant Cost + BowWeaponsLootTable.bowWeapons); // Loot Table Category this.rarity = Item.Rarity.NORMAL; // Core stats - this.attackAnimTime.setBaseValue(800); // ms per shot - this.attackDamage.setBaseValue(12.0F); // base bow damage (arrows further modify) - this.attackRange.setBaseValue(600); // tiles-ish range value used by bows - this.velocity.setBaseValue(100); // base projectile velocity (arrows further modify) - this.knockback.setBaseValue(25); + this.attackAnimTime.setBaseValue(800); // Ms Per Shot + this.attackDamage.setBaseValue(12.0F); // Base Bow Damage + this.attackRange.setBaseValue(600); // Range In Tiles (Ish) + this.velocity.setBaseValue(100); // Base Projectile Velocity + this.knockback.setBaseValue(25); // Base Knockback // Sprite offsets (tune until it looks right in-hand) this.attackXOffset = 8; diff --git a/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java deleted file mode 100644 index 1e72fee..0000000 --- a/src/main/java/examplemod/examples/items/tools/ExampleMeleeSwordWeapon.java +++ /dev/null @@ -1,22 +0,0 @@ -package examplemod.examples.items.tools; - -import necesse.inventory.item.Item; -import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; - -// Extends SwordToolItem -public class ExampleMeleeSwordWeapon extends SwordToolItem { - - // Weapon attack textures are loaded from resources/player/weapons/ - - public ExampleMeleeSwordWeapon() { - 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/items/tools/ExampleSummonOrbWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java similarity index 55% rename from src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java index 77d32d7..6df908b 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleSummonOrbWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java @@ -5,14 +5,14 @@ import necesse.inventory.item.toolItem.summonToolItem.SummonToolItem; import necesse.inventory.lootTable.presets.SummonWeaponsLootTable; -public class ExampleSummonOrbWeapon extends SummonToolItem { - public ExampleSummonOrbWeapon() { - // mobStringID, followPosition, summonSpaceTaken, enchantCost, lootTableCategory - super("examplesummonmob", - FollowPosition.PYRAMID, - 1.0F, - 400, - SummonWeaponsLootTable.summonWeapons); +public class ExampleOrbSummonWeapon extends SummonToolItem { + public ExampleOrbSummonWeapon() { + // , followPosition, summonSpaceTaken, enchantCost, lootTableCategory + super("examplesummonmob", // Mob String ID + FollowPosition.PYRAMID, // Follow Position + 1.0F, // Summon Space Taken + 400, // Enchant Cost + SummonWeaponsLootTable.summonWeapons); // Loot Table Category this.rarity = Item.Rarity.UNCOMMON; diff --git a/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java similarity index 96% rename from src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java index b604b4e..ea31f86 100644 --- a/src/main/java/examplemod/examples/items/tools/ExampleMagicStaffWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java @@ -18,13 +18,13 @@ import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleMagicStaffWeapon extends MagicProjectileToolItem { +public class ExampleStaffMagicWeapon 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 ExampleMagicStaffWeapon() { + public ExampleStaffMagicWeapon() { super(400, null); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); @@ -58,7 +58,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/ExampleSwordMeleeWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeapon.java new file mode 100644 index 0000000..c62d569 --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeapon.java @@ -0,0 +1,23 @@ +package examplemod.examples.items.tools; + +import necesse.inventory.item.Item; +import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; + +// Extends SwordToolItem +public class ExampleSwordMeleeWeapon extends SwordToolItem { + + // Weapon attack textures are loaded from resources/player/weapons/ + + public ExampleSwordMeleeWeapon() { + super(400, // Enchant Cost + null); // Loot Table Category (there isn't a built-in one for melee + 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/maps/biomes/ExampleBiome.java b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java index 64356d0..12fb827 100644 --- a/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -250,11 +250,11 @@ public RandomCaveChestRoom getNewCaveChestRoomPreset(GameRandom random, AtomicIn // which tiles/objects to use when it builds the room. ChestRoomSet exampleSet = new ChestRoomSet( "exampletile", // The floor tile name the preset should use - "examplepressureplateobject", // The pressure plate object to place in the room + "examplepressureplate", // The pressure plate object to place in the room exampleWalls, // WallSet we made earlier. It supplies the wall + door + trap object by looking up IDs that start with "example" columns, // Just the column style (visual decoration) "storagebox", // The chest object that will be placed in the room - "examplewalltrapobject" // A trap object ID to use (this must be a real registered object ID) + "examplewalltrap" // A trap object ID to use (this must be a real registered object ID) ); // Now we build the actual room preset using that set. diff --git a/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java index 5a4d9d6..b410505 100644 --- a/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java @@ -77,13 +77,13 @@ public IncursionLevel getNewIncursionLevel(FallenAltarObjectEntity altar, LevelI } /** - * Colors used for the glowing gateway lights on the fallen altar. - * IncursionBiome requires this method; expected to return list of 6 colors. + * Colours used for the glowing gateway lights on the fallen altar. + * IncursionBiome requires this method; expected to return list of 6 colours. */ @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)); diff --git a/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java b/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java index b6b1b8f..933a8f6 100644 --- a/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java @@ -15,7 +15,7 @@ public ExampleSettlerMob() { super(500, 200, "examplesettler"); // Unlock the job type for THIS settler only - this.jobTypeHandler.getPriority("weeding").disabledBySettler = false; + this.jobTypeHandler.getPriority("examplejobtype").disabledBySettler = false; // Give them a tool to clear grass (optional, but nice) this.equipmentInventory.setItem(6, new necesse.inventory.InventoryItem("farmingscythe")); diff --git a/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java index d68130b..bf639e6 100644 --- a/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java +++ b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java @@ -18,7 +18,7 @@ public class ExampleJobObjectEntity extends ObjectEntity { public ExampleJobObjectEntity(Level level, int tileX, int tileY) { // NOTE: ObjectEntity constructor needs (level, type, x, y) - super(level, "exampleweedingpost", tileX, tileY); + super(level, "examplejobobjectentity", tileX, tileY); this.shouldSave = true; this.scanDX = -radiusTiles; @@ -72,7 +72,7 @@ public void serverTick() { LevelObject lo = level.getLevelObject(x, y); if (lo.object == null || !lo.object.isGrass) continue; - // Add your weeding job + // Add your example job level.jobsLayer.addJob(new ExampleLevelJob(x,y)); } } diff --git a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java index fcb2985..b1e6290 100644 --- a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -6,8 +6,11 @@ public class ExampleBaseRockObject extends RockObject { public ExampleBaseRockObject() { - super("examplebaserock", new Color(92, 37, 23), "examplestone", "objects", "landscaping"); - // Tier of pickaxe required to mine this rock - this.toolTier = 0.0F; + super("examplebaserock", // Texture for the base rock + new Color(92, 37, 23), // Mini Map Pixel Colour + "examplestone", // Dropped Stone + "objects", "landscaping"); // Item Categories + + this.toolTier = 0.0F; // 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 index b2bbfb9..abbd329 100644 --- a/src/main/java/examplemod/examples/objects/ExampleConfigObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleConfigObject.java @@ -1,6 +1,5 @@ package examplemod.examples.objects; -import examplemod.ExampleMod; import examplemod.examples.packets.ExampleConfigInteractPacket; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; diff --git a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java index 6379944..340846b 100644 --- a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -10,16 +10,16 @@ public class ExampleOreRockObject extends RockOreObject { public ExampleOreRockObject(RockObject parentRock) { - super( - parentRock, - "oremask", - "exampleore", - new Color(90, 40, 160), - "exampleore", - 1, - 3, - 2, - true, - "objects", "landscaping"); + + super(parentRock, + "oremask", // Ore Mask Image + "exampleore", // Ore Texture Name + new Color(90, 40, 160), // Mini Map Color + "exampleore", // Dropped Ore + 1, // Min Drop + 3, // Max Drop + 2, // Placed Dropped Ore + true, // Is Incrustion Extraction Object + "objects", "landscaping"); // Categories } } diff --git a/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java index 42de0f3..cb71521 100644 --- a/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java +++ b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java @@ -7,8 +7,10 @@ public class ExamplePressurePlateObject extends MaskedPressurePlateObject { public ExamplePressurePlateObject() { - // Map color used on the minimap. - super("pressureplatemask","exampletile",new Color(120, 80, 200)); + // Map colour used on the minimap. + super("pressureplatemask", // Textue Mask + "exampletile", // Tile Texture + new Color(120, 80, 200)); // Mini Map Color // MaskedPressurePlateObject sets the important flags internally (including isPressurePlate) // and uses a default trigger hitbox through its object entity. diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java index 74c8d0b..dd897cc 100644 --- a/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java @@ -5,7 +5,12 @@ public class ExampleTreeSaplingObject extends TreeSaplingObject { public ExampleTreeSaplingObject(){ - super("examplesapling", "exampletree", 1800, 2700, true); + // Add To Any Sapling Global Ingrediant + super("examplesapling", // Texture Name + "exampletree", // Resulting Object String ID + 1800, // Min Grow Time In Seconds + 2700, // Max Grow Time In Seconds + true); // Add To Any Sapling Global Ingrediant } diff --git a/src/main/java/examplemod/examples/patches/JobFinderSafe.java b/src/main/java/examplemod/examples/patches/JobFinderSafePatch.java similarity index 89% rename from src/main/java/examplemod/examples/patches/JobFinderSafe.java rename to src/main/java/examplemod/examples/patches/JobFinderSafePatch.java index 6419196..407bcf1 100644 --- a/src/main/java/examplemod/examples/patches/JobFinderSafe.java +++ b/src/main/java/examplemod/examples/patches/JobFinderSafePatch.java @@ -9,10 +9,10 @@ import necesse.entity.mobs.job.FoundJob; import necesse.entity.mobs.job.JobTypeHandler; -public final class JobFinderSafe { - private JobFinderSafe() {} +public final class JobFinderSafePatch { + private JobFinderSafePatch() {} - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"rawtypes"}) public static Stream safeStreamFoundJobs(JobTypeHandler handler, EntityJobWorker worker) { if (handler == null || worker == null) return Stream.empty(); diff --git a/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java index 80085bc..d5e7499 100644 --- a/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java +++ b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java @@ -18,7 +18,7 @@ static boolean onEnter( @Advice.FieldValue("worker") EntityJobWorker worker, @Advice.Local("out") Stream out ) { - out = JobFinderSafe.safeStreamFoundJobs(handler, worker); + out = JobFinderSafePatch.safeStreamFoundJobs(handler, worker); return true; // skip vanilla method body } diff --git a/src/main/java/examplemod/examples/presets/ExamplePresetCode.java b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java similarity index 94% rename from src/main/java/examplemod/examples/presets/ExamplePresetCode.java rename to src/main/java/examplemod/examples/presets/ExampleCodePreset.java index dd8909a..7d805f4 100644 --- a/src/main/java/examplemod/examples/presets/ExamplePresetCode.java +++ b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java @@ -13,7 +13,7 @@ * This version builds the room using normal Java code (loops and variables), * instead of using a big PRESET={...} text script. */ -public class ExamplePresetCode extends Preset { +public class ExampleCodePreset extends Preset { /** * Constructor @@ -21,7 +21,7 @@ public class ExamplePresetCode extends Preset { * The GameRandom is passed in so things like loot can be randomized, * but still be repeatable (important for world generation). */ - public ExamplePresetCode(GameRandom random) { + 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. @@ -83,7 +83,7 @@ public ExamplePresetCode(GameRandom random) { int storageboxY = height / 2; /* - * Place the storage box object at the center. + * Place the storage box object at the centre. * * setObject(x, y, objectID, rotation) * @@ -106,7 +106,7 @@ public ExamplePresetCode(GameRandom random) { /* * OPTIONAL SAFETY RULE (CanApply predicate): * - * This says: "Only allow this preset to be placed if the area is suitable." + * "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. @@ -115,8 +115,8 @@ public ExamplePresetCode(GameRandom random) { * !level.getTile(levelX, levelY).isFloor * * Meaning: - * - If the tile already IS a floor, then this returns false (bad) - * - If the tile is NOT a floor, then this returns true (good) + * - 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." diff --git a/src/main/java/examplemod/examples/presets/ExamplePreset.java b/src/main/java/examplemod/examples/presets/ExamplePreset.java index 3f814d3..b225fc5 100644 --- a/src/main/java/examplemod/examples/presets/ExamplePreset.java +++ b/src/main/java/examplemod/examples/presets/ExamplePreset.java @@ -6,20 +6,12 @@ /** * ExamplePreset (Script-based) - * - * This preset is the same idea as your code-built room, but it is created using a big text string + * 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. - * - * Think of it like this: - * - The big string contains a saved layout (tiles + objects + rotations) - * - applyScript(...) reads that string and loads it into this Preset - * - Then we do extra steps (like filling a chest with loot) */ public class ExamplePreset extends Preset { /** - * Constructor - * * You pass in GameRandom so anything random (like loot) can be rolled properly. * In world generation, Necesse often uses a seeded random so the same world seed * produces the same results every time. @@ -36,18 +28,18 @@ public ExamplePreset(GameRandom random) { * It's basically a "saved blueprint" of a structure. * The game can export these, and you can paste them into code like this. * - * The important parts (high level): + * The important parts * * width / height * - Size of the structure. * * tileIDs + tiles - * - "tileIDs" is a small list (palette) of tile types used in this preset. + * - "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 (walls, torches, air, storagebox). + * - Same idea as tiles, but for objects * - "objectIDs" is the palette. * - "objects" is the full grid. * @@ -84,7 +76,7 @@ public ExamplePreset(GameRandom random) { /* * Add loot into the storage box inside the preset. * - * IMPORTANT IDEA: + * The idea here is: * Coordinates here are PRESET coordinates, not world coordinates. * * So (5, 5) means: @@ -112,8 +104,8 @@ public ExamplePreset(GameRandom random) { * * The ! means "not". * So: - * - if isFloor is true, !isFloor is false (so placement fails) - * - if isFloor is false, !isFloor is true (so placement is allowed) + * - 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/ExampleProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java index 3b247c0..6ce8da7 100644 --- a/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java +++ b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java @@ -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 index 953966e..c7f7dc2 100644 --- a/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java +++ b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java @@ -12,34 +12,43 @@ import necesse.level.maps.levelData.jobs.LevelJob; import necesse.level.maps.levelData.jobs.MineObjectLevelJob; +/** + * 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 { + // Create a new job at a tile position public ExampleLevelJob(int tileX, int tileY) { super(tileX, tileY); } + // Create a job from saved data (not used if shouldSave() returns false) public ExampleLevelJob(LoadData save) { super(save); } @Override public boolean isValid() { - // Basic job validity + level presence, etc. + // Use the base checks (it will call isValidObject on the current object) return super.isValid(); } @Override public boolean isValidObject(LevelObject object) { - // Don't clear decorative/player-placed grass + // Do NOT let settlers clear objects that a player placed. if (getLevel().objectLayer.isPlayerPlaced(this.tileX, this.tileY)) return false; - // Only target grass objects + // Only allow this job to target grass objects. return object.object != null && object.object.isGrass; } @Override public boolean isSameJob(LevelJob other) { - // Helps jobsLayer dedupe so your post doesn't spam the same job repeatedly + // Jobs system uses this to avoid duplicates. + // If another ExampleLevelJob exists at the same tile, treat it as the same job. return other instanceof ExampleLevelJob && other.tileX == this.tileX && other.tileY == this.tileY; @@ -47,44 +56,68 @@ public boolean isSameJob(LevelJob other) { @Override public boolean shouldSave() { - // Post will recreate jobs as needed + // Don't save this job. The settlement can recreate it later if needed. return false; } + /** + * This builds the actual steps the settler will do. + * Here we only add ONE step: mine/destroy the grass object. + */ public static JobSequence getJobSequence( EntityJobWorker worker, final boolean useItem, final FoundJob foundJob ) { + // Get the current object at the job tile (might be null if it changed) LevelObject target = foundJob.job.getObject(); - // target/object can be null if the job got invalidated between pickup and execution + // Message shown for the job (in settlement UI) LocalMessage msg = new LocalMessage( "activities", "examplejob", "target", - target != null && target.object != null ? target.object.getLocalization() : new LocalMessage("ui", "unknown") + (target != null && target.object != null) + ? target.object.getLocalization() + : new LocalMessage("ui", "unknown") ); + // A list of work steps final GameLinkedListJobSequence seq = new GameLinkedListJobSequence(msg); + // Add the work step: go to tile + hit the object until it breaks seq.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)), + + // Reservation (stops 2 settlers trying to do the same tile) foundJob.job.reservable, + + // Item used for the "swing" animation (visual only) "farmingscythe", + + // Damage per hit to the object 5, + + // Time per swing (ms) 250, + + // Extra delay between swings (ms) 0 ) { @Override public void onObjectDestroyed(ObjectDamageResult result) { + // Make pickup jobs for any drops addItemPickupJobs(foundJob.priority, result, seq); + + // Remove the job so it doesn't stay posted foundJob.job.remove(); } }); return seq; } -} +} \ 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 index e48dab5..2512e79 100644 --- a/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java +++ b/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java @@ -1,16 +1,11 @@ package examplemod.examples.settlement.settlers; -import java.util.Collections; -import java.util.List; import java.util.function.Supplier; - import necesse.engine.localization.message.GameMessage; import necesse.engine.localization.message.LocalMessage; -import necesse.engine.network.server.ServerClient; import necesse.engine.util.TicketSystemList; import necesse.entity.mobs.friendly.human.HumanMob; import necesse.gfx.gameTexture.GameTexture; -import necesse.inventory.InventoryItem; import necesse.level.maps.levelData.settlementData.ServerSettlementData; import necesse.level.maps.levelData.settlementData.settler.Settler; diff --git a/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java index d7f6c8b..46a5f5a 100644 --- a/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java +++ b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java @@ -7,7 +7,6 @@ import necesse.engine.util.GameMath; import necesse.engine.util.GameRandom; import necesse.gfx.gameTexture.GameTextureSection; -import necesse.inventory.lootTable.LootItemInterface; import necesse.inventory.lootTable.LootTable; import necesse.inventory.lootTable.lootItem.ChanceLootItem; import necesse.level.gameObject.GameObject; @@ -15,71 +14,88 @@ import necesse.level.maps.Level; import necesse.level.maps.regionSystem.SimulatePriorityList; +/** + * ExampleGrassTile + * This is a ground tile. + * It does 3 main things: + * 1) 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 { - // You can tweak these to change growth/spread speeds - public static double growChance = GameMath.getAverageSuccessRuns(7000.0D); + + // How often the grass OBJECT should grow on this tile + public static double growChance = GameMath.getAverageSuccessRuns(7000.0D); + + // How often this TILE should spread onto dirt next to it public static double spreadChance = GameMath.getAverageSuccessRuns(850.0D); + // Used only for picking a random sprite row (visual variation) private final GameRandom drawRandom = new GameRandom(); public ExampleGrassTile() { - // IMPORTANT: this string must match your texture name in resources/tiles/ - // (e.g. resources/tiles/examplegrass.png) + // Texture file: resources/tiles/examplegrasstile.png super(false, "examplegrasstile"); - this.mapColor = new Color(70, 120, 40); // minimap color - this.canBeMined = true; - this.isOrganic = true; + this.mapColor = new Color(70, 120, 40); // minimap colour + this.canBeMined = true; // player can mine/remove it + this.isOrganic = true; // marks it as organic } @Override public LootTable getLootTable(Level level, int tileX, int tileY) { - // Option A: drop vanilla grassseed - // return new LootTable(new ChanceLootItem(0.04F, "grassseed")); - - // Option B: drop your own seed item (if you register one) - return new LootTable(new LootItemInterface[]{ - new ChanceLootItem(0.04F, "examplegrassseed") - }); + // 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) { + public void addSimulateLogic(Level level, int x, int y, long ticks, + SimulatePriorityList list, boolean sendChanges) { + // Off-screen simulation: schedule growth while the chunk is not actively ticking addSimulateGrow(level, x, y, growChance, ticks, "examplegrass", list, sendChanges); } - // Same helper pattern vanilla uses (simplified) + /** + * Off-screen growth: schedule placing the grass object after enough simulated time passes. + */ public static void addSimulateGrow(Level level, int tileX, int tileY, double chance, long ticks, String growObjectID, SimulatePriorityList list, boolean sendChanges) { - if (level.getObjectID(tileX, tileY) == 0) { - double runs = Math.max(1.0D, GameMath.getRunsForSuccess(chance, GameRandom.globalRandom.nextDouble())); - long remainingTicks = (long)(ticks - runs); - if (remainingTicks > 0L) { - GameObject obj = ObjectRegistry.getObject(ObjectRegistry.getObjectID(growObjectID)); - if (obj.canPlace(level, tileX, tileY, 0, false) == null) { - list.add(tileX, tileY, remainingTicks, () -> { - if (obj.canPlace(level, tileX, tileY, 0, false) == null) { - obj.placeObject(level, tileX, tileY, 0, false); - level.objectLayer.setIsPlayerPlaced(tileX, tileY, false); - if (sendChanges) level.sendObjectUpdatePacket(tileX, tileY); - } - }); - } + + // Only grow if there is no object on this tile + if (level.getObjectID(tileX, tileY) != 0) return; + + // Convert the chance into a rough amount of time before it should succeed + double runs = Math.max(1.0D, GameMath.getRunsForSuccess(chance, GameRandom.globalRandom.nextDouble())); + long remainingTicks = (long) (ticks - runs); + if (remainingTicks <= 0L) return; + + GameObject obj = ObjectRegistry.getObject(ObjectRegistry.getObjectID(growObjectID)); + + // canPlace == null means it's allowed to place here + if (obj.canPlace(level, tileX, tileY, 0, false) != null) return; + + // Add a delayed task to place the object later + list.add(tileX, tileY, remainingTicks, () -> { + if (obj.canPlace(level, tileX, tileY, 0, false) == null) { + obj.placeObject(level, tileX, tileY, 0, false); + level.objectLayer.setIsPlayerPlaced(tileX, tileY, false); // natural growth + if (sendChanges) level.sendObjectUpdatePacket(tileX, tileY); } - } + }); } @Override public double spreadToDirtChance() { - // This is what makes dirt convert into your grass when adjacent + // Controls how fast dirt turns into this grass tile when nearby return spreadChance; } @Override public void tick(Level level, int x, int y) { + // Only the server should change the world if (!level.isServer()) return; - // Grow your grass object on empty tiles + // Grow the grass OBJECT on empty tiles sometimes if (level.getObjectID(x, y) == 0 && GameRandom.globalRandom.getChance(growChance)) { GameObject grassObj = ObjectRegistry.getObject(ObjectRegistry.getObjectID("examplegrass")); if (grassObj.canPlace(level, x, y, 0, false) == null) { @@ -92,15 +108,18 @@ public void tick(Level level, int x, int y) { @Override public Point getTerrainSprite(GameTextureSection terrainTexture, Level level, int tileX, int tileY) { - int tile; + // Pick a random row for the sprite, but keep it consistent per tile position + int row; synchronized (drawRandom) { - tile = drawRandom.seeded(getTileSeed(tileX, tileY)).nextInt(terrainTexture.getHeight() / 32); + row = drawRandom.seeded(getTileSeed(tileX, tileY)) + .nextInt(terrainTexture.getHeight() / 32); } - return new Point(0, tile); // column 0, random row + return new Point(0, row); // column 0, chosen row } @Override public int getTerrainPriority() { - return 100; // same as vanilla grass + // Used when tiles overlap/compete in drawing/spreading rules + return 100; } } \ No newline at end of file diff --git a/src/main/resources/buffs/examplearmorsetbonus.png b/src/main/resources/buffs/examplearmorsetbonusbuff.png similarity index 100% rename from src/main/resources/buffs/examplearmorsetbonus.png rename to src/main/resources/buffs/examplearmorsetbonusbuff.png diff --git a/src/main/resources/items/exampletrinketitem.png b/src/main/resources/items/exampletrinket.png similarity index 100% rename from src/main/resources/items/exampletrinketitem.png rename to src/main/resources/items/exampletrinket.png diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 3b6a78f..17f4755 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -15,7 +15,7 @@ examplechair=Example Chair exampleleveleventobject=Example Level Event Object examplejobobject=Example Job Object exampleconfigobject=Example Config Object -examplepressureplateobject=Example Pressure Plate +examplepressureplate=Example Pressure Plate examplewalltrap=Example Wall Trap [item] @@ -37,7 +37,7 @@ examplehelmet=Example Helmet examplechestplate=Example Chestplate exampleboots=Example Boots examplebosssummonitem=Example Boss Summon Item -exampletrinketitem=Example Trinket Item +exampletrinket=Example Trinket [itemtooltip] @@ -55,7 +55,7 @@ examplesettlermobname= The Example Settler [buff] examplebuff=Example Buff -examplearmorsetbonus=Example Armor Set Bonus +examplearmorsetbonusbuff=Example Armor Set Bonus Buff [biome] exampleincursion=Example Incursion @@ -69,15 +69,15 @@ examplemodobjectsubcat=ExampleMod Objects examplemodfurnaturesubcat=ExampleMod Furnature [jobs] -weedingname=Weeding -weedingtip=Keeps grass cleared in assigned zones. +examplejobname=Example Job +examplejobtip=Keeps grass cleared in assigned zones. [ui] examplejobzone=Example Job Zone -examplejobzonedefname=Weeding Zone {number} +examplejobzonedefname=Example Job Zone {number} [activities] -examplejob=Weeding +examplejob=Doing Example Job [journal] examplebiomesurface=Example Biome Surface From 8d3ccbe4b7eba153c14241ca44864c2c5216f7da Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Tue, 17 Feb 2026 01:28:54 +0000 Subject: [PATCH 28/28] Use examplewall (ID 1436) in preset Update ExamplePreset to replace woodwall (ID 85) with examplewall (ID 1436). The objectIDs and objects arrays were modified to reference the new object ID across the preset layout so the preset uses the examplewall asset instead of the previous woodwall entries. --- src/main/java/examplemod/examples/presets/ExamplePreset.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/examplemod/examples/presets/ExamplePreset.java b/src/main/java/examplemod/examples/presets/ExamplePreset.java index b225fc5..e19d01b 100644 --- a/src/main/java/examplemod/examples/presets/ExamplePreset.java +++ b/src/main/java/examplemod/examples/presets/ExamplePreset.java @@ -57,8 +57,8 @@ public ExamplePreset(GameRandom random) { "PRESET={width=11,height=11," + "tileIDs=[98, exampletile]," + "tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98]," + - "objectIDs=[0, air, 290, storagebox, 85, woodwall, 298, walltorch]," + - "objects=[85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 290, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 85, 85, 298, 0, 0, 0, 0, 0, 0, 0, 298, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85]," + + "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";