From 3b1992666dcb97cd2f72a679148d5f15ce399423 Mon Sep 17 00:00:00 2001 From: Mads Skovgaard Date: Mon, 2 Feb 2026 12:51:36 +0100 Subject: [PATCH 1/7] Fixes missing preAntialiasTextures gradle task. --- build.gradle | 10 ++++++++++ 1 file 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" From d25686248ea3369434e70542d3ff9fc5e37e7e9b Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Fri, 20 Feb 2026 18:57:36 +0000 Subject: [PATCH 2/7] inline with main Break out ExampleMod initialization into focused loader classes and add configurable settings/resources. Introduces ExampleModSettings for mod config and logging, and ExampleModResources for textures and sound setup (GameSound + SoundSettings). Move inline registrations and recipe/command/packet logic into new loaders under examplemod.Loaders (Biomes, Buffs, Categories, Commands, Events, Incursions, Items, Jobs, Journal, Mobs, Objects, Packets, Projectiles, Recipes, Resources, Settlers, Tiles) and call them from ExampleMod.init/postInit/initResources. Remove many inline registrations and recipe code from ExampleMod, register recipes via ExampleModRecipes.registerRecipes(), and register commands/packets via their loader classes. Adds numerous example classes and resource assets, reorganizes example packages, and cleans up initialization order to ensure safe cross-references between tiles/objects/items/mobs/etc. --- src/main/java/examplemod/ExampleMod.java | 132 +++----- .../java/examplemod/ExampleModSettings.java | 37 +++ .../examplemod/Loaders/ExampleModBiomes.java | 15 + .../examplemod/Loaders/ExampleModBuffs.java | 21 ++ .../Loaders/ExampleModCategories.java | 118 +++++++ .../Loaders/ExampleModCommands.java | 13 + .../examplemod/Loaders/ExampleModEvents.java | 33 ++ .../Loaders/ExampleModIncursions.java | 17 + .../examplemod/Loaders/ExampleModItems.java | 52 +++ .../examplemod/Loaders/ExampleModJobs.java | 26 ++ .../examplemod/Loaders/ExampleModJournal.java | 40 +++ .../examplemod/Loaders/ExampleModMobs.java | 23 ++ .../examplemod/Loaders/ExampleModObjects.java | 68 ++++ .../examplemod/Loaders/ExampleModPackets.java | 20 ++ .../Loaders/ExampleModProjectiles.java | 15 + .../examplemod/Loaders/ExampleModRecipes.java | 181 +++++++++++ .../Loaders/ExampleModResources.java | 33 ++ .../Loaders/ExampleModSettlers.java | 14 + .../examplemod/Loaders/ExampleModTiles.java | 16 + .../examplemod/examples/ExampleBiome.java | 36 --- .../examplemod/examples/ExampleLootTable.java | 64 ++++ .../examplemod/examples/ExampleSwordItem.java | 21 -- .../examplemod/examples/ai/ExampleAI.java | 65 ++++ .../examplemod/examples/ai/ExampleAILeaf.java | 47 +++ .../examples/buffs/ExampleArmorSetBuff.java | 38 +++ .../examples/buffs/ExampleArrowBuff.java | 63 ++++ .../examples/{ => buffs}/ExampleBuff.java | 2 +- .../examples/buffs/ExampleTrinketBuff.java | 29 ++ .../examples/events/ExampleEvent.java | 35 ++ .../examples/events/ExampleLevelEvent.java | 176 ++++++++++ .../examples/items/ExamplePotionItem.java | 11 - .../examples/items/ammo/ExampleArrowItem.java | 35 ++ .../items/armor/ExampleBootsArmorItem.java | 16 + .../items/armor/ExampleChestArmorItem.java | 17 + .../items/armor/ExampleHelmetArmorItem.java | 24 ++ .../consumable/ExampleBossSummonItem.java | 189 +++++++++++ .../{ => consumable}/ExampleFoodItem.java | 2 +- .../items/consumable/ExamplePotionItem.java | 15 + .../items/materials/ExampleBarItem.java | 13 + .../items/materials/ExampleGrassSeedItem.java | 19 ++ .../ExampleHuntIncursionMaterialItem.java | 5 +- .../items/materials/ExampleLogItem.java | 13 + .../{ => materials}/ExampleMaterialItem.java | 5 +- .../items/materials/ExampleOreItem.java | 13 + .../items/materials/ExampleStoneItem.java | 9 + .../items/tools/ExampleBowRangedWeapon.java | 32 ++ .../items/tools/ExampleOrbSummonWeapon.java | 29 ++ .../tools/ExampleStaffMagicWeapon.java} | 11 +- .../items/tools/ExampleSwordMeleeWeapon.java | 23 ++ .../items/trinkets/ExampleTrinketItem.java | 36 +++ .../examples/maps/biomes/ExampleBiome.java | 302 ++++++++++++++++++ .../incursion}/ExampleIncursionBiome.java | 14 +- .../incursion}/ExampleIncursionLevel.java | 63 ++-- .../examples/mobs/ExampleBossMob.java | 161 ++++++++++ .../examples/{ => mobs}/ExampleMob.java | 2 +- .../examples/mobs/ExampleSettlerMob.java | 46 +++ .../examples/mobs/ExampleSummonWeaponMob.java | 87 +++++ .../objectentity/ExampleJobObjectEntity.java | 79 +++++ .../objectentity/ExampleObjectEntity.java | 97 ++++++ .../objectentity/ExampleTrapObjectEntity.java | 77 +++++ .../objects/ExampleBaseRockObject.java | 16 + .../examples/objects/ExampleConfigObject.java | 86 +++++ .../examples/objects/ExampleGrassObject.java | 21 ++ .../examples/objects/ExampleJobObject.java | 79 +++++ .../objects/ExampleLevelEventObject.java | 89 ++++++ .../examples/{ => objects}/ExampleObject.java | 2 +- .../objects/ExampleOreRockObject.java | 25 ++ .../objects/ExamplePressurePlateObject.java | 18 ++ .../examples/objects/ExampleTreeObject.java | 27 ++ .../objects/ExampleTreeSaplingObject.java | 17 + .../objects/ExampleWallTrapObject.java | 25 ++ .../objects/ExampleWallWindowDoorObject.java | 38 +++ .../objects/ExampleWoodChairObject.java | 11 + .../packets/ExampleConfigInteractPacket.java | 64 ++++ .../examples/{ => packets}/ExamplePacket.java | 2 +- .../packets/ExamplePlaySoundPacket.java | 45 +++ .../ExampleConstructorPatch.java | 3 +- .../{ => patches}/ExampleMethodPatch.java | 2 +- .../examples/patches/JobFinderSafePatch.java | 36 +++ ...JobFinderStreamFoundJobsHandlersPatch.java | 30 ++ .../examples/presets/ExampleCodePreset.java | 128 ++++++++ .../examples/presets/ExamplePreset.java | 114 +++++++ .../projectiles/ExampleArrowProjectile.java | 157 +++++++++ .../{ => projectiles}/ExampleProjectile.java | 4 +- .../settlement/jobs/ExampleLevelJob.java | 123 +++++++ .../settlement/settlers/ExampleSettler.java | 36 +++ .../examples/tiles/ExampleGrassTile.java | 125 ++++++++ .../examples/{ => tiles}/ExampleTile.java | 2 +- .../buffs/examplearmorsetbonusbuff.png | Bin 0 -> 398 bytes src/main/resources/items/examplearrow.png | Bin 0 -> 464 bytes src/main/resources/items/examplebar.png | Bin 0 -> 784 bytes src/main/resources/items/examplebaserock.png | Bin 0 -> 1025 bytes src/main/resources/items/exampleboots.png | Bin 0 -> 406 bytes .../resources/items/examplebosssummonitem.png | Bin 0 -> 407 bytes src/main/resources/items/examplechair.png | Bin 0 -> 423 bytes .../resources/items/examplechestplate.png | Bin 0 -> 648 bytes .../resources/items/exampleconfigobject.png | Bin 0 -> 1271 bytes src/main/resources/items/exampledoor.png | Bin 0 -> 427 bytes src/main/resources/items/examplefood.png | Bin 0 -> 431 bytes src/main/resources/items/examplefooditem.png | Bin 354 -> 0 bytes src/main/resources/items/examplegrass.png | Bin 0 -> 465 bytes src/main/resources/items/examplegrassseed.png | Bin 0 -> 535 bytes src/main/resources/items/examplehelmet.png | Bin 0 -> 534 bytes .../items/examplehuntincursionitem.png | Bin 317 -> 0 bytes .../items/examplehuntincursionmaterial.png | Bin 0 -> 284 bytes .../items/exampleincursiontablet.png | Bin 332 -> 463 bytes src/main/resources/items/examplejobobject.png | Bin 0 -> 291 bytes .../items/exampleleveleventobject.png | Bin 0 -> 289 bytes src/main/resources/items/examplelog.png | Bin 0 -> 477 bytes .../resources/items/examplemagicstaff.png | Bin 0 -> 451 bytes .../resources/items/examplemeleesword.png | Bin 0 -> 464 bytes src/main/resources/items/exampleore.png | Bin 0 -> 549 bytes src/main/resources/items/exampleorerock.png | Bin 0 -> 1025 bytes src/main/resources/items/examplepotion.png | Bin 0 -> 599 bytes .../resources/items/examplepotionitem.png | Bin 469 -> 0 bytes src/main/resources/items/examplerangedbow.png | Bin 0 -> 418 bytes src/main/resources/items/examplesapling.png | Bin 0 -> 514 bytes src/main/resources/items/examplestaff.png | Bin 444 -> 0 bytes src/main/resources/items/examplestone.png | Bin 0 -> 520 bytes src/main/resources/items/examplesummonorb.png | Bin 0 -> 385 bytes src/main/resources/items/examplesword.png | Bin 446 -> 0 bytes src/main/resources/items/exampletree.png | Bin 0 -> 716 bytes src/main/resources/items/exampletrinket.png | Bin 0 -> 545 bytes src/main/resources/items/examplewall.png | Bin 0 -> 560 bytes src/main/resources/locale/en.lang | 73 ++++- src/main/resources/mobs/examplebossmob.png | Bin 0 -> 10700 bytes src/main/resources/mobs/examplesummonmob.png | Bin 0 -> 4665 bytes .../resources/mobs/icons/examplebossmob.png | Bin 0 -> 449 bytes src/main/resources/mobs/icons/examplemob.png | Bin 0 -> 441 bytes .../resources/mobs/icons/examplesummonmob.png | Bin 0 -> 442 bytes .../resources/objects/examplebaserock.png | Bin 0 -> 2183 bytes src/main/resources/objects/examplechair.png | Bin 0 -> 1048 bytes .../resources/objects/exampleconfigobject.png | Bin 0 -> 1271 bytes src/main/resources/objects/examplegrass.png | Bin 0 -> 1186 bytes .../resources/objects/examplejobobject.png | Bin 0 -> 291 bytes .../objects/exampleleveleventobject.png | Bin 0 -> 289 bytes src/main/resources/objects/exampleore.png | Bin 0 -> 2088 bytes src/main/resources/objects/examplesapling.png | Bin 0 -> 547 bytes src/main/resources/objects/exampletree.png | Bin 0 -> 30999 bytes src/main/resources/objects/examplewall.png | Bin 0 -> 5257 bytes .../resources/objects/examplewalltrap.png | Bin 0 -> 1101 bytes .../resources/particles/exampleleaves.png | Bin 0 -> 946 bytes .../player/armor/examplearms_left.png | Bin 0 -> 7333 bytes .../player/armor/examplearms_right.png | Bin 0 -> 7328 bytes .../resources/player/armor/exampleboots.png | Bin 0 -> 9571 bytes .../resources/player/armor/examplechest.png | Bin 0 -> 8431 bytes .../resources/player/armor/examplehelmet.png | Bin 0 -> 7592 bytes .../player/weapons/examplemagicstaff.png | Bin 0 -> 467 bytes .../player/weapons/examplemeleesword.png | Bin 0 -> 451 bytes .../player/weapons/examplerangedbow.png | Bin 0 -> 423 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 .../projectiles/examplearrowprojectile.png | Bin 0 -> 448 bytes src/main/resources/sound/examplesound.ogg | Bin 0 -> 27010 bytes .../tiles/examplegrasstile_splat.png | Bin 0 -> 11459 bytes 156 files changed, 4081 insertions(+), 212 deletions(-) create mode 100644 src/main/java/examplemod/ExampleModSettings.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModBiomes.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModBuffs.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModCategories.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModCommands.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModEvents.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/ExampleModJobs.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModJournal.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/ExampleModRecipes.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModResources.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModSettlers.java create mode 100644 src/main/java/examplemod/Loaders/ExampleModTiles.java delete mode 100644 src/main/java/examplemod/examples/ExampleBiome.java create mode 100644 src/main/java/examplemod/examples/ExampleLootTable.java delete mode 100644 src/main/java/examplemod/examples/ExampleSwordItem.java create mode 100644 src/main/java/examplemod/examples/ai/ExampleAI.java create mode 100644 src/main/java/examplemod/examples/ai/ExampleAILeaf.java create mode 100644 src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java create mode 100644 src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java rename src/main/java/examplemod/examples/{ => buffs}/ExampleBuff.java (95%) create mode 100644 src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java create mode 100644 src/main/java/examplemod/examples/events/ExampleEvent.java create mode 100644 src/main/java/examplemod/examples/events/ExampleLevelEvent.java delete mode 100644 src/main/java/examplemod/examples/items/ExamplePotionItem.java create mode 100644 src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java 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 create mode 100644 src/main/java/examplemod/examples/items/consumable/ExampleBossSummonItem.java rename src/main/java/examplemod/examples/items/{ => consumable}/ExampleFoodItem.java (95%) create mode 100644 src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleBarItem.java create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java rename src/main/java/examplemod/examples/items/{ => materials}/ExampleHuntIncursionMaterialItem.java (56%) create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleLogItem.java rename src/main/java/examplemod/examples/items/{ => materials}/ExampleMaterialItem.java (51%) create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleOreItem.java create mode 100644 src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java rename src/main/java/examplemod/examples/{ExampleProjectileWeapon.java => items/tools/ExampleStaffMagicWeapon.java} (93%) create mode 100644 src/main/java/examplemod/examples/items/tools/ExampleSwordMeleeWeapon.java create mode 100644 src/main/java/examplemod/examples/items/trinkets/ExampleTrinketItem.java create mode 100644 src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java rename src/main/java/examplemod/examples/{ => maps/incursion}/ExampleIncursionBiome.java (89%) rename src/main/java/examplemod/examples/{ => maps/incursion}/ExampleIncursionLevel.java (62%) 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/mobs/ExampleSettlerMob.java create mode 100644 src/main/java/examplemod/examples/mobs/ExampleSummonWeaponMob.java create mode 100644 src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java create mode 100644 src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java create mode 100644 src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleConfigObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleGrassObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleJobObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleLevelEventObject.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/java/examplemod/examples/objects/ExamplePressurePlateObject.java 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/ExampleWallTrapObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java create mode 100644 src/main/java/examplemod/examples/packets/ExampleConfigInteractPacket.java rename src/main/java/examplemod/examples/{ => packets}/ExamplePacket.java (98%) create mode 100644 src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.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/JobFinderSafePatch.java create mode 100644 src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java create mode 100644 src/main/java/examplemod/examples/presets/ExampleCodePreset.java create mode 100644 src/main/java/examplemod/examples/presets/ExamplePreset.java create mode 100644 src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java rename src/main/java/examplemod/examples/{ => projectiles}/ExampleProjectile.java (96%) 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/java/examplemod/examples/tiles/ExampleGrassTile.java rename src/main/java/examplemod/examples/{ => tiles}/ExampleTile.java (97%) create mode 100644 src/main/resources/buffs/examplearmorsetbonusbuff.png create mode 100644 src/main/resources/items/examplearrow.png create mode 100644 src/main/resources/items/examplebar.png create mode 100644 src/main/resources/items/examplebaserock.png create mode 100644 src/main/resources/items/exampleboots.png create mode 100644 src/main/resources/items/examplebosssummonitem.png create mode 100644 src/main/resources/items/examplechair.png create mode 100644 src/main/resources/items/examplechestplate.png create mode 100644 src/main/resources/items/exampleconfigobject.png create mode 100644 src/main/resources/items/exampledoor.png create mode 100644 src/main/resources/items/examplefood.png delete mode 100644 src/main/resources/items/examplefooditem.png create mode 100644 src/main/resources/items/examplegrass.png create mode 100644 src/main/resources/items/examplegrassseed.png create mode 100644 src/main/resources/items/examplehelmet.png delete mode 100644 src/main/resources/items/examplehuntincursionitem.png create mode 100644 src/main/resources/items/examplehuntincursionmaterial.png create mode 100644 src/main/resources/items/examplejobobject.png create mode 100644 src/main/resources/items/exampleleveleventobject.png create mode 100644 src/main/resources/items/examplelog.png create mode 100644 src/main/resources/items/examplemagicstaff.png create mode 100644 src/main/resources/items/examplemeleesword.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/items/examplepotion.png delete mode 100644 src/main/resources/items/examplepotionitem.png create mode 100644 src/main/resources/items/examplerangedbow.png create mode 100644 src/main/resources/items/examplesapling.png delete mode 100644 src/main/resources/items/examplestaff.png create mode 100644 src/main/resources/items/examplestone.png create mode 100644 src/main/resources/items/examplesummonorb.png delete mode 100644 src/main/resources/items/examplesword.png create mode 100644 src/main/resources/items/exampletree.png create mode 100644 src/main/resources/items/exampletrinket.png create mode 100644 src/main/resources/items/examplewall.png create mode 100644 src/main/resources/mobs/examplebossmob.png create mode 100644 src/main/resources/mobs/examplesummonmob.png create mode 100644 src/main/resources/mobs/icons/examplebossmob.png create mode 100644 src/main/resources/mobs/icons/examplemob.png create mode 100644 src/main/resources/mobs/icons/examplesummonmob.png create mode 100644 src/main/resources/objects/examplebaserock.png create mode 100644 src/main/resources/objects/examplechair.png create mode 100644 src/main/resources/objects/exampleconfigobject.png create mode 100644 src/main/resources/objects/examplegrass.png create mode 100644 src/main/resources/objects/examplejobobject.png create mode 100644 src/main/resources/objects/exampleleveleventobject.png create mode 100644 src/main/resources/objects/exampleore.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/objects/examplewall.png create mode 100644 src/main/resources/objects/examplewalltrap.png create mode 100644 src/main/resources/particles/exampleleaves.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 create mode 100644 src/main/resources/player/weapons/examplemagicstaff.png create mode 100644 src/main/resources/player/weapons/examplemeleesword.png create mode 100644 src/main/resources/player/weapons/examplerangedbow.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 create mode 100644 src/main/resources/projectiles/examplearrowprojectile.png create mode 100644 src/main/resources/sound/examplesound.ogg 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 6cc822c..920d719 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -1,126 +1,76 @@ package examplemod; -import examplemod.examples.*; -import examplemod.examples.items.ExampleFoodItem; -import examplemod.examples.items.ExampleHuntIncursionMaterialItem; -import examplemod.examples.items.ExampleMaterialItem; -import examplemod.examples.items.ExamplePotionItem; -import necesse.engine.commands.CommandsManager; +import examplemod.Loaders.*; +import examplemod.examples.maps.biomes.ExampleBiome; import necesse.engine.modLoader.annotations.ModEntry; -import necesse.engine.registries.*; -import necesse.gfx.gameTexture.GameTexture; -import necesse.inventory.recipe.Ingredient; -import necesse.inventory.recipe.Recipe; -import necesse.inventory.recipe.Recipes; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; import necesse.level.maps.biomes.Biome; @ModEntry 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!"); + settings.logLoadedSettings(); // log the loaded settings for debug - // Register a simple biome that will not appear in natural world gen. - EXAMPLE_BIOME = BiomeRegistry.registerBiome("exampleincursion", new ExampleBiome(), false); - - // 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 categories first: Used by Items/Objects to appear correctly in Creative/crafting trees + ExampleModCategories.load(); - // Register our tiles - TileRegistry.registerTile("exampletile", new ExampleTile(), 1, true); + // Register packets early: Anything networked (mobs, settlers, job UIs, events) can safely reference packet IDs + ExampleModPackets.load(); - // Register our objects - ObjectRegistry.registerObject("exampleobject", new ExampleObject(), 2, true); + // Core content building blocks first: Tiles/Objects/Items are referenced by biomes, incursions, mobs, projectiles, buffs, etc. + ExampleModTiles.load(); + ExampleModObjects.load(); + ExampleModItems.load(); - // Register our items - ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); - ItemRegistry.registerItem("examplehuntincursionitem", new ExampleHuntIncursionMaterialItem(), 50, true); - ItemRegistry.registerItem("examplesword", new ExampleSwordItem(), 20, true); - ItemRegistry.registerItem("examplestaff", new ExampleProjectileWeapon(), 30, true); - ItemRegistry.registerItem("examplepotionitem", new ExamplePotionItem(), 10, true); - ItemRegistry.registerItem("examplefooditem", new ExampleFoodItem(),15, true); + // Combat + entity registries next: Projectiles and buffs often reference items/mobs, and mobs can reference buffs/projectiles. + ExampleModProjectiles.load(); + ExampleModBuffs.load(); + ExampleModMobs.load(); - // Register our mob - MobRegistry.registerMob("examplemob", ExampleMob.class, true); + // Settlement systems after mobs/items exist: Settlers are mobs; jobs can reference settlers, items, and packets/UI. + ExampleModSettlers.load(); + ExampleModJobs.load(); - // Register our projectile - ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); + // World generation last-ish: Biomes/incursions can safely reference all registered tiles/objects/mobs/items now. + ExampleModBiomes.load(); + ExampleModIncursions.load(); - // Register our buff - BuffRegistry.registerBuff("examplebuff", new ExampleBuff()); + // Events after everything is registered: Lets event listeners safely reference IDs and content without ordering surprises. + ExampleModEvents.load(); - // Register our packet - PacketRegistry.registerPacket(ExamplePacket.class); + // Journal last: JournalEntry.addMobEntries() resolves MobRegistry immediately at registration time. + ExampleModJournal.load(); } public void initResources() { - // Sometimes your textures will have a black or other outline unintended under rotation or scaling - // This is caused by alpha blending between transparent pixels and the edge - // To fix this, run the preAntialiasTextures gradle task - // It will process your textures and save them again with a fixed alpha edge color - - ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); + ExampleModResources.load(); } public void postInit() { - // Add recipes - // Example item recipe, crafted in inventory for 2 iron bars - Recipes.registerModRecipe(new Recipe( - "exampleitem", - 1, - RecipeTechRegistry.NONE, - new Ingredient[]{ - new Ingredient("ironbar", 2) - } - ).showAfter("woodboat")); // Show recipe after wood boat recipe - - // Example sword recipe, crafted in iron anvil using 4 example items and 5 copper bars - Recipes.registerModRecipe(new Recipe( - "examplesword", - 1, - RecipeTechRegistry.IRON_ANVIL, - new Ingredient[]{ - new Ingredient("exampleitem", 4), - new Ingredient("copperbar", 5) - } - )); - - // Example staff recipe, crafted in workstation using 4 example items and 10 gold bars - Recipes.registerModRecipe(new Recipe( - "examplestaff", - 1, - RecipeTechRegistry.WORKSTATION, - new Ingredient[]{ - new Ingredient("exampleitem", 4), - new Ingredient("goldbar", 10) - } - ).showAfter("exampleitem")); // Show the recipe after example item recipe - - // Example food item recipe - Recipes.registerModRecipe(new Recipe( - "examplefooditem", - 1, - RecipeTechRegistry.COOKING_POT, - new Ingredient[]{ - new Ingredient("bread", 1), - new Ingredient("strawberry", 2), - new Ingredient("sugar", 1) - } - )); + // load our recipes from the ExampleRecipes class so we can keep this class easy to read + ExampleModRecipes.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/ExampleModSettings.java b/src/main/java/examplemod/ExampleModSettings.java new file mode 100644 index 0000000..9536e18 --- /dev/null +++ b/src/main/java/examplemod/ExampleModSettings.java @@ -0,0 +1,37 @@ +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); + } + + public void logLoadedSettings() { + + GameLog.out.println("[ExampleMod] Settings loaded:"); + GameLog.out.println(" exampleBoolean = " + exampleBoolean); + GameLog.out.println(" exampleInt = " + exampleInt); + GameLog.out.println(" exampleString = \"" + exampleString + "\""); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModBiomes.java b/src/main/java/examplemod/Loaders/ExampleModBiomes.java new file mode 100644 index 0000000..b24b3f8 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBiomes.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.maps.biomes.ExampleBiome; +import necesse.engine.registries.BiomeRegistry; + +public class ExampleModBiomes { + public static void load() { + // Register a simple biome that will not appear in natural world gen. + ExampleMod.EXAMPLE_BIOME = BiomeRegistry.registerBiome("examplebiome", new ExampleBiome(), false); + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModBuffs.java b/src/main/java/examplemod/Loaders/ExampleModBuffs.java new file mode 100644 index 0000000..fc48389 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModBuffs.java @@ -0,0 +1,21 @@ +package examplemod.Loaders; + +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("examplearmorsetbonusbuff", 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/ExampleModCategories.java b/src/main/java/examplemod/Loaders/ExampleModCategories.java new file mode 100644 index 0000000..65dd8c0 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCategories.java @@ -0,0 +1,118 @@ +package examplemod.Loaders; + +import necesse.engine.localization.message.LocalMessage; +import necesse.inventory.item.ItemCategory; + +public final class ExampleModCategories { + private ExampleModCategories() {} + + /* + * IMPORTANT (Creative Menu requirement) + * ------------------------------------ + * The Creative menu tabs currently only browse a small set of hard-coded ROOT categories but this will be changed in the future. + * + * Placeables tab roots: tiles / objects / wiring + * Items tab roots: equipment / consumable / materials / misc + * Mobs tab roots: mobs + * + * So: your itemCategoryTree MUST start with one of those roots, otherwise the item/object + * will not appear in Creative even though it is registered. + */ + + // VANILLA PLACABLE + // ===== ROOT ===== + public static final String ROOT_TILES = "tiles"; + public static final String ROOT_OBJECTS = "objects"; + public static final String ROOT_WIRING = "wiring"; + + // ===== VANILLA: TILES children ===== + public static final String TILES_FLOORS = "floors"; + public static final String TILES_LIQUIDS = "liquids"; + public static final String TILES_TERRAIN = "terrain"; + + // ===== VANILLA: OBJECTS children ===== + public static final String OBJECTS_SEEDS = "seeds"; + public static final String OBJECTS_CRAFTINGSTATIONS = "craftingstations"; + public static final String OBJECTS_LIGHTING = "lighting"; + public static final String OBJECTS_FURNITURE = "furniture"; + public static final String OBJECTS_DECORATIONS = "decorations"; + public static final String OBJECTS_WALLSANDDOORS = "wallsanddoors"; + public static final String OBJECTS_FENCESANDGATES = "fencesandgates"; + public static final String OBJECTS_COLUMNS = "columns"; + public static final String OBJECTS_TRAPS = "traps"; + public static final String OBJECTS_LANDSCAPING = "landscaping"; + public static final String OBJECTS_MISC = "misc"; + + // ===== VANILLA: FURNITURE children ===== + public static final String FURNITURE_MISC = "misc"; + public static final String FURNITURE_OAK = "oak"; + public static final String FURNITURE_SPRUCE = "spruce"; + public static final String FURNITURE_PINE = "pine"; + public static final String FURNITURE_MAPLE = "maple"; + public static final String FURNITURE_BIRCH = "birch"; + public static final String FURNITURE_WILLOW = "willow"; + public static final String FURNITURE_DUNGEON = "dungeon"; + public static final String FURNITURE_BONE = "bone"; + public static final String FURNITURE_DRYAD = "dryad"; + public static final String FURNITURE_BAMBOO = "bamboo"; + public static final String FURNITURE_DEADWOOD = "deadwood"; + + // ===== VANILLA: DECORATIONS children ===== + public static final String DECORATIONS_PAINTINGS = "paintings"; + public static final String DECORATIONS_CARPETS = "carpets"; + public static final String DECORATIONS_POTS = "pots"; + public static final String DECORATIONS_BANNERS = "banners"; + + // ===== VANILLA: LANDSCAPING children ===== + public static final String LANDSCAPING_FORESTROCKSANDORES = "forestrocksandores"; + public static final String LANDSCAPING_SNOWROCKSANDORES = "snowrocksandores"; + public static final String LANDSCAPING_PLAINSROCKSANDORES = "plainsrocksandores"; + public static final String LANDSCAPING_SWAMPROCKSANDORES = "swamprocksandores"; + public static final String LANDSCAPING_DESERTROCKSANDORES = "desertrocksandores"; + public static final String LANDSCAPING_INCURSIONROCKSANDORES = "incursionrocksandores"; + public static final String LANDSCAPING_CRYSTALS = "crystals"; + public static final String LANDSCAPING_TABLEDECORATIONS = "tabledecorations"; + public static final String LANDSCAPING_PLANTS = "plants"; + public static final String LANDSCAPING_MASONRY = "masonry"; + public static final String LANDSCAPING_MISC = "misc"; + + // ===== VANILLA: WIRING children ===== + public static final String WIRING_LOGICGATES = "logicgates"; + + + // YOUR MOD ROOT CATEGORY + public static final String MOD = "examplemod"; + // YOUR MOD SUB CATEGORY + public static final String MOD_OBJECTS = "objects"; + + public static final String EXAMPLEWOOD = "examplewood"; + + public static void load() { + + // ITEM CATEGORIES (not Creative-visible right now, but valid categories) + ItemCategory.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectsubcat"), + MOD, MOD_OBJECTS); + + ItemCategory.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD, MOD_OBJECTS,EXAMPLEWOOD); + + // CRAFTING CATEGORIES + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD", + new LocalMessage("itemcategory", "examplemodrootcat"), + MOD); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS", + new LocalMessage("itemcategory", "examplemodobjectsubcat"), + MOD,MOD_OBJECTS); + + ItemCategory.craftingManager.createCategory("Z-EXAMPLEMOD-OBJECTS-FURNATURE", + new LocalMessage("itemcategory", "examplemodfurnaturesubcat"), + MOD,MOD_OBJECTS,EXAMPLEWOOD); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModCommands.java b/src/main/java/examplemod/Loaders/ExampleModCommands.java new file mode 100644 index 0000000..3754142 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModCommands.java @@ -0,0 +1,13 @@ +package examplemod.Loaders; + +import examplemod.examples.ExampleChatCommand; +import necesse.engine.commands.CommandsManager; + +public class ExampleModCommands { + public static void load(){ + + // Register our server chat command + CommandsManager.registerServerCommand(new ExampleChatCommand()); + + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModEvents.java b/src/main/java/examplemod/Loaders/ExampleModEvents.java new file mode 100644 index 0000000..e3a3fad --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModEvents.java @@ -0,0 +1,33 @@ +package examplemod.Loaders; + +import examplemod.examples.events.ExampleEvent; +import examplemod.examples.events.ExampleLevelEvent; +import necesse.engine.GameEventListener; +import necesse.engine.GameEvents; +import necesse.engine.network.server.ServerClient; +import necesse.engine.registries.LevelEventRegistry; + +public class ExampleModEvents { + public static void load() { + // Register our Level Event to the registry + LevelEventRegistry.registerEvent("examplelevelevent", ExampleLevelEvent.class); + + // Register our ExampleEvent Listener + GameEvents.addListener(ExampleEvent.class, new GameEventListener() { + @Override + public void onEvent(ExampleEvent event) { + if (event.level == null || !event.level.isServer()) return; + + ServerClient client = event.level.getServer().getClient(event.clientSlot); + if (client != null) { + client.sendChatMessage(event.message); + client.sendChatMessage("PONG: this message was sent from the ExampleEvent Listener "); + } + } + }); + + } +} + + + diff --git a/src/main/java/examplemod/Loaders/ExampleModIncursions.java b/src/main/java/examplemod/Loaders/ExampleModIncursions.java new file mode 100644 index 0000000..6f328d7 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModIncursions.java @@ -0,0 +1,17 @@ +package examplemod.Loaders; + +import examplemod.examples.maps.incursion.ExampleIncursionBiome; +import examplemod.examples.maps.incursion.ExampleIncursionLevel; +import necesse.engine.registries.IncursionBiomeRegistry; +import necesse.engine.registries.LevelRegistry; + +public class ExampleModIncursions { + public static void load() { + + // Register the incursion biome with tier requirement 1. + IncursionBiomeRegistry.registerBiome("exampleincursion", new ExampleIncursionBiome(), 1); + + // Register the level class used for the incursion. + LevelRegistry.registerLevel("exampleincursionlevel", ExampleIncursionLevel.class); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModItems.java b/src/main/java/examplemod/Loaders/ExampleModItems.java new file mode 100644 index 0000000..2d25289 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModItems.java @@ -0,0 +1,52 @@ +package examplemod.Loaders; + +import examplemod.examples.items.ammo.ExampleArrowItem; +import examplemod.examples.items.armor.ExampleBootsArmorItem; +import examplemod.examples.items.armor.ExampleChestArmorItem; +import examplemod.examples.items.armor.ExampleHelmetArmorItem; +import examplemod.examples.items.consumable.ExampleBossSummonItem; +import examplemod.examples.items.consumable.ExampleFoodItem; +import examplemod.examples.items.consumable.ExamplePotionItem; +import examplemod.examples.items.materials.*; +import examplemod.examples.items.tools.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; + +public class ExampleModItems { + public static void load(){ + + // Materials + ItemRegistry.registerItem("exampleitem", new ExampleMaterialItem(), 10, true); + ItemRegistry.registerItem("examplestone", new ExampleStoneItem(), 15, true); + ItemRegistry.registerItem("exampleore", new ExampleOreItem(), 25, true); + ItemRegistry.registerItem("examplebar", new ExampleBarItem(), 50, true); + ItemRegistry.registerItem("examplehuntincursionmaterial", new ExampleHuntIncursionMaterialItem(), 50, true); + ItemRegistry.registerItem("examplelog", new ExampleLogItem().setItemCategory("materials","logs"),10,true); + ItemRegistry.registerItem("examplegrassseed", new ExampleGrassSeedItem(),1,true); + + // Tools + ItemRegistry.registerItem("examplemeleesword", new 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); + 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); + ItemRegistry.registerItem("examplebosssummonitem", new ExampleBossSummonItem(),1,true); + + // Ammo + ItemRegistry.registerItem("examplearrow", new ExampleArrowItem(),5,true); + + // Trinkets + 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 new file mode 100644 index 0000000..54b6e15 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModJobs.java @@ -0,0 +1,26 @@ +package examplemod.Loaders; + +import examplemod.examples.settlement.jobs.ExampleLevelJob; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.registries.JobTypeRegistry; +import necesse.engine.registries.LevelJobRegistry; +import necesse.entity.mobs.job.JobType; + +public class ExampleModJobs { + + public static void load(){ + // 1) Register the job type + JobTypeRegistry.registerType("examplejobtype", + new JobType( + true, // canChangePriority (shows in settlement UI) + true, // defaultDisabledBySettler (locked for normal settlers) + new LocalMessage("jobs", "examplejobname"), + new LocalMessage("jobs", "examplejobtip") + ) + ); + + // 2) Register our ExampleLevelJob //DEBUG + LevelJobRegistry.registerJob("examplejob", ExampleLevelJob .class, "examplejobtype"); + } + +} diff --git a/src/main/java/examplemod/Loaders/ExampleModJournal.java b/src/main/java/examplemod/Loaders/ExampleModJournal.java new file mode 100644 index 0000000..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/ExampleModMobs.java b/src/main/java/examplemod/Loaders/ExampleModMobs.java new file mode 100644 index 0000000..a9cb57e --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModMobs.java @@ -0,0 +1,23 @@ +package examplemod.Loaders; + +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 { + public static void load(){ + // Register our mob + MobRegistry.registerMob("examplemob", ExampleMob.class, true); + + // Register boss mob + MobRegistry.registerMob("examplebossmob", ExampleBossMob.class,true,true); + + // Register summon mob + MobRegistry.registerMob("examplesummonmob", ExampleSummonWeaponMob.class,true,false); + + // 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 new file mode 100644 index 0000000..830fcc3 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -0,0 +1,68 @@ +package examplemod.Loaders; + +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 + + ObjectRegistry.registerObject("exampleobject", new ExampleObject() + .setItemCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS) + .setCraftingCategory(ExampleModCategories.ROOT_OBJECTS,ExampleModCategories.OBJECTS_COLUMNS), 2, true); + + + // Register a rock object + ExampleBaseRockObject exampleBaseRock = new ExampleBaseRockObject(); + EXAMPLE_BASE_ROCK_ID = ObjectRegistry.registerObject("examplebaserock", exampleBaseRock, -1.0F, true); + + // Register an ore rock object that overlays onto our incursion rock + EXAMPLE_ORE_ROCK_ID = ObjectRegistry.registerObject("exampleorerock", new ExampleOreRockObject(exampleBaseRock), -1.0F, true); + + // Register a wall object, window object and door object + ExampleWallWindowDoorObject.registerWallsDoorsWindows(); + + // Register a tree object + ObjectRegistry.registerObject("exampletree",new ExampleTreeObject(),0.0F,false,false,false); + + // Register a sapling object + ObjectRegistry.registerObject("examplesapling", new ExampleTreeSaplingObject(),10,true); + + // Register a furnature object this won't currently display in creative due to how creative is coded but this is subject to change + ObjectRegistry.registerObject("examplechair", new ExampleWoodChairObject() + .setItemCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD) + .setCraftingCategory(ExampleModCategories.MOD,ExampleModCategories.MOD_OBJECTS,ExampleModCategories.EXAMPLEWOOD),50,true); + + // Register a grass object + ObjectRegistry.registerObject("examplegrass",new ExampleGrassObject(),1,true); + + // Register an object which uses a level event + ObjectRegistry.registerObject("exampleleveleventobject", new ExampleLevelEventObject(),1,true); + + // Register ExampleJobObject an object that triggers our new job to happen //DEBUG + ObjectRegistry.registerObject("examplejobobject",new ExampleJobObject(),1,true); + + // Register an object that uses the mods config file + ObjectRegistry.registerObject("exampleconfigobject", new ExampleConfigObject(),1,true); + + // Register an example pressure plate object + ObjectRegistry.registerObject("examplepressureplate",new ExamplePressurePlateObject(),1,true); + + // Get the wall object we want this trap to attach to. + // ObjectRegistry stores everything as a generic "GameObject" + // so we fetch by string ID ("examplewall") and cast it to WallObject. + // Takes the texture of the wall object and overlays our "examplewalltrap" + WallObject exampleWall = (WallObject) ObjectRegistry.getObject("examplewall"); + ObjectRegistry.registerObject("examplewalltrap",new ExampleWallTrapObject(exampleWall),1,true); + + + + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModPackets.java b/src/main/java/examplemod/Loaders/ExampleModPackets.java new file mode 100644 index 0000000..1a6aabf --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModPackets.java @@ -0,0 +1,20 @@ +package examplemod.Loaders; + +import examplemod.examples.packets.ExampleConfigInteractPacket; +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); + + // 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/ExampleModProjectiles.java b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java new file mode 100644 index 0000000..a6c190d --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModProjectiles.java @@ -0,0 +1,15 @@ +package examplemod.Loaders; + +import examplemod.examples.projectiles.ExampleArrowProjectile; +import examplemod.examples.projectiles.ExampleProjectile; +import necesse.engine.registries.ProjectileRegistry; + +public class ExampleModProjectiles { + public static void load(){ + // Register our projectile + ProjectileRegistry.registerProjectile("exampleprojectile", ExampleProjectile.class, "exampleprojectile", "exampleprojectile_shadow"); + + // Register our arrow projectile + ProjectileRegistry.registerProjectile("examplearrowprojectile", ExampleArrowProjectile.class, "examplearrowprojectile","arrow_shadow"); + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java new file mode 100644 index 0000000..deab3f5 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -0,0 +1,181 @@ +package examplemod.Loaders; + +import necesse.engine.registries.RecipeTechRegistry; +import necesse.inventory.recipe.Ingredient; +import necesse.inventory.recipe.Recipe; +import necesse.inventory.recipe.Recipes; + +/* +here is where we will register our recipes into the game. + there is potentially quite a few of them so this will allow us to maintain cleaner code +*/ +public class ExampleModRecipes { + + //Put your recipe registrations in here + public static void registerRecipes(){ + + // Example item recipe, crafted in inventory for 2 iron bars + Recipes.registerModRecipe(new Recipe( + "exampleitem", + 1, + RecipeTechRegistry.NONE, + new Ingredient[]{ + new Ingredient("examplebar", 2) + } + ).showAfter("woodboat")); // Show recipe after wood boat recipe + + + //FORGE RECIPES + Recipes.registerModRecipe(new Recipe( + "examplebar", + 1, + RecipeTechRegistry.FORGE, + new Ingredient[]{ + new Ingredient("exampleore",2) + }) + ); + + //IRON ANVIL RECIPES + Recipes.registerModRecipe(new Recipe( + "examplemeleesword", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 4), + new Ingredient("examplebar", 5) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplemagicstaff", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 5), + new Ingredient("examplebar", 4) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplesummonorb", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("exampleitem", 3), + new Ingredient("examplebar", 2) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplehelmet", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 8), + new Ingredient("exampleitem", 2) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplechestplate", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 14), + new Ingredient("exampleitem", 4) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleboots", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[] { + new Ingredient("examplebar", 10), + new Ingredient("exampleitem", 3) + } + )); + + //WORKSTATION RECIPES + Recipes.registerModRecipe(new Recipe( + "examplewall", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampledoor", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleobject", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); + + //COOKING POT RECIPES + Recipes.registerModRecipe(new Recipe( + "examplefood", + 1, + RecipeTechRegistry.COOKING_POT, + new Ingredient[]{ + new Ingredient("bread", 1), + new Ingredient("strawberry", 2), + new Ingredient("sugar", 1) + } + )); + + //ALCHEMY RECIPES + Recipes.registerModRecipe(new Recipe( + "examplepotion", + 1, + RecipeTechRegistry.ALCHEMY, + new Ingredient[]{ + new Ingredient("speedpotion", 1), + } + )); + + //LANDSCAPING RECIPES + Recipes.registerModRecipe(new Recipe( + "examplebaserock", + 1, + RecipeTechRegistry.LANDSCAPING, + new Ingredient[]{ + new Ingredient("examplestone", 5), + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleorerock", + 1, + RecipeTechRegistry.LANDSCAPING, + new Ingredient[]{ + new Ingredient("examplestone", 5), + new Ingredient("exampleore", 5), + } + )); + + //CARPENTER RECIPES + Recipes.registerModRecipe(new Recipe( + "examplechair", + 1, + RecipeTechRegistry.CARPENTER, + new Ingredient[]{ + new Ingredient("examplelog", 5), + } + )); + + } +} diff --git a/src/main/java/examplemod/Loaders/ExampleModResources.java b/src/main/java/examplemod/Loaders/ExampleModResources.java new file mode 100644 index 0000000..668a259 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModResources.java @@ -0,0 +1,33 @@ +package examplemod.Loaders; + +import examplemod.ExampleMod; +import examplemod.examples.mobs.ExampleBossMob; +import examplemod.examples.mobs.ExampleMob; +import examplemod.examples.mobs.ExampleSummonWeaponMob; +import necesse.engine.sound.SoundSettings; +import necesse.engine.sound.gameSound.GameSound; +import necesse.gfx.gameTexture.GameTexture; + +public class ExampleModResources { + public static void load(){ + // Sometimes your textures will have a black or other outline unintended under rotation or scaling + // This is caused by alpha blending between transparent pixels and the edge + // To fix this, run the preAntialiasTextures gradle task + // It will process your textures and save them again with a fixed alpha edge color + + ExampleMob.texture = GameTexture.fromFile("mobs/examplemob"); + ExampleBossMob.texture = GameTexture.fromFile("mobs/examplebossmob"); + ExampleSummonWeaponMob.texture = GameTexture.fromFile("mobs/examplesummonmob"); + + //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 + 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/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/Loaders/ExampleModTiles.java b/src/main/java/examplemod/Loaders/ExampleModTiles.java new file mode 100644 index 0000000..b4d5769 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTiles.java @@ -0,0 +1,16 @@ +package examplemod.Loaders; + +import examplemod.examples.tiles.ExampleGrassTile; +import examplemod.examples.tiles.ExampleTile; +import necesse.engine.registries.TileRegistry; + +public class ExampleModTiles { + + public static int EXAMPLE_TILE_ID = -1; + + public static void load(){ + // Register our tiles + 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/ExampleBiome.java b/src/main/java/examplemod/examples/ExampleBiome.java deleted file mode 100644 index a6ae51e..0000000 --- a/src/main/java/examplemod/examples/ExampleBiome.java +++ /dev/null @@ -1,36 +0,0 @@ -package examplemod.examples; - -import necesse.engine.AbstractMusicList; -import necesse.engine.MusicList; -import necesse.engine.registries.MusicRegistry; -import necesse.entity.mobs.PlayerMob; -import necesse.level.maps.Level; -import necesse.level.maps.biomes.Biome; -import necesse.level.maps.biomes.MobSpawnTable; - -// A minimalist biome used solely for the ExampleIncursion -// the Example Mob is used here as the enemy spawn -public class ExampleBiome extends Biome { - - public static MobSpawnTable critters = new MobSpawnTable() - .include(Biome.defaultCaveCritters); - - public static MobSpawnTable mobs = new MobSpawnTable() - .add(100,"examplemob"); - - @Override - public AbstractMusicList getLevelMusic(Level level, PlayerMob perspective) { - return new MusicList(MusicRegistry.ForestPath); - } - - @Override - public MobSpawnTable getCritterSpawnTable(Level level) { - return critters; - } - - @Override - public MobSpawnTable getMobSpawnTable(Level level) { - return mobs; - } - -} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleLootTable.java b/src/main/java/examplemod/examples/ExampleLootTable.java new file mode 100644 index 0000000..9f397d2 --- /dev/null +++ b/src/main/java/examplemod/examples/ExampleLootTable.java @@ -0,0 +1,64 @@ +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; +import necesse.inventory.lootTable.lootItem.RotationLootItem; + +/** + * 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( + + // Rotating entries: + // 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("exampletrinket"), // 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. + 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, "examplemeleesword"), + new ChanceLootItem(0.60f, "examplemagicstaff"), + new ChanceLootItem(0.60f, "examplesummonorb"), + new ChanceLootItem(0.60f, "examplerangedbow") + ) + ); + + /** + * Private constructor to prevent instantiation. + * This class is intended to be used statically: ExampleLootTable.exampleloottable + */ + private ExampleLootTable() { + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleSwordItem.java b/src/main/java/examplemod/examples/ExampleSwordItem.java deleted file mode 100644 index 68fc649..0000000 --- a/src/main/java/examplemod/examples/ExampleSwordItem.java +++ /dev/null @@ -1,21 +0,0 @@ -package examplemod.examples; - -import necesse.inventory.item.Item; -import necesse.inventory.item.toolItem.swordToolItem.SwordToolItem; - -// Extends SwordToolItem -public class ExampleSwordItem extends SwordToolItem { - - // Weapon attack textures are loaded from resources/player/weapons/ - - public ExampleSwordItem() { - super(400, null); - rarity = Item.Rarity.UNCOMMON; - attackAnimTime.setBaseValue(300); // 300 ms attack time - attackDamage.setBaseValue(20) // Base sword damage - .setUpgradedValue(1, 95); // Upgraded tier 1 damage - attackRange.setBaseValue(120); // 120 range - knockback.setBaseValue(100); // 100 knockback - } - -} diff --git a/src/main/java/examplemod/examples/ai/ExampleAI.java b/src/main/java/examplemod/examples/ai/ExampleAI.java new file mode 100644 index 0000000..f078f6f --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAI.java @@ -0,0 +1,65 @@ +package examplemod.examples.ai; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.Mob; +import necesse.entity.mobs.ai.behaviourTree.composites.SelectorAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.CollisionChaserAINode; +import necesse.entity.mobs.ai.behaviourTree.leaves.WandererAINode; +import necesse.entity.mobs.ai.behaviourTree.trees.CollisionPlayerChaserAI; + +public class ExampleAI extends SelectorAINode { + + // Plays a sound when then boss appears + public final ExampleAILeaf soundPlay; + + // 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; + + // “walk around randomly” node. This is what happens when there’s no player to chase. + public final WandererAINode wanderer; + + public ExampleAI(int searchDistance, GameDamage damage, int knockback, int wanderFrequency) { + + // A Selector is basically: "try child #1, if it can run then use it, + // otherwise try child #2, otherwise child #3..." + // So the ORDER we add children is the ORDER of priority. + + // 1) Teleport / reposition leaf (highest priority). + // (In my leaf: 8 tiles = how far to check for open space, 10 tiles = how far to search for a valid spot) + this.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 + public boolean attackTarget(T mob, Mob target) { + return ExampleAI.this.attackTarget(mob, target); + } + }; + addChild(this.chaser); + + // 3) Wander around if we aren’t teleporting, and we aren’t chasing anyone. + this.wanderer = new WandererAINode<>(wanderFrequency); + addChild(this.wanderer); + } + + // This is the “how to attack” part used by the chaser above. + // Keeping it here makes it easy to change later (special attacks, effects, etc). + public boolean attackTarget(T mob, Mob target) { + + // simpleAttack is the vanilla helper for a basic melee hit. + // It handles cooldown/range stuff internally and returns true if an attack happened. + return CollisionChaserAINode.simpleAttack( + mob, + target, + this.chaser.damage, // use the damage we configured in the chaser + this.chaser.knockback // use the knockback we configured in the chaser + ); + } +} diff --git a/src/main/java/examplemod/examples/ai/ExampleAILeaf.java b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java new file mode 100644 index 0000000..4a56704 --- /dev/null +++ b/src/main/java/examplemod/examples/ai/ExampleAILeaf.java @@ -0,0 +1,47 @@ +package examplemod.examples.ai; + +import examplemod.examples.packets.ExamplePlaySoundPacket; +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; + +/** + * 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 { + + // Ensure this only fires once per mob instance. + private boolean didRun = false; + + @Override + protected void onRootSet(AINode root, T mob, Blackboard blackboard) { + // No setup needed. + } + + @Override + public void init(T mob, Blackboard blackboard) { + // No init needed. + } + + @Override + public AINodeResult tick(T mob, Blackboard blackboard) { + // Run once. + if (didRun) return AINodeResult.FAILURE; + didRun = true; + + // Only the server should broadcast packets to clients. + if (mob == null || !mob.isServer()) return AINodeResult.FAILURE; + + if (mob.getLevel() != null && mob.getLevel().getServer() != null) { + mob.getLevel().getServer().network.sendToClientsWithEntity( + new ExamplePlaySoundPacket(mob.x, mob.y), + mob + ); + } + + return AINodeResult.FAILURE; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java new file mode 100644 index 0000000..d5d3548 --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArmorSetBuff.java @@ -0,0 +1,38 @@ +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; + +/** + * Set bonus buff: + * When a player wears the full armor set, this buff is applied. + * It gives +10% damage and +10% movement speed. + */ +public class ExampleArmorSetBuff extends SimpleSetBonusBuff { + + public ExampleArmorSetBuff() { + // The parent class (SimpleSetBonusBuff) takes the stat boosts here. + super( + new ModifierValue<>(BuffModifiers.ALL_DAMAGE, 0.10f), // +10% damage + new ModifierValue<>(BuffModifiers.SPEED, 0.10f) // +10% speed + ); + } + + @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/buffs/ExampleArrowBuff.java b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java new file mode 100644 index 0000000..5203dbd --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleArrowBuff.java @@ -0,0 +1,63 @@ +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 every 250ms (about 1/4 second) + private static final int HEAL_INTERVAL_MS = 250; + + public ExampleArrowBuff() { + // Keep this buff hidden + temporary + this.canCancel = false; + this.isVisible = false; + this.isPassive = false; + this.shouldSave = false; + } + + @Override + public void init(ActiveBuff buff, BuffEventSubscriber eventSubscriber) { + // Timer for this buff instance + buff.getGndData().setInt("timePassed", 0); + + // "healPerTick" is set by whoever applies the buff + // buff.getGndData().setInt("healPerTick", 2); + } + + @Override + public void serverTick(ActiveBuff buff) { + Mob mob = buff.owner; + if (mob == null) return; + + // How much to heal each time (0 = no healing) + int healPerTick = buff.getGndData().getInt("healPerTick"); + if (healPerTick <= 0) return; + + // Add ~50ms per server tick + int time = buff.getGndData().getInt("timePassed") + 50; + + // Not ready to heal yet + if (time < HEAL_INTERVAL_MS) { + buff.getGndData().setInt("timePassed", time); + return; + } + + // Ready: keep leftover time and heal once + buff.getGndData().setInt("timePassed", time - HEAL_INTERVAL_MS); + + // Heal, but don't go past max health + int newHealth = Math.min(mob.getMaxHealth(), mob.getHealth() + healPerTick); + + // 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/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/buffs/ExampleTrinketBuff.java b/src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java new file mode 100644 index 0000000..032e5ef --- /dev/null +++ b/src/main/java/examplemod/examples/buffs/ExampleTrinketBuff.java @@ -0,0 +1,29 @@ +package examplemod.examples.buffs; + +import necesse.entity.mobs.buffs.ActiveBuff; +import necesse.entity.mobs.buffs.BuffEventSubscriber; +import necesse.entity.mobs.buffs.BuffModifiers; +import necesse.entity.mobs.buffs.staticBuffs.armorBuffs.trinketBuffs.SimpleTrinketBuff; + +public class ExampleTrinketBuff extends SimpleTrinketBuff { + public ExampleTrinketBuff(){ + + } + + @Override + public void init(ActiveBuff activeBuff, BuffEventSubscriber buffEventSubscriber) { + // Apply modifiers here + activeBuff.setModifier(BuffModifiers.SPELUNKER,true); // +50% speed + } + + @Override + public void serverTick(ActiveBuff buff) { + // You can do server ticks here + } + + @Override + public void clientTick(ActiveBuff buff) { + // You can do client ticks here, like adding particles to buff.owner + } + +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/events/ExampleEvent.java b/src/main/java/examplemod/examples/events/ExampleEvent.java new file mode 100644 index 0000000..4c4cc7e --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleEvent.java @@ -0,0 +1,35 @@ +package examplemod.examples.events; + +import necesse.engine.events.GameEvent; +import necesse.level.maps.Level; + +/* + * ExampleEvent is a small "notification" object for our mod. + * + * Compared to a LevelEvent + * it does not exist in the world + * it does not tick + * it does not draw anything + * + * Instead, we create one and pass it through GameEvents.triggerEvent(...) + * so any registered listeners can react. + */ +public class ExampleEvent extends GameEvent { + + // The level where the event happened + public final Level level; + + // The slot id of the player this event relates to + public final int clientSlot; + + // Simple data payload for the demo + public final String message; + + public ExampleEvent(Level level, int clientSlot) { + this.level = level; + this.clientSlot = clientSlot; + + // For a real mod you would usually pass the message into the constructor. + this.message = "PING: this message was sent from the ExampleEvent"; + } +} diff --git a/src/main/java/examplemod/examples/events/ExampleLevelEvent.java b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java new file mode 100644 index 0000000..82fefe1 --- /dev/null +++ b/src/main/java/examplemod/examples/events/ExampleLevelEvent.java @@ -0,0 +1,176 @@ +package examplemod.examples.events; + +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.server.ServerClient; +import necesse.entity.levelEvent.LevelEvent; +import necesse.entity.levelEvent.mobAbilityLevelEvent.MobHealthChangeEvent; +import necesse.entity.mobs.GameDamage; +import necesse.entity.particle.Particle; + +import java.awt.Color; + +/** + * LevelEvent + * Server: sends a chat message to a specific player. + * Client: spawns a quick burst of particles at a world position. + * events.addHidden(ev) server only + * events.add(ev) server + clients + */ +public class ExampleLevelEvent extends LevelEvent { + + // Which ServerClient (player) to message. -1 = invalid/unset. + private int targetSlot = -1; + + // Message to send. + private String message = ""; + + // World position (pixels) where the particle effect should appear. + private int worldX; + private int worldY; + + // Simple lifetime for the client effect (in ticks). + private int ticksLeft = 20; + + // Small guard so the server only sends the message once. + private boolean sentServerMessage = false; + + // Required empty constructor for registry/network spawning + public ExampleLevelEvent() { + } + + /** + * targetSlot player to message + * worldX particle position X in pixels (tileX * 32 + 16 is tile center) + * worldY particle position Y in pixels + */ + public ExampleLevelEvent(int targetSlot, int worldX, int worldY) { + this.targetSlot = targetSlot; + this.worldX = worldX; + this.worldY = worldY; + this.message = "ZING: this message was sent from the ExampleLevelEvent"; + } + + @Override + public void init() { + super.init(); + + // Server side: send the message once. + if (isServer() && !sentServerMessage) { + sentServerMessage = true; + + //target the specific client that triggered the event and make sure it's not returned null + ServerClient client = level.getServer().getClient(targetSlot); + if (client != null) { + + GameDamage dmg = new GameDamage(30F); + + //Apply damage + int healthBefore = client.playerMob.getHealth(); + client.playerMob.isServerHit(dmg, worldX, worldY, 0.0F, null); + int healthAfter = client.playerMob.getHealth(); + + //How much damage was actually taken + int damageTaken = healthBefore - healthAfter; + + if (damageTaken > 0) { + //Restore that amount clamped to current max health, and compute the healing applied because this is a demo, and we aren't mean xD + int finalHealth = Math.min(client.playerMob.getMaxHealth(), healthAfter + damageTaken); + int healApplied = finalHealth - healthAfter; + + //send a health change event to apply + level.entityManager.events.add(new MobHealthChangeEvent(client.playerMob, finalHealth, healApplied)); + } + + client.sendChatMessage(message); + } + + // Do NOT call over() here if you want clients to see the event. + // Let it exist long enough for the spawn packet to be processed. + } + } + + @Override + public void serverTick() { + super.serverTick(); + + // If this event was added as hidden, it will never go to clients, + // so we can end it right away after doing server work. + if (sentServerMessage) { + over(); + } + } + + @Override + public void clientTick() { + super.clientTick(); + + // Quick particle burst for a short time, then end. + if (ticksLeft-- <= 0) { + over(); + return; + } + + Color c = new Color(120, 200, 255); + + for (int i = 0; i < 6; i++) { + float ox = (float) (Math.random() * 24.0 - 12.0); // -12..12 px + float oy = (float) (Math.random() * 24.0 - 12.0); + + level.entityManager + .addParticle(worldX + ox, worldY + oy, Particle.GType.COSMETIC) + .color(c) + .sizeFades(30, 60) + .fadesAlphaTime(250, 150) + .lifeTime(350); + } + } + + @Override + public void setupSpawnPacket(PacketWriter writer) { + super.setupSpawnPacket(writer); + + /* + * setupSpawnPacket(...) is called when the server spawns this LevelEvent to clients + * (when you use level.entityManager.events.add/level.entityManager.events.addHidden) + * + * Anything you want the client-side version of this event to know must be written here. + * The client will read these values in applySpawnPacket(...) in the exact same order. + */ + + // Who the event should target (which player/server client slot) + writer.putNextInt(targetSlot); + + // Any text payload we want associated with the event + writer.putNextString(message); + + // World position for effects (these should be pixel coords, not tile coords) + writer.putNextInt(worldX); + writer.putNextInt(worldY); + + // How long the client-side effect should run + writer.putNextInt(ticksLeft); + } + + @Override + public void applySpawnPacket(PacketReader reader) { + super.applySpawnPacket(reader); + + /* + * applySpawnPacket(...) is called on the client when it receives the spawn packet + * for this LevelEvent. + * + * Make sure you read values back in the same order you wrote them in setupSpawnPacket(...), + * otherwise you'll desync fields and get confusing bugs. + */ + + // Read target player slot + message payload + targetSlot = reader.getNextInt(); + message = reader.getNextString(); + + // Read effect position + lifetime + worldX = reader.getNextInt(); + worldY = reader.getNextInt(); + ticksLeft = reader.getNextInt(); + } +} diff --git a/src/main/java/examplemod/examples/items/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/ExamplePotionItem.java deleted file mode 100644 index 17bdcdc..0000000 --- a/src/main/java/examplemod/examples/items/ExamplePotionItem.java +++ /dev/null @@ -1,11 +0,0 @@ -package examplemod.examples.items; - -import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; - -public class ExamplePotionItem extends SimplePotionItem { - - public ExamplePotionItem() { - super(100,Rarity.COMMON,"examplebuff",100, "examplepotionitemtip"); - } - -} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java new file mode 100644 index 0000000..3d8d6b0 --- /dev/null +++ b/src/main/java/examplemod/examples/items/ammo/ExampleArrowItem.java @@ -0,0 +1,35 @@ +package examplemod.examples.items.ammo; + +import necesse.engine.registries.ProjectileRegistry; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.itemAttacker.ItemAttackerMob; +import necesse.entity.projectile.Projectile; +import necesse.inventory.item.arrowItem.ArrowItem; + +public class ExampleArrowItem extends ArrowItem { + + public ExampleArrowItem() { + super(5000); // stack size like vanilla arrows + + // 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) { + + return ProjectileRegistry.getProjectile( + "examplearrowprojectile", // your projectile stringID + owner.getLevel(), + x, y, targetX, targetY, + velocity, range, + damage, knockback, + owner + ); + } +} diff --git a/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java b/src/main/java/examplemod/examples/items/armor/ExampleBootsArmorItem.java new file mode 100644 index 0000000..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..1ce4bb1 --- /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 + "examplearmorsetbonusbuff" //buff STRING ID + ); + } +} 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/items/ExampleFoodItem.java b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java similarity index 95% rename from src/main/java/examplemod/examples/items/ExampleFoodItem.java rename to src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java index 1f5ce13..ce4279b 100644 --- a/src/main/java/examplemod/examples/items/ExampleFoodItem.java +++ b/src/main/java/examplemod/examples/items/consumable/ExampleFoodItem.java @@ -1,4 +1,4 @@ -package examplemod.examples.items; +package examplemod.examples.items.consumable; import necesse.engine.modifiers.ModifierValue; import necesse.entity.mobs.buffs.BuffModifiers; diff --git a/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java new file mode 100644 index 0000000..d9fe2b7 --- /dev/null +++ b/src/main/java/examplemod/examples/items/consumable/ExamplePotionItem.java @@ -0,0 +1,15 @@ +package examplemod.examples.items.consumable; + +import necesse.inventory.item.placeableItem.consumableItem.potionConsumableItem.SimplePotionItem; + +public class ExamplePotionItem extends SimplePotionItem { + + public ExamplePotionItem() { + super(100, // Max Stack Size + Rarity.COMMON, // Item Rarity + "examplebuff", // Buff 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 new file mode 100644 index 0000000..83e47a3 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleBarItem.java @@ -0,0 +1,13 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleBarItem extends MatItem { + + public ExampleBarItem() { + super(500, // Max Stack Size + Item.Rarity.UNCOMMON); // Rarity + + } +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java new file mode 100644 index 0000000..d2e9b63 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleGrassSeedItem.java @@ -0,0 +1,19 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.tileItem.GrassSeedItem; + +/** + * A seed item that turns dirt into our custom grass tile when placed. + * uses GrassSeedItem for grass seeds. It handles: + * Only placing on dirt + * Tile placement + preview + * Consuming the item (unless in god mode) + * "Grass seed" style tooltip and crafting ingredients + */ +public class ExampleGrassSeedItem extends GrassSeedItem { + + public ExampleGrassSeedItem() { + // This must match your TileRegistry stringID + super("examplegrasstile"); + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java similarity index 56% rename from src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java index 8c15c37..7796e8d 100644 --- a/src/main/java/examplemod/examples/items/ExampleHuntIncursionMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleHuntIncursionMaterialItem.java @@ -1,11 +1,12 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; 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 new file mode 100644 index 0000000..ed190f3 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleLogItem.java @@ -0,0 +1,13 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.matItem.MatItem; + +public class ExampleLogItem extends MatItem { + + public ExampleLogItem() { + super(500, // Max Stack Size + Rarity.UNCOMMON, // Rarity + new String[]{"anylog"}); // Global Ingrediants + + } +} diff --git a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java similarity index 51% rename from src/main/java/examplemod/examples/items/ExampleMaterialItem.java rename to src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java index a20ddb6..558a20a 100644 --- a/src/main/java/examplemod/examples/items/ExampleMaterialItem.java +++ b/src/main/java/examplemod/examples/items/materials/ExampleMaterialItem.java @@ -1,11 +1,12 @@ -package examplemod.examples.items; +package examplemod.examples.items.materials; import necesse.inventory.item.matItem.MatItem; 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 new file mode 100644 index 0000000..4b1ff07 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleOreItem.java @@ -0,0 +1,13 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.Item; +import necesse.inventory.item.matItem.MatItem; + +public class ExampleOreItem extends MatItem { + + public ExampleOreItem() { + super(500, // Max Stack Size + Item.Rarity.UNCOMMON); // Rarity + + } +} diff --git a/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java new file mode 100644 index 0000000..720f8c8 --- /dev/null +++ b/src/main/java/examplemod/examples/items/materials/ExampleStoneItem.java @@ -0,0 +1,9 @@ +package examplemod.examples.items.materials; + +import necesse.inventory.item.placeableItem.StonePlaceableItem; + +public class ExampleStoneItem extends StonePlaceableItem { + public ExampleStoneItem(){ + super(100); // Max Stack Size + } +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java new file mode 100644 index 0000000..46c8574 --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleBowRangedWeapon.java @@ -0,0 +1,32 @@ +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 ExampleBowRangedWeapon extends BowProjectileToolItem { + public ExampleBowRangedWeapon() { + // (enchantCost, lootTableCategory) + 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 + 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; + this.attackYOffset = 20; + + // How much the bow sprite “stretches” while charging + this.attackSpriteStretch = 4; + + // Optional + this.canBeUsedForRaids = true; + } +} diff --git a/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.java new file mode 100644 index 0000000..6df908b --- /dev/null +++ b/src/main/java/examplemod/examples/items/tools/ExampleOrbSummonWeapon.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 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; + + // 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/ExampleProjectileWeapon.java b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java similarity index 93% rename from src/main/java/examplemod/examples/ExampleProjectileWeapon.java rename to src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java index 31aff93..ea31f86 100644 --- a/src/main/java/examplemod/examples/ExampleProjectileWeapon.java +++ b/src/main/java/examplemod/examples/items/tools/ExampleStaffMagicWeapon.java @@ -1,5 +1,6 @@ -package examplemod.examples; +package examplemod.examples.items.tools; +import examplemod.examples.projectiles.ExampleProjectile; import necesse.engine.localization.Localization; import necesse.engine.network.gameNetworkData.GNDItemMap; import necesse.engine.sound.SoundEffect; @@ -17,13 +18,13 @@ import necesse.level.maps.Level; // Extends MagicProjectileToolItem -public class ExampleProjectileWeapon extends MagicProjectileToolItem { +public class 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 ExampleProjectileWeapon() { + public ExampleStaffMagicWeapon() { super(400, null); rarity = Rarity.RARE; attackAnimTime.setBaseValue(300); @@ -41,7 +42,7 @@ public ExampleProjectileWeapon() { @Override public ListGameTooltips getPreEnchantmentTooltips(InventoryItem item, PlayerMob perspective, GameBlackboard blackboard) { ListGameTooltips tooltips = super.getPreEnchantmentTooltips(item, perspective, blackboard); - tooltips.add(Localization.translate("itemtooltip", "examplestafftip")); + tooltips.add(Localization.translate("itemtooltip", "examplemagicstafftip")); return tooltips; } @@ -57,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/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/java/examplemod/examples/maps/biomes/ExampleBiome.java b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java new file mode 100644 index 0000000..12fb827 --- /dev/null +++ b/src/main/java/examplemod/examples/maps/biomes/ExampleBiome.java @@ -0,0 +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; + +/** + * 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 { + + // ========================================================================= + // Spawns + // ========================================================================= + + public static final MobSpawnTable surfaceCritters = new MobSpawnTable() + .include(Biome.defaultSurfaceCritters); + + public static final MobSpawnTable caveCritters = new MobSpawnTable() + .include(Biome.defaultCaveCritters); + + public static final MobSpawnTable surfaceMobs = new MobSpawnTable() + .add(30, "examplemob"); + + public static final MobSpawnTable caveMobs = new MobSpawnTable() + .add(100, "examplemob"); + + public static final MobSpawnTable deepCaveMobs = new MobSpawnTable() + .add(100, "examplemob"); + + public ExampleBiome() { + super(); + this.setGenerationWeight(1.0F); + } + + // ========================================================================= + // Base tiles / rocks + // ========================================================================= + + /** + * 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"); + 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; + } + + /** + * 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); + + // 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); + } + + // ========================================================================= + // Region passes + // ========================================================================= + + @Override + public void generateRegionSurfaceTerrain(Region region, BiomeGeneratorStack stack, GameRandom random) { + super.generateRegionSurfaceTerrain(region, stack, random); + + final int grassTile = getGenerationTerrainTileID(); + + stack.startPlaceOnVein(this, region, random, "exampleTrees") + .onlyOnTile(grassTile) + .chance(0.10D) + .placeObject("exampletree"); + + stack.startPlace(this, region, random) + .chance(0.40D) + .onlyOnTile(grassTile) + .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 + } + + @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); + } + + @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 + // ========================================================================= + + @Override + public MobSpawnTable getCritterSpawnTable(Level level) { + return level.isCave ? caveCritters : surfaceCritters; + } + + @Override + public MobSpawnTable getMobSpawnTable(Level level) { + 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 + "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 + "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. + 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/ExampleIncursionBiome.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java similarity index 89% rename from src/main/java/examplemod/examples/ExampleIncursionBiome.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java index 6671f98..b410505 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionBiome.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionBiome.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.maps.incursion; import necesse.engine.network.server.Server; import necesse.engine.registries.ItemRegistry; @@ -26,13 +26,13 @@ public class ExampleIncursionBiome extends IncursionBiome { public ExampleIncursionBiome() { - super("reaper"); // The boss mob string ID for this incursion + super("examplebossmob"); // The boss mob string ID for this incursion } // Items required to be obtained when completing an extraction objective in this incursion @Override public Collection getExtractionItems(IncursionData data) { - return Collections.singleton(ItemRegistry.getItem("tungstenore")); + return Collections.singleton(ItemRegistry.getItem("exampleore")); } /** @@ -42,7 +42,7 @@ public Collection getExtractionItems(IncursionData data) { @Override public LootTable getHuntDrop(IncursionData incursionData) { return new LootTable( - new ChanceLootItem(0.66F, "examplehuntincursionitem") + new ChanceLootItem(0.66F, "examplehuntincursionmaterial") ); } @@ -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/ExampleIncursionLevel.java b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java similarity index 62% rename from src/main/java/examplemod/examples/ExampleIncursionLevel.java rename to src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java index bfcecef..ea8c555 100644 --- a/src/main/java/examplemod/examples/ExampleIncursionLevel.java +++ b/src/main/java/examplemod/examples/maps/incursion/ExampleIncursionLevel.java @@ -1,6 +1,7 @@ -package examplemod.examples; +package examplemod.examples.maps.incursion; import examplemod.ExampleMod; +import examplemod.examples.presets.ExamplePreset; import necesse.engine.GameEvents; import necesse.engine.events.worldGeneration.GenerateCaveLayoutEvent; import necesse.engine.events.worldGeneration.GeneratedCaveOresEvent; @@ -15,6 +16,9 @@ import necesse.level.maps.incursion.BiomeExtractionIncursionData; import necesse.level.maps.incursion.BiomeMissionIncursionData; import necesse.level.maps.incursion.IncursionBiome; +import necesse.level.maps.presets.Preset; + +import java.awt.*; /** * Example incursion level. @@ -44,46 +48,69 @@ public ExampleIncursionLevel(LevelIdentifier identifier, BiomeMissionIncursionDa } public void generateLevel(BiomeMissionIncursionData incursionData, AltarData altarData) { - // Create the cave generator using deep rock tiles for floors and walls - CaveGeneration cg = new CaveGeneration(this, "deeprocktile", "deeprock"); - - // Seed the generator so this incursion layout is deterministic per mission + CaveGeneration cg = new CaveGeneration(this, "deeprocktile", "examplebaserock"); cg.random.setSeed(incursionData.getUniqueID()); - // Fire the cave layout generation event, allowing mods or perks to modify - // or cancel cave generation before the default logic runs GameEvents.triggerEvent( new GenerateCaveLayoutEvent(this, cg), - e -> { - cg.generateLevel(0.38F, 4, 3, 6); - } + e -> cg.generateLevel(0.38F, 4, 3, 6) ); - // Used to reserve space so later generation steps avoid overwriting the entrance + //entrance + perks (anything that must never be overwritten by perk presets) PresetGeneration entranceAndPerkPresets = new PresetGeneration(this); - // Generate an incursion entrance that clears terrain, - // blends edges, reserves space, and places the return portal - IncursionBiome.generateEntrance( + //your own structures (custom rooms, etc.) + PresetGeneration structurePresets = new PresetGeneration(this); + + // Generate entrance (this reserves space inside entranceAndPerkPresets) + int spawnSize = 32; + Point entranceMid = IncursionBiome.generateEntrance( this, entranceAndPerkPresets, cg.random, - 32, + spawnSize, cg.rockTile, "exampletile", "exampletile", "exampleobject" ); - // Now call incursion perks to generate their presets + // reserve the entrance space in structurePresets too, so your own structures don't overwrite the entrance area. + int ex = entranceMid.x - spawnSize / 2; + int ey = entranceMid.y - spawnSize / 2; + structurePresets.addOccupiedSpace(ex, ey, spawnSize, spawnSize); + + + //EXAMPLE PRESET + Preset examplePreset = new ExamplePreset(cg.random); + Point placedAt = structurePresets.findRandomValidPositionAndApply( + cg.random, + 2500,//realistically this would be lower if you didn't want it to be guaranteed + examplePreset, + 8, + true,// randomizeMirrorX + true, // randomizeMirrorY + true, // randomizeRotation + false // overrideCanPlace (false = respect canApply rules) + ); + + if (placedAt != null) { + structurePresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + entranceAndPerkPresets.addOccupiedSpace(placedAt.x, placedAt.y, examplePreset.width, examplePreset.height); + } + + /* + Perk presets use entranceAndPerkPresets, so they avoid the entrance as well as any presets + you added to structurePresets + */ generatePresetsBasedOnPerks(altarData, entranceAndPerkPresets, cg.random, baseBiome); // This call clears all invalid objects/tiles, so that there are no cut in half beds, etc. GenerationTools.checkValid(this); - // For extraction incursions, guarantee tungsten ore veins for objectives + // For extraction incursions, guarantee example ore veins for objectives if (incursionData instanceof BiomeExtractionIncursionData) { - cg.generateGuaranteedOreVeins(40, 4, 8, ObjectRegistry.getObjectID("tungstenoredeeprock")); + cg.generateGuaranteedOreVeins(40, 4, 8, ObjectRegistry.getObjectID("exampleorerock")); } // Generate upgrade shard and alchemy shard ores cg.generateGuaranteedOreVeins(75, 6, 12, ObjectRegistry.getObjectID("upgradesharddeeprock")); diff --git a/src/main/java/examplemod/examples/mobs/ExampleBossMob.java b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java new file mode 100644 index 0000000..8f0455d --- /dev/null +++ b/src/main/java/examplemod/examples/mobs/ExampleBossMob.java @@ -0,0 +1,161 @@ +package examplemod.examples.mobs; + +import examplemod.examples.ai.ExampleAI; +import necesse.engine.eventStatusBars.EventStatusBarManager; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.engine.registries.MusicRegistry; +import necesse.engine.sound.SoundManager; +import necesse.engine.util.GameRandom; +import necesse.entity.mobs.GameDamage; +import necesse.entity.mobs.MobDrawable; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.mobs.ai.behaviourTree.BehaviourTreeAI; +import necesse.entity.mobs.hostile.bosses.BossMob; +import necesse.entity.particle.FleshParticle; +import necesse.entity.particle.Particle; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptions; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.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; + +import java.awt.*; +import java.util.List; + +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) + ); + + // 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); + 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); + } + + //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(); + // Setup AI + this.ai = new BehaviourTreeAI<>( + this, + new ExampleAI<>( + 1380, // search distance (in pixels) + new GameDamage(60), // collide damage + 150, // knockback + 12000 // wander frequency + ) + ); + } + // 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 + for (int i = 0; i < 4; i++) { + getLevel().entityManager.addParticle(new FleshParticle( + getLevel(), texture, + GameRandom.globalRandom.nextInt(5), // Randomize between the debris sprites + 8, + 32, + x, y, 20f, // Position + knockbackX, knockbackY // Basically start speed of the particles + ), Particle.GType.IMPORTANT_COSMETIC); + } + } + + @Override + protected void addDrawables(List list, OrderableDrawables tileList, OrderableDrawables topList, Level level, int x, int y, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + super.addDrawables(list, tileList, topList, level, x, y, tickManager, camera, perspective); + // Tile positions are basically level positions divided by 32. getTileX() does this for us etc. + GameLight light = level.getLightLevel(getTileX(), getTileY()); + int drawX = camera.getDrawX(x) - 32; + int drawY = camera.getDrawY(y) - 51; + + // A helper method to get the sprite of the current animation/direction of this mob + Point sprite = getAnimSprite(x, y, getDir()); + + drawY += getBobbing(x, y); + drawY += getLevel().getTile(getTileX(), getTileY()).getMobSinkingAmount(this); + + DrawOptions drawOptions = texture.initDraw() + .sprite(sprite.x, sprite.y, 64) + .light(light) + .pos(drawX, drawY); + + list.add(new MobDrawable() { + @Override + public void draw(TickManager tickManager) { + drawOptions.draw(); + } + }); + + addShadowDrawables(tileList, level, x, y, light, camera); + } + + @Override + public int getRockSpeed() { + // Change the speed at which this mobs animation plays + return 20; + } + + @Override + public void clientTick() { + super.clientTick(); + + // Only show boss bar when the client player is close enough + if (isClientPlayerNearby()) { + EventStatusBarManager.registerMobHealthStatusBar(this); + } + // Optional: set boss music here too if you want + SoundManager.setMusic(MusicRegistry.AscendedReturn, SoundManager.MusicPriority.EVENT, 1.5F); + } + + +} diff --git a/src/main/java/examplemod/examples/ExampleMob.java b/src/main/java/examplemod/examples/mobs/ExampleMob.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleMob.java rename to src/main/java/examplemod/examples/mobs/ExampleMob.java index c537747..c3151e4 100644 --- a/src/main/java/examplemod/examples/ExampleMob.java +++ b/src/main/java/examplemod/examples/mobs/ExampleMob.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.mobs; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.engine.util.GameRandom; diff --git a/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java b/src/main/java/examplemod/examples/mobs/ExampleSettlerMob.java new file mode 100644 index 0000000..933a8f6 --- /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("examplejobtype").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/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/objectentity/ExampleJobObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleJobObjectEntity.java new file mode 100644 index 0000000..bf639e6 --- /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, "examplejobobjectentity", 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 example job + level.jobsLayer.addJob(new ExampleLevelJob(x,y)); + } + } +} diff --git a/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java new file mode 100644 index 0000000..96768ec --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleObjectEntity.java @@ -0,0 +1,97 @@ +package examplemod.examples.objectentity; + +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/objectentity/ExampleTrapObjectEntity.java b/src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java new file mode 100644 index 0000000..2317fc1 --- /dev/null +++ b/src/main/java/examplemod/examples/objectentity/ExampleTrapObjectEntity.java @@ -0,0 +1,77 @@ +package examplemod.examples.objectentity; + +import java.awt.Point; + +import necesse.entity.mobs.GameDamage; +import necesse.entity.objectEntity.TrapObjectEntity; +import necesse.entity.projectile.TrapArrowProjectile; +import necesse.level.maps.Level; + +/* + * Arrow trap logic. + * When this trap is triggered by a wire, it shoots an arrow in the direction it faces. + */ +public class ExampleTrapObjectEntity extends TrapObjectEntity { + + // The damage the arrow will deal when it hits something. + public static final GameDamage DAMAGE = new GameDamage(40.0F, 100.0F, 0.0F, 2.0F, 1.0F); + + public ExampleTrapObjectEntity(Level level, int x, int y) { + // Cooldown in milliseconds (1000ms = 1 second). + super(level, x, y, 1000L); + + // This object entity is meant to be recreated, not saved. + this.shouldSave = false; + } + + @Override + public void triggerTrap(int wireID, int dir) { + // Only the server should spawn projectiles. + // Also, don't fire again while we're still on cooldown. + if (isClient() || onCooldown()) return; + + // If a different wire is active at the same time, ignore this trigger. + if (otherWireActive(wireID)) return; + + // Find the tile position the trap should fire from. + Point tilePos = getPos(this.tileX, this.tileY, dir); + + // Turn the direction number (0..3) into a simple (x,y) direction. + Point d = getDir(dir); + + // Convert tile coordinates into pixel coordinates (32 pixels per tile). + int xPos = tilePos.x * 32; + int yPos = tilePos.y * 32; + + // Shift the spawn position a bit so the arrow looks like it comes from the correct side. + if (d.x == 0) xPos += 16; // shooting up/down: centre of the tile + else if (d.x == -1) xPos += 30; // shooting left: near the left edge + else if (d.x == 1) xPos += 2; // shooting right: near the right edge + + if (d.y == 0) yPos += 16; // shooting left/right: centre of the tile + else if (d.y == -1) yPos += 30; // shooting up: near the top edge + else if (d.y == 1) yPos += 2; // shooting down: near the bottom edge + + // Create and spawn the projectile. + // The "target" is just one step in the direction we're firing. + getLevel().entityManager.projectiles.add(new TrapArrowProjectile( + xPos, yPos, + xPos + d.x, + yPos + d.y, + DAMAGE, + null + )); + + // Start the cooldown so it can't fire again instantly. + startCooldown(); + } + + // Converts 0..3 into up/right/down/left. + private Point getDir(int dir) { + if (dir == 0) return new Point(0, -1); // up + if (dir == 1) return new Point(1, 0); // right + if (dir == 2) return new Point(0, 1); // down + if (dir == 3) return new Point(-1, 0); // left + return new Point(0, 0); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java new file mode 100644 index 0000000..b1e6290 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleBaseRockObject.java @@ -0,0 +1,16 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; + +import java.awt.Color; + +public class ExampleBaseRockObject extends RockObject { + + public ExampleBaseRockObject() { + 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 new file mode 100644 index 0000000..abbd329 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleConfigObject.java @@ -0,0 +1,86 @@ +package examplemod.examples.objects; + +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/ExampleGrassObject.java b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java new file mode 100644 index 0000000..22c1e17 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleGrassObject.java @@ -0,0 +1,21 @@ +package examplemod.examples.objects; + +import necesse.inventory.lootTable.LootTable; +import necesse.inventory.lootTable.lootItem.ChanceLootItem; +import necesse.level.gameObject.GrassObject; +import necesse.level.maps.Level; + +public class ExampleGrassObject extends GrassObject { + + public ExampleGrassObject() { + // "examplegrass" is the texture name, 2 = variants/height setting used by GrassObject + super("examplegrass", 2); + } + + public LootTable getLootTable(Level level, int tileX, int tileY) { + // 4% chance, tweak to match vanilla feel + return new LootTable( + new ChanceLootItem(0.04F, "examplegrassseed") + ); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/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 new file mode 100644 index 0000000..cf624fd --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleLevelEventObject.java @@ -0,0 +1,89 @@ +package examplemod.examples.objects; + +import examplemod.examples.objectentity.ExampleObjectEntity; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.entity.objectEntity.ObjectEntity; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.texture.TextureDrawOptionsEnd; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.gameObject.GameObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.awt.Rectangle; +import java.util.List; + +/* + * Basic placeable demo object that: + * - draws a 32x32 sprite in the world + * - on interact (server side), spawns our ExampleLevelEvent + * - also triggers our custom ExampleEvent through GameEvents (so listeners can react) + */ +public class ExampleLevelEventObject extends GameObject { + + // Loaded once from mod resources in loadTextures() + private GameTexture texture; + + public ExampleLevelEventObject() { + //no physics shape + super(new Rectangle()); + this.isSolid = false; + } + + @Override + public void loadTextures() { + super.loadTextures(); + + // Loads: src/main/resources/objects/exampleleveleventobject.png + // (no ".png" in the string) + this.texture = GameTexture.fromFile("objects/exampleleveleventobject"); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, TickManager tickManager, + GameCamera camera, PlayerMob perspective) { + + // Match sprite lighting to the level light at this tile + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coordinates to screen draw coordinates + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Build draw options once (sprite + lighting + position) + final TextureDrawOptionsEnd opts = this.texture.initDraw() + .sprite(0, 0, 32) // sprite index (0,0), size 32 + .light(light) + .pos(drawX, drawY); + + /* + */ + tileList.add(tm -> opts.draw()); + } + + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, + PlayerMob player, GameCamera camera) { + + // Placement preview ("ghost" sprite) while holding the item + GameLight light = level.getLightLevel(tileX, tileY); + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + this.texture.initDraw() + .sprite(0, 0, 32) + .light(light) + .alpha(alpha) + .draw(drawX, drawY); + } + + @Override + public ObjectEntity getNewObjectEntity(Level level, int x, int y) { + return new ExampleObjectEntity(level, x, y); + } +} diff --git a/src/main/java/examplemod/examples/ExampleObject.java b/src/main/java/examplemod/examples/objects/ExampleObject.java similarity index 98% rename from src/main/java/examplemod/examples/ExampleObject.java rename to src/main/java/examplemod/examples/objects/ExampleObject.java index 7ad35be..f611cfe 100644 --- a/src/main/java/examplemod/examples/ExampleObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleObject.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.objects; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.PlayerMob; diff --git a/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java new file mode 100644 index 0000000..340846b --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleOreRockObject.java @@ -0,0 +1,25 @@ +package examplemod.examples.objects; +import necesse.level.gameObject.RockObject; +import necesse.level.gameObject.RockOreObject; + +import java.awt.Color; + +/** + * Example ore rock that uses our ExampleIncursionDeepRockObject as its parent rock. + */ +public class ExampleOreRockObject extends RockOreObject { + + public ExampleOreRockObject(RockObject parentRock) { + + super(parentRock, + "oremask", // 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 new file mode 100644 index 0000000..cb71521 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExamplePressurePlateObject.java @@ -0,0 +1,18 @@ +package examplemod.examples.objects; + +import java.awt.Color; + +import necesse.level.gameObject.MaskedPressurePlateObject; + +public class ExamplePressurePlateObject extends MaskedPressurePlateObject { + + public ExamplePressurePlateObject() { + // 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. + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleTreeObject.java b/src/main/java/examplemod/examples/objects/ExampleTreeObject.java new file mode 100644 index 0000000..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..dd897cc --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleTreeSaplingObject.java @@ -0,0 +1,17 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.TreeSaplingObject; + +public class ExampleTreeSaplingObject extends TreeSaplingObject { + + public ExampleTreeSaplingObject(){ + // 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/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/objects/ExampleWallWindowDoorObject.java b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java new file mode 100644 index 0000000..2dea232 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWallWindowDoorObject.java @@ -0,0 +1,38 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.WallObject; +import necesse.inventory.item.toolItem.ToolType; + +import java.awt.Color; + + + +public final class ExampleWallWindowDoorObject { + private ExampleWallWindowDoorObject() {} + + public static int EXAMPLEWALL; + public static int EXAMPLEDOOR; + public static int EXAMPLEDOOROPEN; + public static int EXAMPLEWINDOW; + + public static void registerWallsDoorsWindows() { + // registers wall, door pair and window objecs + int[] ids = WallObject.registerWallObjects( + "example",// prefix + "examplewall", // texture name + "walloutlines", // outline texture + 0.0F, // tool tier + new Color(255, 220, 80), + ToolType.PICKAXE, + -1.0F, // wall broker value + -1.0F, // door broker value + true, // obtainable + true // count in stats + ); + + EXAMPLEWALL = ids[0]; + EXAMPLEDOOR = ids[1]; + EXAMPLEDOOROPEN = ids[2]; + EXAMPLEWINDOW = ids[3]; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java new file mode 100644 index 0000000..eaf623f --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWoodChairObject.java @@ -0,0 +1,11 @@ +package examplemod.examples.objects; + +import necesse.level.gameObject.furniture.ChairObject; + +import java.awt.*; + +public class ExampleWoodChairObject extends ChairObject { + public ExampleWoodChairObject(){ + super("examplechair", new Color(116, 69, 43)); + } +} diff --git a/src/main/java/examplemod/examples/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/ExamplePacket.java b/src/main/java/examplemod/examples/packets/ExamplePacket.java similarity index 98% rename from src/main/java/examplemod/examples/ExamplePacket.java rename to src/main/java/examplemod/examples/packets/ExamplePacket.java index f0255c8..6264da2 100644 --- a/src/main/java/examplemod/examples/ExamplePacket.java +++ b/src/main/java/examplemod/examples/packets/ExamplePacket.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.packets; import necesse.engine.network.NetworkPacket; import necesse.engine.network.Packet; diff --git a/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java new file mode 100644 index 0000000..fe80682 --- /dev/null +++ b/src/main/java/examplemod/examples/packets/ExamplePlaySoundPacket.java @@ -0,0 +1,45 @@ +package examplemod.examples.packets; + +import examplemod.ExampleMod; +import necesse.engine.network.NetworkPacket; +import necesse.engine.network.Packet; +import necesse.engine.network.PacketReader; +import necesse.engine.network.PacketWriter; +import necesse.engine.network.client.Client; +import necesse.engine.sound.SoundEffect; + +/** + * SERVER -> CLIENT packet: + * Tells the receiving client to play the example sound at a specific world position (x, y). + */ + +public class ExamplePlaySoundPacket extends Packet { + public final float x; + public final float y; + + // Decode (CLIENT receiving) + public ExamplePlaySoundPacket(byte[] data) { + super(data); + PacketReader r = new PacketReader(this); + x = r.getNextFloat(); + y = r.getNextFloat(); + } + + // Encode (SERVER sending) + public ExamplePlaySoundPacket(float x, float y) { + this.x = x; + this.y = y; + + PacketWriter w = new PacketWriter(this); + w.putNextFloat(x); + w.putNextFloat(y); + } + + // Runs ONLY on client + @Override + public void processClient(NetworkPacket packet, Client client) { + if (ExampleMod.EXAMPLESOUNDSETTINGS != null) { + ExampleMod.EXAMPLESOUNDSETTINGS.play(SoundEffect.effect(x, y)); + } + } +} diff --git a/src/main/java/examplemod/examples/ExampleConstructorPatch.java b/src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java similarity index 95% rename from src/main/java/examplemod/examples/ExampleConstructorPatch.java rename to src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java index 7b48c45..eb20d2f 100644 --- a/src/main/java/examplemod/examples/ExampleConstructorPatch.java +++ b/src/main/java/examplemod/examples/patches/ExampleConstructorPatch.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.patches; import necesse.engine.modLoader.annotations.ModConstructorPatch; import necesse.entity.mobs.friendly.critters.RabbitMob; @@ -22,5 +22,4 @@ static void onExit(@Advice.This RabbitMob rabbitMob) { // Debug message to know it's working System.out.println("Exited RabbitMob constructor: " + rabbitMob.getStringID()); } - } diff --git a/src/main/java/examplemod/examples/ExampleMethodPatch.java b/src/main/java/examplemod/examples/patches/ExampleMethodPatch.java similarity index 99% rename from src/main/java/examplemod/examples/ExampleMethodPatch.java rename to src/main/java/examplemod/examples/patches/ExampleMethodPatch.java index 00c5082..6bc6809 100644 --- a/src/main/java/examplemod/examples/ExampleMethodPatch.java +++ b/src/main/java/examplemod/examples/patches/ExampleMethodPatch.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.patches; import necesse.engine.modLoader.annotations.ModMethodPatch; import necesse.inventory.lootTable.LootTable; diff --git a/src/main/java/examplemod/examples/patches/JobFinderSafePatch.java b/src/main/java/examplemod/examples/patches/JobFinderSafePatch.java new file mode 100644 index 0000000..407bcf1 --- /dev/null +++ b/src/main/java/examplemod/examples/patches/JobFinderSafePatch.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 JobFinderSafePatch { + private JobFinderSafePatch() {} + + @SuppressWarnings({"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/JobFinderStreamFoundJobsHandlersPatch.java b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.java new file mode 100644 index 0000000..d5e7499 --- /dev/null +++ b/src/main/java/examplemod/examples/patches/JobFinderStreamFoundJobsHandlersPatch.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 JobFinderStreamFoundJobsHandlersPatch { + + @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 = JobFinderSafePatch.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/presets/ExampleCodePreset.java b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java new file mode 100644 index 0000000..7d805f4 --- /dev/null +++ b/src/main/java/examplemod/examples/presets/ExampleCodePreset.java @@ -0,0 +1,128 @@ +package examplemod.examples.presets; + +import examplemod.examples.ExampleLootTable; +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.registries.TileRegistry; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +/** + * ExamplePresetCode + * This class describes a small "structure" (a room) that the game can stamp into the world. + * In Necesse, a "Preset" is basically a small grid of tiles + objects that can be placed onto a level. + * This version builds the room using normal Java code (loops and variables), + * instead of using a big PRESET={...} text script. + */ +public class ExampleCodePreset extends Preset { + + /** + * Constructor + * Constructors run when you create the object: new ExamplePresetCode(random) + * The GameRandom is passed in so things like loot can be randomized, + * but still be repeatable (important for world generation). + */ + public ExampleCodePreset(GameRandom random) { + + // This calls the Preset parent class constructor. + // It sets the size of the preset to 15 tiles wide and 11 tiles tall. + super(15, 11); + + /* + * Tiles and Objects in Necesse use numeric IDs internally. + * + * - TileRegistry.getTileID("name") looks up a TILE by its string ID + * - ObjectRegistry.getObjectID("name") looks up an OBJECT by its string ID + * + * We store those numbers in variables so we can use them repeatedly. + */ + int floor = TileRegistry.getTileID("stonefloor"); // ground tile + int wall = ObjectRegistry.getObjectID("stonewall"); // wall object + int air = ObjectRegistry.getObjectID("air"); // "nothing here" object + int storagebox = ObjectRegistry.getObjectID("storagebox"); // chest/container object + + /* + * Fill the entire preset area with a base: + * + * - Every tile becomes stone floor + * - Every object becomes air (empty) + * + * width and height are fields from the Preset parent class (because we called super(15, 11)). + */ + for (int x = 0; x < width; x++) { // loop across columns (left -> right) + for (int y = 0; y < height; y++) { // loop across rows (top -> bottom) + setTile(x, y, floor); // place the floor tile at (x, y) + setObject(x, y, air); // clear any object at (x, y) + } + } + + /* + * Build the walls around the edge of the preset. + * + * First: top wall (y = 0) and bottom wall (y = height - 1) + */ + for (int x = 0; x < width; x++) { + setObject(x, 0, wall); // top edge + setObject(x, height - 1, wall); // bottom edge + } + + /* + * Next: left wall (x = 0) and right wall (x = width - 1) + */ + for (int y = 0; y < height; y++) { + setObject(0, y, wall); // left edge + setObject(width - 1, y, wall); // right edge + } + + /* + * Choose a position in the middle of the room for the storage box. + * + * width / 2 and height / 2 are integer division in Java. + * Example: 15 / 2 becomes 7 (Java drops the .5) + */ + int storageboxX = width / 2; + int storageboxY = height / 2; + + /* + * Place the storage box object at the centre. + * + * setObject(x, y, objectID, rotation) + * + * Some objects use rotation to decide which way they face. + * 0/1/2/3 usually mean different directions. + * For a storage box it usually doesn't matter much, but we set it anyway. + */ + setObject(storageboxX, storageboxY, storagebox, 1); + + /* + * Fill the storage box with loot. + * + * ExampleLootTable.exampleloottable is your custom LootTable from the other class. + * + * addInventory(...) searches for an object with an inventory at that position + * (like a storagebox) and then generates loot into it. + */ + addInventory(ExampleLootTable.exampleloottable, random, storageboxX, storageboxY); + + /* + * OPTIONAL SAFETY RULE (CanApply predicate): + * + * "Only allow this preset to be placed if the area is suitable." + * + * addCanApplyRectEachPredicate(...) checks every tile in a rectangle. + * If ANY tile fails the test, the preset cannot be applied there. + * + * Our test: + * !level.getTile(levelX, levelY).isFloor + * + * Meaning: + * - If the tile already IS a floor, then this returns false + * - If the tile is NOT a floor, then this returns true + * + * In plain English: + * "Don't place this room on top of an area that already has flooring." + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor + ); + } +} diff --git a/src/main/java/examplemod/examples/presets/ExamplePreset.java b/src/main/java/examplemod/examples/presets/ExamplePreset.java new file mode 100644 index 0000000..e19d01b --- /dev/null +++ b/src/main/java/examplemod/examples/presets/ExamplePreset.java @@ -0,0 +1,114 @@ +package examplemod.examples.presets; + +import examplemod.examples.ExampleLootTable; +import necesse.engine.util.GameRandom; +import necesse.level.maps.presets.Preset; + +/** + * ExamplePreset (Script-based) + * This preset is the same idea as the code-built room, but it is created using a big text string + * in Necesse's "PRESET script" format. + */ +public class ExamplePreset extends Preset { + + /** + * You pass in GameRandom so anything random (like loot) can be rolled properly. + * In world generation, Necesse often uses a seeded random so the same world seed + * produces the same results every time. + */ + public ExamplePreset(GameRandom random) { + + // Create a preset that is 11 tiles wide and 11 tiles tall. + // The Preset parent class uses this to create arrays for tiles/objects/rotations. + super(11, 11); + + /* + * This is a PRESET script string. + * + * It's basically a "saved blueprint" of a structure. + * The game can export these, and you can paste them into code like this. + * + * The important parts + * + * width / height + * - Size of the structure. + * + * tileIDs + tiles + * - "tileIDs" is a list of tile types used in this preset. + * - "tiles" is the full grid. + * - Each number in "tiles" refers to an entry from tileIDs. + * + * objectIDs + objects + * - Same idea as tiles, but for objects + * - "objectIDs" is the palette. + * - "objects" is the full grid. + * + * rotations + * - Rotation for each placed object (same length/order as the objects grid). + * - Most objects use rotation 0/1/2/3 for directions. + * + * ...Clear flags... + * - These tell the game whether it should clear decorations/walls/etc when stamping the preset. + * + * The string is huge because it contains *every tile* in the 11x11 grid. + * 11 x 11 = 121 entries, which matches the long arrays you see. + */ + String examplePresetScript = + "PRESET={width=11,height=11," + + "tileIDs=[98, exampletile]," + + "tiles=[98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98]," + + "objectIDs=[0, air, 290, storagebox, 1436, examplewall, 298, walltorch]," + + "objects=[1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 298, 0, 0, 0, 0, 0, 0, 0, 298, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 290, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1436, 1436, 298, 0, 0, 0, 0, 0, 0, 0, 298, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436, 1436]," + + "rotations=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2]," + + "tileObjectsClear=true,wallDecorObjectsClear=true,tableDecorObjectsClear=true," + + "clearOtherWires=false}\n"; + + /* + * applyScript(...) reads that big PRESET string and fills in: + * - which tiles exist at each coordinate + * - which objects exist at each coordinate + * - which rotations the objects use + * + * After this line runs, this Preset now "contains" that room layout. + */ + this.applyScript(examplePresetScript); + + /* + * Add loot into the storage box inside the preset. + * + * The idea here is: + * Coordinates here are PRESET coordinates, not world coordinates. + * + * So (5, 5) means: + * - 5 tiles from the left edge of the preset + * - 5 tiles from the top edge of the preset + * + * We are assuming the storage box was placed at that coordinate in the script. + */ + addInventory(ExampleLootTable.exampleloottable, random, 5, 5); + + /* + * Optional placement rule: + * + * addCanApplyRectEachPredicate checks a rectangle area and decides if the preset is allowed + * to be stamped there. + * + * This can prevent things like: + * - placing the room on top of an existing base + * - overwriting important tiles + * + * The lambda (level, levelX, levelY, dir) -> ... is a short way to write a function. + * + * Our rule says: + * "If the world tile is already a floor, do NOT allow the preset to be placed." + * + * The ! means "not". + * So: + * - if isFloor is true, !isFloor is false then placement fails + * - if isFloor is false, !isFloor is true then placement is allowed + */ + addCanApplyRectEachPredicate(0, 0, width, height, 0, + (level, levelX, levelY, dir) -> !level.getTile(levelX, levelY).isFloor + ); + } +} diff --git a/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java b/src/main/java/examplemod/examples/projectiles/ExampleArrowProjectile.java new file mode 100644 index 0000000..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 96% rename from src/main/java/examplemod/examples/ExampleProjectile.java rename to src/main/java/examplemod/examples/projectiles/ExampleProjectile.java index 8e46ce8..6ce8da7 100644 --- a/src/main/java/examplemod/examples/ExampleProjectile.java +++ b/src/main/java/examplemod/examples/projectiles/ExampleProjectile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.projectiles; import necesse.engine.gameLoop.tickManager.TickManager; import necesse.entity.mobs.GameDamage; @@ -65,7 +65,7 @@ public Trail getTrail() { @Override public void updateTarget() { - // When we have traveled longer than 20 distance, start to find and update the target + // When we have travelled longer than 20 distance, start to find and update the target if (traveledDistance > 20) { findTarget( m -> m.isHostile, // Filter all non hostile diff --git a/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java new file mode 100644 index 0000000..c7f7dc2 --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/jobs/ExampleLevelJob.java @@ -0,0 +1,123 @@ +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; + +/** + * 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() { + // Use the base checks (it will call isValidObject on the current object) + return super.isValid(); + } + + @Override + public boolean isValidObject(LevelObject object) { + // Do NOT let settlers clear objects that a player placed. + if (getLevel().objectLayer.isPlayerPlaced(this.tileX, this.tileY)) return false; + + // Only allow this job to target grass objects. + return object.object != null && object.object.isGrass; + } + + @Override + public boolean isSameJob(LevelJob other) { + // 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; + } + + @Override + public boolean shouldSave() { + // 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(); + + // 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") + ); + + // 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 new file mode 100644 index 0000000..2512e79 --- /dev/null +++ b/src/main/java/examplemod/examples/settlement/settlers/ExampleSettler.java @@ -0,0 +1,36 @@ +package examplemod.examples.settlement.settlers; + +import java.util.function.Supplier; +import necesse.engine.localization.message.GameMessage; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.util.TicketSystemList; +import necesse.entity.mobs.friendly.human.HumanMob; +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.maps.levelData.settlementData.ServerSettlementData; +import necesse.level.maps.levelData.settlementData.settler.Settler; + +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/java/examplemod/examples/tiles/ExampleGrassTile.java b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java new file mode 100644 index 0000000..46a5f5a --- /dev/null +++ b/src/main/java/examplemod/examples/tiles/ExampleGrassTile.java @@ -0,0 +1,125 @@ +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.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; + +/** + * 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 { + + // 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() { + // Texture file: resources/tiles/examplegrasstile.png + super(false, "examplegrasstile"); + + 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) { + // 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) { + // Off-screen simulation: schedule growth while the chunk is not actively ticking + addSimulateGrow(level, x, y, growChance, ticks, "examplegrass", list, sendChanges); + } + + /** + * 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) { + + // 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() { + // 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 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) { + 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) { + // Pick a random row for the sprite, but keep it consistent per tile position + int row; + synchronized (drawRandom) { + row = drawRandom.seeded(getTileSeed(tileX, tileY)) + .nextInt(terrainTexture.getHeight() / 32); + } + return new Point(0, row); // column 0, chosen row + } + + @Override + public int getTerrainPriority() { + // Used when tiles overlap/compete in drawing/spreading rules + return 100; + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/ExampleTile.java b/src/main/java/examplemod/examples/tiles/ExampleTile.java similarity index 97% rename from src/main/java/examplemod/examples/ExampleTile.java rename to src/main/java/examplemod/examples/tiles/ExampleTile.java index 9189d36..2b981be 100644 --- a/src/main/java/examplemod/examples/ExampleTile.java +++ b/src/main/java/examplemod/examples/tiles/ExampleTile.java @@ -1,4 +1,4 @@ -package examplemod.examples; +package examplemod.examples.tiles; import necesse.engine.util.GameRandom; import necesse.gfx.gameTexture.GameTexture; diff --git a/src/main/resources/buffs/examplearmorsetbonusbuff.png b/src/main/resources/buffs/examplearmorsetbonusbuff.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/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/examplebar.png b/src/main/resources/items/examplebar.png new file mode 100644 index 0000000000000000000000000000000000000000..f23f5a87b38e921058fca9a3c717a528ce4a27c2 GIT binary patch literal 784 zcmV+r1MmEaP)Vfd5ax3Q23dv&43Duo zKv_nS@$QrRC~m+OB9w%{f1u?I3mGPXE#d1IL9r~bFbdsaSVM#)2cY?!q}T(67EC{K zNRSf(czyn<@D12uu#kbdGM*t179uDiL6ie1@i}@hp@#%1AprOJPtjitY^A(lvw!^l z38p{wyk%en`jMTHje!A3Ut_q1VmB;A&_e>R1916#k=0&?f5-lzdd`Z0fr){UA$f-f z1Je)U%Qs>|03*(sk1#R(`}+?g5&j2y{Tjnz9!u)GF?3RZ`q zI6#{b5?q!tW(;qiyd~(9y)qXV7-UeDS-(&LrU6ExLjhX|{6GHW6vNg2lUNM_<@5jk zz+{fa4p0@mx191*L?;a33jkLMPS7Q3W0U+ zw}Hz6Sez#_6oTdT7_=CFF#N&C{~2;F){#c!m#KP!!?Gh z|8Eg?0J_g_Fx)}a1oY5z3}!Mc(J?WgrcRLM*nAGthtmP*K1a{TFbjhiyufCm$2lRN6LbIrg@BOHu{nSd z=xPRFszqtZ%w_0DaSD3Kz{>b!BI6w0U*IT!#SyGLpv3dU`W#;rz*7-1AwsUtu{r=f zwUQDdr23qcM2I&;h)wOdqLrk60W?G~n%nsDIid7KPC>-TKt(X%-E#odEd1ChP)q{= O0000000BXNkl< zU|?YU4+IPY1Q_wz&B(||6=&hIgM|ni{!{1*yuM&UHXP(*WUR~}O>qb)F-S3>n~P#6 za_~Td9L2{Q7&_qm{|pcaG5{iotQ6u=bngZ+RG|x@L_0VHU@lZ)kiqaVD+3Dy2(vIS zGk`FJ4H9Dpi-Qs|1pfzG^q&#vL==o}Ajl4weGJeXz<})YZ47TNf*Ks~c!K>u+}i(0k;Q-mUegC0W|AppuU3^)P0;b2dIRWo9cAOS|CtjPEu!9ys< z;sAv0*bQZ1z~&5?C_)ae<#0=|<^p0u0p@oye2x;nI2=GI6c9mAiqC-_V<4CYUMMGX{?1QWPAWkM;1P;&t&I#BaDA)h1C8X{XaLj|ir^j3zRci3VF zQuzKui33mxhg`^Dv)qfJ2-Xg0gPL0pr2oUbiyo;AAVV0jxBxvwh_M`BIs>(e8IfEB zVpbzGFhNVbe+>UIk{u}M(4z%=nr1}R-N3+r5I_$BM(nO-!Wkkk^|<1kkpX`N266z* zQnYye&w!~JyF&ortLsdr08&F-Sin?)(RL z1l|x}M9wep==g`0I0^X}!x<05{hy0|+- z0jxv>+4~<)2q65A84`%FCDvjru>tlMh67M7Lv;iLUVz=l3=EhhCXzAOLjcne3nl$t6Vi00000NkvXXu0mjfujrhe 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..4f01a02cd361ee078906a93b5d5a4a8838b46b38 GIT binary patch literal 406 zcmV;H0crk;P)wqQne6+Aj3O`_YAZEAhTiiz{(NyT<{-cIeJ{e;vVKhLXI91^e)po-677NLg}ELXtffw>4C5(AzK7@Zj$Kn?@DNSQ$qZ}i?}xDS?R zXJBLe$?ywBEl`XBC`OVwKm!=bstZ7t6Q~V|&O|5 zfCy(Z64@dm)&cM+V5D-vL$rCLU9-`S$$*Cd09! literal 0 HcmV?d00001 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/items/examplechair.png b/src/main/resources/items/examplechair.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8293e93f88893ba882f2a9f1a21c17f45f0492 GIT binary patch literal 423 zcmV;Y0a*TtP)m{lXa~T2PRJHw)xpde5@~?gJ|J0r#R2c00{{U(Pov49 R&?x`_002ovPDHLkV1m)=sAd2F 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..e12990509b00069cce688fc8cb818fb33ce792c8 GIT binary patch literal 648 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 VWjLedf8hWC002ovPDHLkV1kTmvvvRg literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplefood.png b/src/main/resources/items/examplefood.png new file mode 100644 index 0000000000000000000000000000000000000000..1138beff4827c9a3567fe0de1d8b140ed0d6c1c5 GIT binary patch literal 431 zcmV;g0Z{&lP)O8umGCKEdL5@ZEJWBqX(D07X9w0q2=lpsAUDH@_g^V; z0MHCZpc!B{U<(O+A@CohF(|@{qG(vR;uy>WSVIEE0cbv_YW@I)Cd_=~kiZrKC_X=P z>=s21frTKhkQlt#5ax3eD>aHjfKUhy?t%#9^@PORSVO>y@ikWQw0WfEn!z6eFmEoH zuL@?)N;(Fn9fibELf|74`)`l{P@WY;6B`mkCj<~_K@{xs_P$P(5LjJO2QE{LMCATM zLjqj#8KsCY-~&S^1YoHiJ?1sSZaA7_71;&-Bwu%N16dN`l<;s>kdGewDNsdPM{(C?@ jJdeyCX6#Q8VPJSN(fh&EBQvFefyUtJ>gTe~DWM4fxEh5H 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 Xc4f1tv6Xe5MR^EJ!|-Aq?hVydi=kiZ%RPz_)o z5bJYvMqiAU@ln9uml`Qu?#UJ4+{wyKN%GHC!3yt`M7+(iD45=JxXpE@I=T$YB0lmXtl`- zMe8w!V`RnH;LnDjxI>CRqJ2rMzQLacIv6^@zOG=XK=Hj518N#T4<(pBLTO|0hXBk6 z=plg~BCy~g*5@$w10Dj{N3{P?D#To^$xC&S6rO@SULh@g~BR z{~?_bazX-RIZQu3SCEnmP(lLcQ*^_L^*Kx*QN3fBVrWPZ>LW4|<8zpqEMz1yMg}SZ Y00;b`iN#~YFaQ7m07*qoM6N<$f+&yGH~;_u literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplehuntincursionitem.png b/src/main/resources/items/examplehuntincursionitem.png deleted file mode 100644 index d05149eae91a7340817c289fbc42e783a908097f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 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-(HYPJYQ@=HTfI^L)E{-7) zhm#W|Se0cB9!>o1&%b*HTQbL=^n`?j6%U1%ta8mdKI;Vst065QL-T(jq diff --git a/src/main/resources/items/examplehuntincursionmaterial.png b/src/main/resources/items/examplehuntincursionmaterial.png new file mode 100644 index 0000000000000000000000000000000000000000..bfd5c9495458e3d18c68d4c55d9552576b6415f2 GIT binary patch literal 284 zcmV+%0ptFOP)MN8e0Qnq*C77709s(dUVdf(n zj*<(YK@awM4WA%Y9ntdXJ*36=HamJb7-21Z*%44|em48!6Z+9(CJ4XK8a8@kALV(pqCH%_V8GG!#SUqGAQ i37U^aJEsF)4gdfGym$y%S!rVc0000A13dHs?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/examplejobobject.png b/src/main/resources/items/examplejobobject.png new file mode 100644 index 0000000000000000000000000000000000000000..214f4fa360af024fc5afbb24306d139044066b3f GIT binary patch literal 291 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&Y~rV4mpi9Ssyp_jGX# ziD*sUBE0SId4qNSwSk3J%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/items/examplelog.png b/src/main/resources/items/examplelog.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f130b5aaa90e3a9aea7ddc164fa03ff59c1c54 GIT binary patch literal 477 zcmV<30V4j1P)QUYa9YDcgEWYqtr+pyK~XkD_c1pEH^Wzk zuP6=xiZKGkz;3`664VF*xP>5#tQjm(JbaYlI7}XENRZ?J96ly07KIsv7-0I5LxP+T z!0Tg}Yg8DNz)TpukYOp-kihEzTt1#(Hw&w;=QPbHIZYE20z~;3WHZdSAoc-#@2gTc4hIcp(LiRDT<)j1yz7Y5ivOJz48pS+dTr#9EBr*`|%IYwyU@;OW)Roerg_y+kHXgDJh zCO#K15D5-69b^DyM{LRo`50XfwkTi(6+p1E8(TSmW(Xq#1pvhXuvChxWrNEed@UYA zZo`)dQ9=S{5HNHY@fFXc1_s_15i}$)npl+hn3y!cK+QNN6e6Qtv(c{EfQJA8mU4sI T=}SgWX}cq5|Tc2vG-**th^R$8C#!rw zF%4G6Q<4i{Apvp?F?9yk(hS`J@)FDp4`2MnS`X}>EC%*Dxgmiy8=||MmgNEq3Hc49 z#8Z=HVQ|cS1r8ZlNRV1O5aj?|afvTvh)oMD3{(cUR!D+P1m+5&$_JV^$w1MBuY91W zPKQMS$dX%+|6olEz%+ule4r=_s8McEGYv2z=L%TKNhpa?#Q~Ir1XX)Rurz_A8F literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplemeleesword.png b/src/main/resources/items/examplemeleesword.png new file mode 100644 index 0000000000000000000000000000000000000000..5e8aa5e825360cf43af0134fedf97d08fdf123aa GIT binary patch literal 464 zcmV;>0WbcEP)WXE*2~{;z|^Zw1_RDqd=2EodFa=ghGi_N3!73 zu##aRSW1OK8B7~9*uX-Dsv$vgBBXXm;0%Gg4EIq2=Pbh!nuG+|*^pWxfl?B}5||W& zB*Qa?=ioSRV`u>TT$w=;Jx#zuh@#RIB?@5qoRpLf3ki^G$SEhVx}KgPfi)456A}z$ z0v00lF@n-W6+;6`nqZ(3!0P}~LPC*2fq|^L1Y`*z4D&1~B!KMy$QW2+{s&5`LlR*u`+p&PPtbl0*=1vql2fSI%KuS4)u9i^H zBbO6EwXl*A#ZHP6A))%8m~w&|xqwhW;B7^Zc1{O87XSbPsTJ 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..d9433bd8755c3dafd0dba3224728c6582f422b2c GIT binary patch literal 549 zcmV+=0^0qFP)pg%oa$L9NQ1;?A4Yt3;LV1(e9F(j!|;*e z3sDXLs$~SK1-k)TNDvbO7#4vH^k%RH8>+e43abaN`yK;}Z)G?P(~mVIusQ(db5RCC zhSv=5sZvsaLJejva!6o}0;oc;%d;86!47!9@C;0E33yD>I@2Cjkg&PX#k{#P%cpL#qTtfC_r}|Azy(K zm@}Oh^#xAY4Aj>i{&v;Vl_n25tsmYR20X1FB*C%000BXNkl< zU|?YU4+IPY1Q_wz&B(||6=&hIgM|ni{!{1*yuM&UHXP(*WUR~}O>qb)F-S3>n~P#6 za_~Td9L2{Q7&_qm{|pcaG5{iotQ6u=bngZ+RG|x@L_0VHU@lZ)kiqaVD+3Dy2(vIS zGk`FJ4H9Dpi-Qs|1pfzG^q&#vL==o}Ajl4weGJeXz<})YZ47TNf*Ks~c!K>u+}i(0k;Q-mUegC0W|AppuU3^)P0;b2dIRWo9cAOS|CtjPEu!9ys< z;sAv0*bQZ1z~&5?C_)ae<#0=|<^p0u0p@oye2x;nI2=GI6c9mAiqC-_V<4CYUMMGX{?1QWPAWkM;1P;&t&I#BaDA)h1C8X{XaLj|ir^j3zRci3VF zQuzKui33mxhg`^Dv)qfJ2-Xg0gPL0pr2oUbiyo;AAVV0jxBxvwh_M`BIs>(e8IfEB zVpbzGFhNVbe+>UIk{u}M(4z%=nr1}R-N3+r5I_$BM(nO-!Wkkk^|<1kkpX`N266z* zQnYye&w!~JyF&ortLsdr08&F-Sin?)(RL z1l|x}M9wep==g`0I0^X}!x<05{hy0|+- z0jxv>+4~<)2q65A84`%FCDvjru>tlMh67M7Lv;iLUVz=l3=EhhCXzAOLjcne3nl$t6Vi00000NkvXXu0mjfujrhe literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplepotion.png b/src/main/resources/items/examplepotion.png new file mode 100644 index 0000000000000000000000000000000000000000..1164dc07c58813c786dd92c04047056add284e31 GIT binary patch literal 599 zcmV-d0;v6oP)6N%0c{UI$cH%tA3tL{W|5$B*B5E#Q|@V?fu}+L4A+ z4>2JCPi$}QKVtAHg&20$X#t9%4*L?o{4L$HNXi{p9RTwIzqkm)$CqyyC;=cdVCG>9 z349^&A7nYq$BeHa&OrA$%yH+SA>z9N5<;-FffOQGa|aW{00V+)0PgdM1w=XZEL5HG ziUgw6qd0(?agT00uC#zH1W@Asz4j)sSxO8-V7lW^4_=>Du#|x1-#bF|`CNsh30!Gm z@TLJ++%qsdg!)(>#pf`e;tmdoK7={gRSy1KfN=Li6fdBs1w@EEWFQ3$-VlJ5WDKzK z3$28&U~j?ox9BAt z%*U`ehpERE_b_>^)iE{G0j1P^W<>H01C}-)igH3eMo&Q`^^BmogHUgl5nn49 l-2vqEj2IcH2nM`+4giMAwk&u(WV-+W002ovPDHLkV1lD=0q+0+ literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplepotionitem.png b/src/main/resources/items/examplepotionitem.png deleted file mode 100644 index d06e5b2d467ffd2924f39c7c8d98a986a368915a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmV;`0V@89P)Px$k4Z#9R9J=Wm$6F2P!xv0YDWhZad1#~N5Q#qaB2kyK}4`FI=P5q@ByTvxH#!l zK}x})V4d{|Dh~Dmf`j6s4^W81?Ww&tY0?zo2J~N&d+&b__y5nyNdkWznL#)1t*`jq zwpjbnL9^ki-_|&3&WPBKa+36_&UY=ip2*Mq=MT04$b6`IQ@P-&JzuCQ zzSIWbLvJV2>wuu6e68G9K;g|-i+%B}I}pf=nftG~J8iwxI*SbGX7{!6Xmc5W`_n5R zSPRx2=vf1jd+^%V8ExHE8iZ%W2}gPz2*T4wW-a_UqVwXAUI#jzOlsEG*WK)@u_Ii0 zS4Rwmbq9LBsiYqiHdR0PbS?lWwQeH=iZf%6y8zU$!=HbFbq8Xf0?DB$a-C?t@ZxeW zNUsB3In(Mt-&6|C4kwE1?VcYJCluBlNc>-5ej}QQQ*6Sz1ONC2JQ``@%<(6 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/items/examplesapling.png b/src/main/resources/items/examplesapling.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba0488861fef03c26fb93275ae455da9a2dac3b GIT binary patch literal 514 zcmV+d0{#7oP)M)A4A;{MtOm0YG3js!ufiNG!d;|*#7>ynh=;~k&l46j+>oybzz*04_shJwt z5nmd>84~7oW^lXn7_#B)lTD`>(0vRHNw7RHR2i`)P^=E1MM!`w$5w(8lMNZg8N?V4 zF&qWwZxse*xQ{0_O=Wn;@E*(;Vi3d{Be;AHQ;gREFc;@Arjs4rxO`5O1JG<`{LJu~ zVHLyL|K<#kG>})9jiRKushmNGL5Pu;frn@Z6IBvU`9BkEpcSJzL4)4`BW26~?bLMu zF4HD9O<@oQ`WYmDp5Y={(KNUkB&2#Fzb*%^C59o20h9(vHDka-fTUJ2G!cT!16~GR zuxAAs1ioLs=j&cF`xEqdFJtkyDFT{Cw^7q|mJ z#WOVQNlJXM_@X2cGX7@x&B(&Q0`fW8eTC7^>42950GHN)TOJO++W-In07*qoM6N<$ Ef+D`n(*OVf literal 0 HcmV?d00001 diff --git a/src/main/resources/items/examplestaff.png b/src/main/resources/items/examplestaff.png deleted file mode 100644 index 40200b40a213fe3a77c6f60364f6c1c1bf3c70f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 444 zcmV;t0Ym*(xk6w^zld>oRFY; mIe--BM70%1JEsHQIRyYKsDcCwvE`Kj00009U0Y diff --git a/src/main/resources/items/examplestone.png b/src/main/resources/items/examplestone.png new file mode 100644 index 0000000000000000000000000000000000000000..e8ce2a29832afdca67c08be20757d2f81d161bbc GIT binary patch literal 520 zcmV+j0{8uiP)50cbuaDb^$z#2H}vv4sRtQ2;dn>?1-xhq+FR zK^@F&Wax$oF=BH76T<)l7HZiK^D)dkU>YFGXsixE&*vNr>()^YzRt_(F|!A!ToE)3!G7a9uh1JbONvtLa!qRUr9)8 zq5=gRD8IveMoP*@4)nnn0)$dEa*1acV9x;KvoRn_NRwX~yR+<%8_)0>QG=a+nz|aD>dSPh?SBSxU4pNWoL97lSB~6gpQUa+* zZk!TR7cdeT{&+(S-RCefsoFV3ZoXr2G}dN0EowtXQc~=Ich3P6x2ahvz7*B~0000< KMNUMnLSTYKjLU5R literal 0 HcmV?d00001 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/items/examplesword.png b/src/main/resources/items/examplesword.png deleted file mode 100644 index 57758c18ab539cd31d4e1408283dfebf51ec79f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmV;v0YUzWP)WXE*2~{;z|^Zw1_RDqkvzHg#i>oghGi_N3!73 zU_Vy~EXAt?QL@GH6)a?^8WJQYLTZNu&Jg%~{|`#w`0N)0^Qj#YWM@Nag#=1T2uomG z5=;!=Ui<^cxls)tBrPbipr;8~2vJm;qC^2KpOcc(VIcu>4LRimR@c)rB(NqzazcWE zOu#~fK1NWQIM?zCB~36;3E*`ADIvil&&)thT|!g}#+D{v0RSs84<^5Z=L#qTy-vYu z7co)5Kx#33h;5#B+GaV9TZ~q@*Qu z1E^6mvQQy0GlD__mi;AFu=^i783YE;6KQ6F9vt4K9^#UKylMOhWlXt1%^vtT8lvgOkZQTfj1;j z8~}5v5Q88v_TDp46YvA03|mNG3xWUd7~V5PF@%Bb|IF|OOy6X5n2H#2O*8WKzl0}ODcfd>qaz)mw^Fa*;uAA)@DQs<1-+3TA&GQdI%rU0f7 zmp=z@2*6S`us}g^JhBfjG+ltp-Dh}+5>hY;m^ni)1WXx>vHA!(P7Ue|;2twzFkqO| zG#|sGpezj%e8KP%Yak5XG;o*U9#$6%FbKfKvHAD|!v_XZz@Q8PMo@}rha%4jvnVgGZ}v{{J<~+r67XUR^+yT@C7}{a%{CB)&>c>16UYX82&K) z0hi_2LIT-hn9qTF7=e0F+(dMBjIL0fQI&y-fwbiEi{Tf;&HuLvrbnXMg#Q@+F~EEy z!XS*-&_@i9!Scj97_S4A85O~Xf$H*)(54szk${(hhXJPl?*IEZbrIDj1X=FG;0ZPa z76O+TuAmsI$Djk|OEE}-=`9S~$!`-<;&XJbfej0>+jYu|@$*AA%h+@jQD(t%TWOMb<*$x2yxW_0000NSwSk3J%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/items/examplewall.png b/src/main/resources/items/examplewall.png new file mode 100644 index 0000000000000000000000000000000000000000..2d82ccc97cbd90a3d647fbf67d4a84208e6a21e4 GIT binary patch literal 560 zcmV-00?+-4P)d(cA;mtrRD!9usB!9Xw#d}sJhR>%{pk8WPY zl?Vxj04$Xgiqi`W7r-H9z+iw9Vz6?6>}-fF{H5xIF@&WUoEf$??Zp{gph69sB*74P z$nX%XPmDo~U|RUW@PmOA^&kUT7$^XDo1VaRUu3vQmPtfA;1$CwxCP1#GGI2c*k6Xf zC=NiD!{;)r4gjTCVroFkIwK^rjTlxpZNh06Nr@1YU|^{iJ?;TDO6K5a+P?QTC>#R_$I^J}SfmA@W z13-BkWH?d_2g5Fy8j3>zlw$FvgXK-@7$ks@6pKhogvjjy^f*>vP{3+0EVau(%Ltev zLUjS%+5=?Q1%%=i7D6ze!qgL+JLuQ+!I5An%7!2_afJ}EIRa}4;7YL!Gyqsb0J$!B y!SI44Ye`j)H3W#Q3#btw_|gC)10BGCch3Rb0)Tamz$s1u0000 The Example Settler [buff] examplebuff=Example Buff +examplearmorsetbonusbuff=Example Armor Set Bonus Buff [biome] exampleincursion=Example Incursion [incursion] -exampleincursion=Example Incursion \ No newline at end of file +exampleincursion=Example Incursion + +[itemcategory] +examplemodrootcat=ExampleMod +examplemodobjectsubcat=ExampleMod Objects +examplemodfurnaturesubcat=ExampleMod Furnature + +[jobs] +examplejobname=Example Job +examplejobtip=Keeps grass cleared in assigned zones. + +[ui] +examplejobzone=Example Job Zone +examplejobzonedefname=Example Job Zone {number} + +[activities] +examplejob=Doing Example Job + +[journal] +examplebiomesurface=Example Biome Surface +examplebiomecave=Example Biome Cave +examplebiomedeepcave=Example Biome Deep Cave + diff --git a/src/main/resources/mobs/examplebossmob.png b/src/main/resources/mobs/examplebossmob.png new file mode 100644 index 0000000000000000000000000000000000000000..e88afcb9172643e9141e187f88444916e66f23df GIT binary patch literal 10700 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 BSz-VH literal 0 HcmV?d00001 diff --git a/src/main/resources/mobs/icons/examplebossmob.png b/src/main/resources/mobs/icons/examplebossmob.png new file mode 100644 index 0000000000000000000000000000000000000000..068bd480a50a1ed098746980dba2e0ed7e5b025f GIT binary patch literal 449 zcmV;y0Y3hTP)@aYpt1_Sjl0`-8+hPfQw z#s5JH7J7P;-vI0E`EHE_%3!&#MSh`I( zeHvG)p-oxw^3NXz6c<|y2!LHcDE*>46FrqI`}B#4>Cx}s4730&AcK#5{|<}9e_%^+ zq+cRj$_i2=#?1Vm=@BqIsO{ocfB!Oo%41MHg;LAH(K9KZ$-#}WnJ|aIUCx5$QU*!@$hR=_80BL;KxTn$M0e>!hHrT7k!RpSkq5dQ z#%E+=pb@~DDhRn0<`$SbbeDtV>EmKzU5Xxh*!0oI#b>6kL~)EsW;oucMOTlkm_9DP za_kgV7oxiy=2Beh>6a?doeOgjx{L9-6lMm^!WgBjKzDJ%;$E;2dd`BS+Xb=3xKa&m z%8DN^-!Y)LSjWl?>;gjR7u}iYspR339ZXDLAHHCq1z`CGH2BTo3$RH1hinP4IS-af zfG%YP>1P)g`p@+35%pdC6Bx#z@)%T4q13X(q*IW~(A5ca~`j%ECZ;W z0>U5{gX+8gTyhc&z=&kzmX`qA$idCU0CW-FW<PqU%~5G?YbwJo9c1tWS3 j2&SKfKvH8QHz@)Dec6s_nqwru00000NkvXXu0mjf1;NCn 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/objects/examplebaserock.png b/src/main/resources/objects/examplebaserock.png new file mode 100644 index 0000000000000000000000000000000000000000..1dd1765b135b6e6df45cb8dc77e83841787f94d9 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 0 HcmV?d00001 diff --git a/src/main/resources/objects/examplechair.png b/src/main/resources/objects/examplechair.png new file mode 100644 index 0000000000000000000000000000000000000000..24218ef5434f53c6d0f52eacced22b111561ab8d GIT binary patch literal 1048 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 T@UOZ9w2;Bm)z4*}Q$iB}h6GWA 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..d56047fa6e5ca345bb83c97e3526015b9c0d2d85 GIT binary patch literal 2088 zcmV+@2-o+CP)n zV2J$B00E5i|1Sfx!WjcWG+3OAfs+AUK9MmBF8__;I|CYET={=JSO{4iObxnvZ05t% z;;;v9J~nkgF}Od_9fr+37GyOkiGV)zBt26GRP!$?d7vXB$Yuo!}cG_o0N46F>eVgMF%F!dmZ z{{_b$Cst?RvxgZP;~$gotJjJiv%8 zykV9gn}e+kU<4&&q?lwN5@3`iP<=3QtYr(9m?UH!+&nb%;VG6B_cQ+gkBE>*e(YdF z&OB6P*nG(dEa9MbjnPs4I>W_h5^#s0tzOltHPz+br0_(?C2>v&bzs~@|z<^)`(qM%kPnT}?V(9kWhU|Gpw3LEu8JrC& zu-F;c!1{oN6ELqbF%SW;=3!7Ig&9jTeE-eH`1uD1gBb5823W+w%!J9`do9Lr^|=h# zoCM2*j1S(5G90+2g<>wY0DyT4Czblm!3<%>Cm6ZdeloDL{9%+5 z`0yV&r13O%fNcyW2K3S$TWx|ZfI#ZBCCwSaOim(~BPfw@|BVQvw$xLwJSYLIQiK5%QfL7Iwn$6-6$8w2Xs9y0`pn0G53nHB*XS`1s;ik8aAyY+)&B=Z3IoGg zxNH6cQzip)AcFWM&kn)cDIou1WC2hJf;iSD@MwrK8)||Yr^50A6 zA_GXh9F~w^V_?IYmw`(EgFKI(U4SJxBMSr8_8C_7Fe_mZ1In7fm}Y=_{y(0`0n0Il zF$6F$Lm@P_z!I?1AK3s+Q8DM^ZnR)e@48y9k79j3_Bss7@K`8+2a6H{!22f)K)@}!x0_%mrEI_aKVR<)} zF&xxO1DlVf`hy1;fp!2&%!53Dt-B2i0DM^p>Rv|VY{vrgIG+0XKPYR!dRyqB%YX+E z=%q0-;0MUbm;p~p!POUmnU2dIcoqOF1SK!pFz;Op<>3KLMlxcL7n z6lr|DMP!BO;{~k1bom>Yu0d*8@Wd!esf(0F(DOQ>kS4cpLC9S6u)>+5&{HC`>SY84 z0t<3%z*=O4>SuJD(W|m3#t?A5f@@F(W)HC`fz%X(t{#0H0_1*H;JC$-|EsZ9xc|{h zT}EJI1vwevNT%>Si>+2cN@=)K5YaJ8zV%iBFX=r;W<*%7G#mL+ARh* z_1jqOgmprK^<){=Ueaa21qkI;LcKq9bCEkgpgP`IPK@E~BPFmhVEYB5bJXn%F#+1k zMKKP!$@L%PdE}PfiHEX`7V?jgOJh*z!ui045hJjd_#dc-0fdp-=nQyj6-F{+5Uvf> z1^`A1*d3tODyWV}ayLjE#hpNnU_Pvui|kJDcmVo{4%9H1LtzHO902Bnnw!^ON-*rW zr3CIMAsY%+2=^!{?T9}Ne;8p6BlPwLEHBIQe`a|6kr%~YG&h5s0Cwk-4}1(i|FD31 ztqd{(pZ+6vq|keVAO|!0>Yc+DLml%_|BV}^Hwfx5L2V`&v#=P0joQKj70L4`EjpyJLb%zW#t^Kh zs44jfJ)eV&0;|3GQWB%r`soKd)mI|CH)yO7WGbka z^%^)Xa23+aV<5NQMvighaRh8Ghc#x90}#dr4U@rADy)t2^qm+3$SfqH=qa+?leHK88j z9+VXI?hEJtcVB>`SI-$3kplo_R2bbfT)jM)IM)A4A;{MtOm0YG3js!ufiNG!d;|*#7>ynh=;~k&l46j+>oybzz*04_shJwt z5nmc0DkNZzfQ1aYkAWcxRssxFMr;Wbs{?2e5+KX5m7v6ALq>51F@{47N5T19g+UqY zqmvA$!Sp+Z_b7byXu;)km^!=;fVnt_F`ew_#^rOO9Drsk<7bA?467K{ftVw4110}5_J3G6e&7Y3O6XaAoQbRH>bpbix8 z=njCT1(;1hejSwEL`>ieu0|<2(TSA($Vv@EDg>I?7}yvdKubJb2JJ>+5+N)#!%{as(?OOKlloz1kW&&iaWZf)TxPh6;to=MPL>1kg#@WShdF?( l-Z8GIASLK2asUGr0RXwvpu#N^YF7XN002ovPDHLkV1k0^(b)h1 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..31c3476491424f5e83458c2f0026c4bad7b3d737 GIT binary patch literal 30999 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 TMwDIW00000NkvXXu0mjfO0z!#5zr7cx zkU%bDu&HrpbY@@!LT(0b238;hv9YNmWDZOZ%zYY0Y78lLi5Nz`Zh8e5!yAB5Rj@qI zh-MPJEC2U@TB=~>h z+${zhA8W93SfJftxP@0c&N7f47{d{aAq?tTY7C&50U3_ZB=nd8sSRfI2Wt^z5M=ld zgxF$tPSbo8W6{ePkR+_k1EsuQXjurCnd}Vg46u}iD<#6*C&?he@SWiYiaSK>#K3$} z1~G=iO-FEg9cKUnWA;BNP@Y_S3QpFrn1PvuoU94N@c;J=?-|S)OySBdHeF(n0y-Kb z04iTV^g@QEV0nBg8I)B)DG#JDo*^2=T9BC_flmyd7}Ob58E!J%#+8%Mo%{be!wUv< z^I(B?lHoL1JvRdv<5#p|3nYOOfNa3>1U-h4ZA11r10Db?OWC326uMIVIz6}$dPHDL z$uPyZ$~urdvYE&!246}9rFL9q+Sb{Ct+i#aVd!b+W#9de2o@>Yz(j%hNTz{ z1~s@QWJiJw1xW}q2r<58c!w=G|NjJ5`9Go6KC(HWK!cSnuo%W;4!YTlK(!3O6bnva zB@BgNdE|&dwhLcM1O?z1V9@66M9Xx@}j|>6Qk zWgsIkzcT<+1~`U6DFhbF$R2+^`wip0=lAj14X;gQ8Dtpzt4zQXqPWarpb+47ClT(U$okPCeF|d_0KxMa Udi)wJQ2+n{07*qoM6N<$g3^bG?f?J) literal 0 HcmV?d00001 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..08da02e86625807d33d4b99b5ed1988e13a61089 GIT binary patch literal 7333 zcmeHL3s6&68oqguM7(K&Sh(SlKvM;^v=@kNmVyZnlc?oc>r!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 0 HcmV?d00001 diff --git a/src/main/resources/player/armor/examplearms_right.png b/src/main/resources/player/armor/examplearms_right.png new file mode 100644 index 0000000000000000000000000000000000000000..ba2aca1e0bb79b5be0ad93b9bed573f190509aef GIT binary patch literal 7328 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/player/armor/examplechest.png b/src/main/resources/player/armor/examplechest.png new file mode 100644 index 0000000000000000000000000000000000000000..bd8d7373c9d50282ba1be2bf0045e70e6f6c1d3c GIT binary patch literal 8431 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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..bded8d4006e5bc1d8820dcfc7d530fb56e12ecaf GIT binary patch literal 7592 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 AiU0rr 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/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 z7UeZN7w4M~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 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/tiles/examplegrasstile_splat.png b/src/main/resources/tiles/examplegrasstile_splat.png new file mode 100644 index 0000000000000000000000000000000000000000..11981eac9661a7e97c547b46872347624cde8796 GIT binary patch literal 11459 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 literal 0 HcmV?d00001 From a0813ef9253f57b7a0dd4eeaebff39af472245b5 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Fri, 20 Feb 2026 21:23:16 +0000 Subject: [PATCH 3/7] Add example crafting station and tech Introduce a custom crafting station and corresponding tech to support mod recipes. Adds ExampleModTech (registers EXAMPLE_TECH), new ExampleCraftingStationObject (crafting station implementation, texture loading and draw/preview stubs, returns EXAMPLE_TECH from getCraftingTechs), and registers the object in ExampleModObjects. Wire tech loading into ExampleMod.load() and update ExampleModRecipes to use ExampleModTech.EXAMPLE_TECH instead of the default WORKSTATION. This enables recipes to require the new crafting station. --- src/main/java/examplemod/ExampleMod.java | 3 + .../examplemod/Loaders/ExampleModObjects.java | 3 + .../examplemod/Loaders/ExampleModRecipes.java | 2 +- .../examplemod/Loaders/ExampleModTech.java | 19 +++++ .../objects/ExampleCraftingStationObject.java | 83 +++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/main/java/examplemod/Loaders/ExampleModTech.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index 920d719..a8a4b72 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -27,6 +27,9 @@ public void init() { System.out.println("Hello world from my example mod!"); settings.logLoadedSettings(); // log the loaded settings for debug + // Register Tech Trees + ExampleModTech.load(); + // Register categories first: Used by Items/Objects to appear correctly in Creative/crafting trees ExampleModCategories.load(); diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index 830fcc3..c26b631 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -62,6 +62,9 @@ public static void load(){ WallObject exampleWall = (WallObject) ObjectRegistry.getObject("examplewall"); ObjectRegistry.registerObject("examplewalltrap",new ExampleWallTrapObject(exampleWall),1,true); + // Register a Crafting Station + ObjectRegistry.registerObject("examplecraftingstation", new ExampleCraftingStationObject(),1,true); + } diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index deab3f5..3593e2d 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -118,7 +118,7 @@ public static void registerRecipes(){ Recipes.registerModRecipe(new Recipe( "exampleobject", 1, - RecipeTechRegistry.WORKSTATION, + ExampleModTech.EXAMPLE_TECH, new Ingredient[]{ new Ingredient("examplestone", 7), new Ingredient("exampleitem", 3) diff --git a/src/main/java/examplemod/Loaders/ExampleModTech.java b/src/main/java/examplemod/Loaders/ExampleModTech.java new file mode 100644 index 0000000..d2278e4 --- /dev/null +++ b/src/main/java/examplemod/Loaders/ExampleModTech.java @@ -0,0 +1,19 @@ +package examplemod.Loaders; + +import examplemod.examples.buffs.ExampleArmorSetBuff; +import examplemod.examples.buffs.ExampleArrowBuff; +import examplemod.examples.buffs.ExampleBuff; +import examplemod.examples.buffs.ExampleTrinketBuff; +import necesse.engine.registries.BuffRegistry; +import necesse.engine.registries.RecipeTechRegistry; +import necesse.inventory.recipe.Tech; + +public class ExampleModTech { + public static Tech EXAMPLE_TECH; + + public static void load(){ + // stringID: how recipes refer to it internally + // itemStringID: used for icon/tooltips (usually your crafting station item id) + EXAMPLE_TECH = RecipeTechRegistry.registerTech("exampletech", "examplecraftingstation"); + } +} diff --git a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java new file mode 100644 index 0000000..e5acad6 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java @@ -0,0 +1,83 @@ +package examplemod.examples.objects; + +import examplemod.Loaders.ExampleModTech; +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.inventory.recipe.Tech; +import necesse.level.gameObject.container.CraftingStationObject; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +import java.util.List; + +//TODO add rendering code +public class ExampleCraftingStationObject extends CraftingStationObject { + // Loaded once from mod resources in loadTextures() + private GameTexture texture; + + public ExampleCraftingStationObject() { + super(); + // Optional: set categories like vanilla crafting stations do + //setItemCategory("objects", "craftingstations"); + //setCraftingCategory("craftingstations"); + } + + @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 Tech[] getCraftingTechs() { + // Assign what tech/s the station will use + return new Tech[] { ExampleModTech.EXAMPLE_TECH }; + } +} \ No newline at end of file From 7ffabe6d8b8134fde3c661d604a23997a0675f43 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Sat, 21 Feb 2026 03:40:46 +0000 Subject: [PATCH 4/7] Add duo workstation multi-tile object Introduce a 1x2 duo workstation as two objects: ExampleWorkstationDuoObject (master) and ExampleWorkstationDuo2Object (partner). Both classes implement drawing, preview, placement options, multitile linking via counterID, and texture loading; register() registers and links the pair. Register the duo in ExampleModObjects, add localization entry (exampleworkstationduo) and provide the texture asset objects/exampleworkstationduo.png. Also minor tweaks to ExampleCraftingStationObject (comments/TODO) and added crafting station localization. --- .../examplemod/Loaders/ExampleModObjects.java | 2 + .../objects/ExampleCraftingStationObject.java | 5 +- .../objects/ExampleWorkstationDuo2Object.java | 136 ++++++++++ .../objects/ExampleWorkstationDuoObject.java | 243 ++++++++++++++++++ src/main/resources/locale/en.lang | 2 + .../objects/exampleworkstationduo.png | Bin 0 -> 1382 bytes 6 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java create mode 100644 src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java create mode 100644 src/main/resources/objects/exampleworkstationduo.png diff --git a/src/main/java/examplemod/Loaders/ExampleModObjects.java b/src/main/java/examplemod/Loaders/ExampleModObjects.java index c26b631..cd6bc69 100644 --- a/src/main/java/examplemod/Loaders/ExampleModObjects.java +++ b/src/main/java/examplemod/Loaders/ExampleModObjects.java @@ -65,6 +65,8 @@ public static void load(){ // Register a Crafting Station ObjectRegistry.registerObject("examplecraftingstation", new ExampleCraftingStationObject(),1,true); + // Register Workstation Duo + ExampleWorkstationDuoObject.register(); } diff --git a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java index e5acad6..e4ee4d5 100644 --- a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java @@ -13,13 +13,14 @@ import necesse.level.maps.Level; import necesse.level.maps.light.GameLight; +//TODO figure out item art + import java.util.List; -//TODO add rendering code public class ExampleCraftingStationObject extends CraftingStationObject { // Loaded once from mod resources in loadTextures() private GameTexture texture; - + public ExampleCraftingStationObject() { super(); // Optional: set categories like vanilla crafting stations do diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java new file mode 100644 index 0000000..0f13e45 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java @@ -0,0 +1,136 @@ +package examplemod.examples.objects; + +import necesse.gfx.gameTexture.GameTexture; +import necesse.level.gameObject.container.CraftingStationObject; +import necesse.level.maps.multiTile.MultiTile; +import necesse.level.maps.multiTile.SideMultiTile; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptionsList; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; +import java.util.List; +import java.awt.Color; +import java.awt.Rectangle; + +public class ExampleWorkstationDuo2Object extends CraftingStationObject { + public GameTexture texture; + + // This stores the OTHER object id (the main/masterpiece) + protected int counterID; + + public ExampleWorkstationDuo2Object() { + super(new Rectangle(32, 32)); + this.mapColor = new Color(132, 91, 25); + this.isLightTransparent = true; + this.hoverHitbox = new Rectangle(0, -16, 32, 48); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, + TickManager tickManager, GameCamera camera, PlayerMob perspective) { + + // How bright to draw this tile (depends on time of day / light sources) + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile position -> screen position + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Which way the object is facing (0-3) + int rotation = level.getObjectRotation(tileX, tileY); + + // Store draw calls here, then draw them at the end + final DrawOptionsList options = new DrawOptionsList(); + + // This is the "partner" half of the duo workstation. + if (rotation == 0) { + // Rotation 0: draw 2 sprites stacked (top + bottom) + options.add(this.texture.initDraw().sprite(0, 0, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) // cracks if damaged + .light(light) // apply lighting + .pos(drawX, drawY - 32)); // top piece (1 tile above) + + options.add(this.texture.initDraw().sprite(0, 1, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); // bottom piece (on the tile) + + } else if (rotation == 1) { + // Rotation 1: draw 2 sprites stacked (top + bottom) + options.add(this.texture.initDraw().sprite(1, 5, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); // top + + options.add(this.texture.initDraw().sprite(1, 6, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); // bottom + + // Optional extra: animated flame overlay. + // We aren't using this here ? what uses this ? + // We pick a different flame sprite every 300 ticks: + // worldTime % 1200 gives a loop, / 300 makes it 0..3 (4 frames). + //int flameSprite = (int) (level.getWorldEntity().getWorldTime() % 1200L / 300L); + + // flameSprite % 2 picks column 0 or 1 + // 7 + flameSprite / 2 picks row 7 or 8 + // (So the 4 frames are arranged in a 2x2 block starting at (0,7)) + + //options.add(this.texture.initDraw().sprite(flameSprite % 2, 7 + flameSprite / 2, 32) + //.addObjectDamageOverlay(this, level, tileX, tileY) + //.light(light) + //.pos(drawX, drawY)); // flame sits on the bottom tile + + } else if (rotation == 2) { + // Rotation 2: only 1 sprite needed for this half + options.add(this.texture.initDraw().sprite(1, 2, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); + + } else { + // Rotation 3: draw 2 sprites stacked (top + bottom) + options.add(this.texture.initDraw().sprite(0, 3, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); // top + + options.add(this.texture.initDraw().sprite(0, 4, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); // bottom + } + + // Tell the engine "here is how to draw this object tile" + // LevelSortedDrawable makes sure it sorts correctly with other objects/entities. + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + return 16; // normal sorting height (middle of the tile) + } + + @Override + public void draw(TickManager tickManager) { + options.draw(); // run all the draw calls we added above + } + }); + } + + @Override + public void loadTextures() { + // Usually both pieces share the same texture sheet + this.texture = GameTexture.fromFile("objects/exampleworkstationduo"); + } + + @Override + public MultiTile getMultiTile(int rotation) { + // Same 1x2 multi-tile, but THIS is NOT the masterpiece (false) + return new SideMultiTile(0, 0, 1, 2, rotation, false, getID(), this.counterID); + } +} \ No newline at end of file diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java new file mode 100644 index 0000000..eaaf4a2 --- /dev/null +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java @@ -0,0 +1,243 @@ +package examplemod.examples.objects; + +import java.util.ArrayList; +import java.util.List; +import java.awt.Color; +import java.awt.Rectangle; +import necesse.engine.localization.message.GameMessage; +import necesse.engine.localization.message.LocalMessage; +import necesse.engine.registries.ObjectRegistry; +import necesse.engine.registries.RecipeTechRegistry; +import necesse.gfx.gameTexture.GameTexture; +import necesse.inventory.recipe.Tech; +import necesse.level.gameObject.ObjectPlaceOption; +import necesse.level.gameObject.container.CraftingStationObject; +import necesse.level.maps.multiTile.MultiTile; +import necesse.level.maps.multiTile.SideMultiTile; +import necesse.engine.gameLoop.tickManager.TickManager; +import necesse.entity.mobs.PlayerMob; +import necesse.gfx.camera.GameCamera; +import necesse.gfx.drawOptions.DrawOptionsList; +import necesse.gfx.drawables.LevelSortedDrawable; +import necesse.gfx.drawables.OrderableDrawables; +import necesse.level.maps.Level; +import necesse.level.maps.light.GameLight; + +//TODO figure out item art + +public class ExampleWorkstationDuoObject extends CraftingStationObject { + public GameTexture texture; + + // This stores the OTHER object id (the partner piece) + protected int counterID; + + public ExampleWorkstationDuoObject() { + super(new Rectangle(32, 32)); + this.mapColor = new Color(132, 91, 25); + this.isLightTransparent = true; + + // Optional: gives a nicer hover area (same idea as vanilla duo) + this.hoverHitbox = new Rectangle(0, -16, 32, 48); + } + + @Override + public void addDrawables(List list, OrderableDrawables tileList, + Level level, int tileX, int tileY, + TickManager tickManager, GameCamera camera, PlayerMob perspective) { + + // Light affects brightness (day/night, torches, etc.) + GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coordinates to screen draw coordinates + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + // Rotation is 0-3 This decides which sprites to draw. + int rotation = level.getObjectRotation(tileX, tileY); + + // Build a list of draw calls to be called at the end + final DrawOptionsList options = new DrawOptionsList(); + + /* + * This object is part of a 1x2 object + * Depending on rotation, the visible shape changes: + * Some rotations draw only 1 sprite + * Some rotations draw 2 sprites stacked vertically (top + bottom) + * + * sprite(x, y, 32) means: + * take the 32x32 sprite at grid position (x, y) from the texture sheet + */ + + if (rotation == 0) { + // Rotation 0: this tile only needs 1 sprite + options.add(this.texture.initDraw().sprite(0, 2, 32) + // Adds cracks/visual damage if the object is damaged + .addObjectDamageOverlay(this, level, tileX, tileY) + // Apply lighting + .light(light) + // Draw at the tile position + .pos(drawX, drawY)); + + } else if (rotation == 1) { + // Rotation 1: draw 2 sprites (top part above, bottom part on the tile) + options.add(this.texture.initDraw().sprite(0, 5, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + // Top sprite sits 1 tile (32px) above + .pos(drawX, drawY - 32)); + + options.add(this.texture.initDraw().sprite(0, 6, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + // Bottom sprite sits on the tile + .pos(drawX, drawY)); + + } else if (rotation == 2) { + // Rotation 2: also 2 sprites (top + bottom), but from different sheet coords + options.add(this.texture.initDraw().sprite(1, 0, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + + options.add(this.texture.initDraw().sprite(1, 1, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); + + } else { + // Rotation 3: also 2 sprites (top + bottom), again different sheet coords + options.add(this.texture.initDraw().sprite(1, 3, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY - 32)); + + options.add(this.texture.initDraw().sprite(1, 4, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); + } + + /* + * Necesse draws objects using LevelSortedDrawable so they sort correctly + * in front or behind other things. + * + * We add ONE drawable entry for this tile, and inside it, we draw our options list. + */ + list.add(new LevelSortedDrawable(this, tileX, tileY) { + + @Override + public int getSortY() { + // Sort position within the tile. + // 16 = middle of tile because 1 tile = 32 + return 16; + } + + @Override + public void draw(TickManager tickManager) { + // Actually draw everything we queued up above + options.draw(); + } + }); + } + + @Override + public void drawPreview(Level level, int tileX, int tileY, int rotation, + float alpha, PlayerMob player, GameCamera camera) { + + // Preview uses camera coords too + int drawX = camera.getTileDrawX(tileX); + int drawY = camera.getTileDrawY(tileY); + + /* + * drawPreview is shown when the player is holding the object before placing it. + * For a DUO workstation we draw multiple sprites so the player sees + * the full shape that will be placed (both tiles, not just the clicked one). + * + * alpha controls transparency of the preview (ghost placement) + */ + + if (rotation == 0) { + // Draw this main tile sprite + this.texture.initDraw().sprite(0, 2, 32).alpha(alpha).draw(drawX, drawY); + + // Draw the partner tile pieces above it (2 tiles tall) + this.texture.initDraw().sprite(0, 0, 32).alpha(alpha).draw(drawX, drawY - 64); + this.texture.initDraw().sprite(0, 1, 32).alpha(alpha).draw(drawX, drawY - 32); + + } else if (rotation == 1) { + // Draw this tile (2-high) + this.texture.initDraw().sprite(0, 5, 32).alpha(alpha).draw(drawX, drawY - 32); + this.texture.initDraw().sprite(0, 6, 32).alpha(alpha).draw(drawX, drawY); + + // Draw the partner tile to the RIGHT (x + 32) + this.texture.initDraw().sprite(1, 5, 32).alpha(alpha).draw(drawX + 32, drawY - 32); + this.texture.initDraw().sprite(1, 6, 32).alpha(alpha).draw(drawX + 32, drawY); + + } else if (rotation == 2) { + // Draw this tile (2-high) + this.texture.initDraw().sprite(1, 0, 32).alpha(alpha).draw(drawX, drawY - 32); + this.texture.initDraw().sprite(1, 1, 32).alpha(alpha).draw(drawX, drawY); + + // Draw the partner tile below (y + 32) + this.texture.initDraw().sprite(1, 2, 32).alpha(alpha).draw(drawX, drawY + 32); + + } else { + // Draw this tile (2-high) + this.texture.initDraw().sprite(1, 3, 32).alpha(alpha).draw(drawX, drawY - 32); + this.texture.initDraw().sprite(1, 4, 32).alpha(alpha).draw(drawX, drawY); + + // Draw the partner tile to the LEFT (x - 32) + this.texture.initDraw().sprite(0, 3, 32).alpha(alpha).draw(drawX - 32, drawY - 32); + this.texture.initDraw().sprite(0, 4, 32).alpha(alpha).draw(drawX - 32, drawY); + } + } + + @Override + public ArrayList getPlaceOptions(Level level, int levelX, int levelY, + PlayerMob playerMob, int playerDir, + boolean offsetMultiTile) { + // duo workstations shift the direction by -1 + // so the placed "rotation" matches how the sprites are laid out. + int fixedDir = Math.floorMod(playerDir - 1, 4); + return super.getPlaceOptions(level, levelX, levelY, playerMob, fixedDir, offsetMultiTile); + } + + @Override + public GameMessage getNewLocalization() { + return new LocalMessage("object", "exampleworkstationduo"); + } + + @Override + public void loadTextures() { + // objects/exampleworkstationduo.png + this.texture = GameTexture.fromFile("objects/exampleworkstationduo"); + } + + @Override + public MultiTile getMultiTile(int rotation) { + // 1x2 “side” multi-tile, THIS is the masterpiece (true) + // The ids array order matches the two tiles in the multi-tile. + return new SideMultiTile(0, 1, 1, 2, rotation, true, this.counterID, getID()); + } + + @Override + public Tech[] getCraftingTechs() { + // Use whatever techs you want. This is just an example. + return new Tech[] { RecipeTechRegistry.WORKSTATION }; + } + + // Call this from your mod init to register BOTH pieces + public static int[] register() { + ExampleWorkstationDuoObject main = new ExampleWorkstationDuoObject(); + ExampleWorkstationDuo2Object part = new ExampleWorkstationDuo2Object(); + + int mainID = ObjectRegistry.registerObject("exampleworkstationduo", main, 10.0F, true); + int partID = ObjectRegistry.registerObject("exampleworkstationduo2", part, 0.0F, false); + + // Link them together (this is the key) + main.counterID = partID; + part.counterID = mainID; + + return new int[] { mainID, partID }; + } +} \ No newline at end of file diff --git a/src/main/resources/locale/en.lang b/src/main/resources/locale/en.lang index 17f4755..35b8073 100644 --- a/src/main/resources/locale/en.lang +++ b/src/main/resources/locale/en.lang @@ -17,6 +17,8 @@ examplejobobject=Example Job Object exampleconfigobject=Example Config Object examplepressureplate=Example Pressure Plate examplewalltrap=Example Wall Trap +examplecraftingstation=Example Crafting Station +exampleworkstationduo=Example Workstation Duo [item] exampleitem=Example Item diff --git a/src/main/resources/objects/exampleworkstationduo.png b/src/main/resources/objects/exampleworkstationduo.png new file mode 100644 index 0000000000000000000000000000000000000000..98e6f83ae55a542d8467466e44737eb67234d16a GIT binary patch literal 1382 zcmeAS@N?(olHy`uVBq!ia0vp^4nX{XgAGV-E=@21QjEnx?oNz1PwLbIIV|apzK#qG z8~eHcB(ehe3dtTpz6=aiY77hwEes65fI<>&pI$Nt9Wh&7rus8ffV% zPZ!6KiaBrRobFp}CF1J4ddjcEDU)hqvnBy)r~U~-T+JK%BkeX!FIl>Joy|7!<7NW0 z-xcoq&iLfhu8HyT{}$fA#~|duzF;DQ6+^EzWB=v5Uq0{ivQMz-jbvZrJFVfnTXpt_ z8E?2g-22dYHTl5P(%Zg|oeL~DqLdj+7^FBCFkkRskZnlY+F(1Exn%*@1cq{^HU3At zSud2AdTZ{Jci`z-89Je;po=viB6Qwv!vj*B8kL*BGa9gjHn4znF5^~Q@S6F>>eGkA z8(1zdX55M4KH|5_>$re8)3bH|yx#Mh+-IF8^~2(Bx8KQb)+=+41@-53vP_DOsmJ6}d72-v>Mn-k%-IcqD11RCE4E6AL@`6}m@vSOmVjp%@ZTZ=~@Rmuv zoI(3hTc+(yYtj}TE@v!eytj^nKb|o%+2M`Gb-Q1tcGkbAFDy(he$VxkFJ&Y1hG}mi z6KuGI|K3XyGuW&8Y~o@5raab&*!&%$2}i|ipStxcx36z_Q{7bUC<+X7%RLO?<}O)( z#2$J%ykW}7)S55+N(`~nuVDc5T%*9IXAyI;)HLNC;a zujq|t3=6%mrXk{I^yM`TYbO8`+1Vux>t6E}>n-3~z^o%U`_-DX&S%@t9(`85T60q+ zqv8$IvaXxY*{A#|oB!+Hja!R~cjRq)r(wl*WyiNX!OxBbtAiu7XU%8LGS+T*#a4U2 zecfxbpX-it-S{1maEo=%*Z%@StZ#O5P0~*AY&yPJ{?I8sfyocHepF29VNB+{x7+R7 z)&mhu*B8q>9XvYWx!eB5#*V;b5A^M_8=sctOlI2V0t^PjSvzI!Sz`^0S%^`cmwc!2l{{Pb?w(VY5>92dum&QyO=6wu@>xO4-+D4W>D#b&_qinvvt}y>|9U=6=pV<4%95vdCdXa-V}0C4Hf{ek_ L)z4*}Q$iB}VN6T; literal 0 HcmV?d00001 From 0d5c33508aa7555e89de66625dcbb9d1abde07f0 Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Sat, 21 Feb 2026 19:17:44 +0000 Subject: [PATCH 5/7] Add collisions and update crafting station visuals Add per-rotation collision hitboxes for ExampleCraftingStationObject, ExampleWorkstationDuoObject, and ExampleWorkstationDuo2Object so objects block movement with pixel-accurate rectangles. Update ExampleCraftingStationObject rendering to use the new examplecraftingstation texture, support 4 rotations, draw tall sprites using texture height, include damage overlay, and sort drawables properly; also adjust preview positioning. Re-enable flame sprite drawing in ExampleWorkstationDuo2Object and tidy draw code. Swap crafting tech from RecipeTechRegistry.WORKSTATION to ExampleModTech.EXAMPLE_TECH and add the necessary import. Add new item and object PNG assets (examplecraftingstation, exampleworkstationduo) and update the existing exampleworkstationduo object texture. --- .../objects/ExampleCraftingStationObject.java | 56 +++++++++++------- .../objects/ExampleWorkstationDuo2Object.java | 39 ++++++++++-- .../objects/ExampleWorkstationDuoObject.java | 36 ++++++++++- .../items/examplecraftingstation.png | Bin 0 -> 446 bytes .../resources/items/exampleworkstationduo.png | Bin 0 -> 574 bytes .../objects/examplecraftingstation.png | Bin 0 -> 1021 bytes .../objects/exampleworkstationduo.png | Bin 1382 -> 2146 bytes 7 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/items/examplecraftingstation.png create mode 100644 src/main/resources/items/exampleworkstationduo.png create mode 100644 src/main/resources/objects/examplecraftingstation.png diff --git a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java index e4ee4d5..41f789f 100644 --- a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java @@ -15,6 +15,7 @@ //TODO figure out item art +import java.awt.*; import java.util.List; public class ExampleCraftingStationObject extends CraftingStationObject { @@ -34,46 +35,59 @@ public void loadTextures() { // Loads: src/main/resources/objects/exampleleveleventobject.png // (no ".png" in the string) - this.texture = GameTexture.fromFile("objects/examplejobobject"); + this.texture = GameTexture.fromFile("objects/examplecraftingstation"); + } + + @Override + public Rectangle getCollision(Level level, int x, int y, int rotation) { + if (rotation % 2 == 0) + return new Rectangle(x * 32 + 8, y * 32 + 8, 16, 20); + return new Rectangle(x * 32 + 5, y * 32 + 14, 22, 16); } @Override public void addDrawables(List list, OrderableDrawables tileList, - Level level, int tileX, int tileY, TickManager tickManager, - GameCamera camera, PlayerMob perspective) { + 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); + // 0-3 direction index (same as vanilla) + int rotation = level.getObjectRotation(tileX, tileY) % 4; - /* - */ - tileList.add(tm -> opts.draw()); + // 4 rotations across, tall sprite (32 wide, texture height tall) + final TextureDrawOptionsEnd options = this.texture.initDraw() + .sprite(rotation, 0, 32, this.texture.getHeight()) + .light(light) + .addObjectDamageOverlay(this, level, tileX, tileY) + .pos(drawX, drawY - this.texture.getHeight() + 32); + + list.add(new LevelSortedDrawable(this, tileX, tileY) { + @Override + public int getSortY() { + return 16; + } + + @Override + public void draw(TickManager tickManager) { + options.draw(); + } + }); } @Override - public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, - PlayerMob player, GameCamera camera) { + 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) + .sprite(rotation % 4, 0, 32, this.texture.getHeight()) .alpha(alpha) - .draw(drawX, drawY); + .draw(drawX, drawY - this.texture.getHeight() + 32); } @Override diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java index 0f13e45..5bd11db 100644 --- a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuo2Object.java @@ -28,6 +28,35 @@ public ExampleWorkstationDuo2Object() { this.isLightTransparent = true; this.hoverHitbox = new Rectangle(0, -16, 32, 48); } + @Override + public Rectangle getCollision(Level level, int x, int y, int rotation) { + + // Collision is in pixels, not tiles. + // Each tile is 32x32 pixels. + // + // We return a rectangle that blocks movement for this object. + // Different rotations can have different shapes, so we pick a different + // rectangle depending on which way the object is facing. + + if (rotation == 0) + // Rotation 0: inset a bit from the tile edges (4px in from left/top), + // and make it mostly tall (24x28) so it doesn't fill the whole tile. + return new Rectangle(x * 32 + 4, y * 32 + 4, 24, 28); + + if (rotation == 1) + // Rotation 1: shape is wider / shorter. + // Starts at left edge, 6px down, size 26x20. + return new Rectangle(x * 32, y * 32 + 6, 26, 20); + + if (rotation == 2) + // Rotation 2: similar to rotation 0 but shifted upward a bit. + // Starts 4px in from left, at the top edge, size 24x26. + return new Rectangle(x * 32 + 4, y * 32, 24, 26); + + // Rotation 3 (default): same idea as rotation 1 but shifted right a bit. + // Starts 6px in from left and 6px down, size 26x20. + return new Rectangle(x * 32 + 6, y * 32 + 6, 26, 20); + } @Override public void addDrawables(List list, OrderableDrawables tileList, @@ -76,16 +105,16 @@ public void addDrawables(List list, OrderableDrawables tile // We aren't using this here ? what uses this ? // We pick a different flame sprite every 300 ticks: // worldTime % 1200 gives a loop, / 300 makes it 0..3 (4 frames). - //int flameSprite = (int) (level.getWorldEntity().getWorldTime() % 1200L / 300L); + int flameSprite = (int) (level.getWorldEntity().getWorldTime() % 1200L / 300L); // flameSprite % 2 picks column 0 or 1 // 7 + flameSprite / 2 picks row 7 or 8 // (So the 4 frames are arranged in a 2x2 block starting at (0,7)) - //options.add(this.texture.initDraw().sprite(flameSprite % 2, 7 + flameSprite / 2, 32) - //.addObjectDamageOverlay(this, level, tileX, tileY) - //.light(light) - //.pos(drawX, drawY)); // flame sits on the bottom tile + options.add(this.texture.initDraw().sprite(flameSprite % 2, 7 + flameSprite / 2, 32) + .addObjectDamageOverlay(this, level, tileX, tileY) + .light(light) + .pos(drawX, drawY)); // flame sits on the bottom tile } else if (rotation == 2) { // Rotation 2: only 1 sprite needed for this half diff --git a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java index eaaf4a2..e21682f 100644 --- a/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleWorkstationDuoObject.java @@ -4,10 +4,11 @@ import java.util.List; import java.awt.Color; import java.awt.Rectangle; + +import examplemod.Loaders.ExampleModTech; import necesse.engine.localization.message.GameMessage; import necesse.engine.localization.message.LocalMessage; import necesse.engine.registries.ObjectRegistry; -import necesse.engine.registries.RecipeTechRegistry; import necesse.gfx.gameTexture.GameTexture; import necesse.inventory.recipe.Tech; import necesse.level.gameObject.ObjectPlaceOption; @@ -40,6 +41,37 @@ public ExampleWorkstationDuoObject() { this.hoverHitbox = new Rectangle(0, -16, 32, 48); } + @Override + public Rectangle getCollision(Level level, int x, int y, int rotation) { + + // This returns the "solid" hitbox for the object (what blocks movement). + // Coordinates are in pixels: + // - x,y are tile coords + // - each tile is 32x32 pixels + // + // The object can be rotated (0-3), and the collision box changes slightly + // depending on which way it's facing. + + if (rotation == 0) + // Rotation 0: a taller box that starts at the top of the tile. + // 4px inset from the left, 24px wide, 26px tall. + return new Rectangle(x * 32 + 4, y * 32, 24, 26); + + if (rotation == 1) + // Rotation 1: a shorter/wider box, shifted in from left and down. + // Starts 6px in and 6px down, 26px wide, 20px tall. + return new Rectangle(x * 32 + 6, y * 32 + 6, 26, 20); + + if (rotation == 2) + // Rotation 2: the tallest version (almost fills the tile vertically). + // Starts 4px in and 4px down, 24px wide, 28px tall. + return new Rectangle(x * 32 + 4, y * 32 + 4, 24, 28); + + // Rotation 3 (default): similar size to rotation 1 but shifted left a bit. + // Starts at the left edge, 6px down, 26px wide, 20px tall. + return new Rectangle(x * 32, y * 32 + 6, 26, 20); + } + @Override public void addDrawables(List list, OrderableDrawables tileList, Level level, int tileX, int tileY, @@ -223,7 +255,7 @@ public MultiTile getMultiTile(int rotation) { @Override public Tech[] getCraftingTechs() { // Use whatever techs you want. This is just an example. - return new Tech[] { RecipeTechRegistry.WORKSTATION }; + return new Tech[] { ExampleModTech.EXAMPLE_TECH }; } // Call this from your mod init to register BOTH pieces diff --git a/src/main/resources/items/examplecraftingstation.png b/src/main/resources/items/examplecraftingstation.png new file mode 100644 index 0000000000000000000000000000000000000000..d607d8d137985237b482c22ec9320c123466a9bd 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-GyN#0~^EQ^J*t5fFhg)9+AZi4BWyX%*Zfnjs#GUy~NYkmHicys4$Oo+@8MB1 zx;TbJw7#9{$#uv;!fn2=7^|zm8%do-;s$yyHUGRV-Nc`90-ri%KZf|Z+ zQPVv6eb$rq88hnD6RkGPTl@3-ui}+88NYvRU|O(Wy5-zdlh^~>JziT~Ir7%)_1>9j zKMJxB$uVa=%38>EAj6WY#^9ch-7bb|9*)@c%m)ezz1B(nF!?4i;qFb@Z#4|p3z?a{ zX9hiZDe7PC;=_4K&&Eh{C*xb+74x{Rp4jZOJ@~+(>IaFs6<1qS#MZ23J$5xfK-o%o z*~!)uycw$(cBxF4dMG;ObDiDuvqj1u&Kd3D;@Y3=FysC61EEf=vAg^38!G(2P#Pnz jr{=T&vf$3f88ejsM8|BY7k6y}1`va%tDnm{r-UW|o)4yv literal 0 HcmV?d00001 diff --git a/src/main/resources/items/exampleworkstationduo.png b/src/main/resources/items/exampleworkstationduo.png new file mode 100644 index 0000000000000000000000000000000000000000..22a2c0f435129de508cfddf25ed673a9343922f2 GIT binary patch literal 574 zcmV-E0>S->P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;$G(1^}baCdhaI000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2kHV96Ame)UCu%P00DwYL_t(oh3%JJOTu6nhA+((Iz^2#wa|_-qLb(#BoNP{ zf}md-^b10a=sct>B7<}qB&H=IXb1JSHn)Ye*dp(f@odk>d*AQZGx+cD*q!h&elRq8 zf2156*7DjtfN|loE0JKnyR-wqRjUj@CcGkChA^sx`xG#nLxFTK1wf^9Z<+WDb4xg` zz~_7j=GoYmp=0&620%WM1K^}q03aDiX#DzvzG1xNl>j(voCE4wa9jb?T_8&`0EJo+ zfX%rL0LravKxO{aYXYi5(dh^PG5e^&lCWwkqn?J5R!r#|8~ElvFqKsVWGdX2K3DJ&@b|z|>NSp`aDX<;STE;o9z(|)O?u591!PY{`$L?@{KOhjE4r}bY zc4e%@%b~Q!R!q(Z_bQ-AZ_xXd0XVLJ>3@N~H#F<(e;V(&0uxN|27)zhFq#Yup#T5? M07*qoM6N<$f>-S3ga7~l literal 0 HcmV?d00001 diff --git a/src/main/resources/objects/examplecraftingstation.png b/src/main/resources/objects/examplecraftingstation.png new file mode 100644 index 0000000000000000000000000000000000000000..b6d4d8fb20e07ad0e404e471e2a8d56b7e8019de GIT binary patch literal 1021 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 delta 1241 zcmaDP@QiE1+XPdX1^=k^_}s_r(F}{<^L_be~&@PfqlV51}lbMYsUV| zcfWkz=VhN@(;LaY#&=r7cem>74>R6yeYp3b@oMscr=_=jA3N6;8GY=Qp{}I!)?_ z#ocbdlijRW<{S&^&#UibnLPh^`~UA1g&`(vK5O|tswuAKJ|ea2=&i=nuhX=4zKl!| zuzi;|O=y;wfKyD9Xy`J>*LeZOIVXiSlq3Z;t8(tVKRcH3NYYBF=KPN)7Iy3_bdT<` zp0QTs?j|Xn{ik!pKJ2R6^0Ry4Et7gVgZ87gOxu~(q%AyL&REQNZyg7JeLQ1ivcnsV z>vq3P?W})KUs#x2{GRJ6U&==24b$F4CfINZ|Gk$aX0TWF*~G*AO?j*lvH3eh6OM}4 zK6UF?ZeQQLmU|e&&0Vtoh&}Xjc*B&DsWo5t$){}K_@_(-FiS1#0(Z=~66zFbDI(A1M&t1S63gkTd!1U!0 z!gsJ zbqAsjEos>2{(e%f*Ho?zLKb$vn5Ttas1aY$8_gINdSOjN#LwuVGlH=nal`BOIk*S#CJ78UQv+w@Mu zitWmdZ+U{B9SiDL2S;enn$MbLtljX6t@eKVy4Pku*B#}$@jD{n7VDm`{{@6t-|Xa? zq@Cc|bbPV=p;LMSlOJsTsF>2jn9O-^x7)R?2O^rTFP3*Ycyz*ZxBZKa9f64-=-*{G zJ}t|c%(Tq~7!ZcDcFNqd#u^&4>JM{Y_{@AEKc4}X{@w5goeKAj*KS^*z=kNC@axdt zyp{J|S*O^fo!;AU>8`e3S-<|reU-^?|EXT)%1G>q*nL!AYr*^9{r{&)Y}>uA(qH$Q z#|ydTzuhlA>qN|MQM2=T*N}@ARNaSKRdX=DmFX oPQT3yTykI}7Dy_?mYzP#Pg(u(k^PgypBR9^)78&qol`;+0CAZyv;Y7A From 23336983a85438c92b1b7ea60797776ebbfd5d4d Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Sat, 21 Feb 2026 19:24:12 +0000 Subject: [PATCH 6/7] Document craft station rendering and collision Add explanatory comments and small refactor in ExampleCraftingStationObject: remove leftover commented category hints, extract local light variable, and document tile->pixel conversions, rotation cases, hitbox dimensions, sprite anchoring and preview transparency. These changes improve readability and maintenance; no functional behavior changes. --- .../objects/ExampleCraftingStationObject.java | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java index 41f789f..665686b 100644 --- a/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java +++ b/src/main/java/examplemod/examples/objects/ExampleCraftingStationObject.java @@ -24,24 +24,31 @@ public class ExampleCraftingStationObject extends CraftingStationObject { public ExampleCraftingStationObject() { super(); - // Optional: set categories like vanilla crafting stations do - //setItemCategory("objects", "craftingstations"); - //setCraftingCategory("craftingstations"); } @Override public void loadTextures() { super.loadTextures(); - // Loads: src/main/resources/objects/exampleleveleventobject.png - // (no ".png" in the string) this.texture = GameTexture.fromFile("objects/examplecraftingstation"); } @Override public Rectangle getCollision(Level level, int x, int y, int rotation) { + + // This is the "solid" hitbox for the object + // x,y are tile coords, but Rectangle uses pixels + // Each tile is 32x32 pixels, so we multiply by 32 to get pixel position. + if (rotation % 2 == 0) + // Rotations 0 and 2 (even): + // A narrower hitbox centered in the tile. + // Starts 8px in from left/top, 16px wide, 20px tall. return new Rectangle(x * 32 + 8, y * 32 + 8, 16, 20); + + // Rotations 1 and 3 (odd): + // A wider hitbox, shifted down a bit (so the "base" is lower). + // Starts 5px in from left, 14px down, 22px wide, 16px tall. return new Rectangle(x * 32 + 5, y * 32 + 14, 22, 16); } @@ -50,29 +57,41 @@ public void addDrawables(List list, OrderableDrawables tile Level level, int tileX, int tileY, TickManager tickManager, GameCamera camera, PlayerMob perspective) { + // Light level at this tile (affects brightness) GameLight light = level.getLightLevel(tileX, tileY); + + // Convert tile coords -> screen coords int drawX = camera.getTileDrawX(tileX); int drawY = camera.getTileDrawY(tileY); - // 0-3 direction index (same as vanilla) + // Object rotation/direction (0-3) int rotation = level.getObjectRotation(tileX, tileY) % 4; - // 4 rotations across, tall sprite (32 wide, texture height tall) + /* + * Draw a "tall" object sprite: + * 4 rotations across the texture (x = 0..3) + * each rotation is 32px wide + * the height is the full texture height + * + * We also shift it upward so the bottom of the sprite sits on the tile. + * The "+ 32" part is the same anchoring style used by objects like the Alchemy Table. + */ final TextureDrawOptionsEnd options = this.texture.initDraw() - .sprite(rotation, 0, 32, this.texture.getHeight()) - .light(light) - .addObjectDamageOverlay(this, level, tileX, tileY) - .pos(drawX, drawY - this.texture.getHeight() + 32); + .sprite(rotation, 0, 32, this.texture.getHeight()) // pick rotation column + .light(light) // apply lighting + .addObjectDamageOverlay(this, level, tileX, tileY) // cracks if damaged + .pos(drawX, drawY - this.texture.getHeight() + 32); // anchor sprite to tile + // Add as a sorted drawable so it layers correctly with mobs/objects in the world list.add(new LevelSortedDrawable(this, tileX, tileY) { @Override public int getSortY() { - return 16; + return 16; // standard sort height for many crafting objects } @Override public void draw(TickManager tickManager) { - options.draw(); + options.draw(); // perform the draw call we built above } }); } @@ -81,13 +100,15 @@ public void draw(TickManager tickManager) { public void drawPreview(Level level, int tileX, int tileY, int rotation, float alpha, PlayerMob player, GameCamera camera) { + // Preview is the "ghost" you see before placing the object int drawX = camera.getTileDrawX(tileX); int drawY = camera.getTileDrawY(tileY); + // Same sprite logic as addDrawables, but with transparency (alpha) this.texture.initDraw() - .sprite(rotation % 4, 0, 32, this.texture.getHeight()) - .alpha(alpha) - .draw(drawX, drawY - this.texture.getHeight() + 32); + .sprite(rotation % 4, 0, 32, this.texture.getHeight()) // choose rotation column + .alpha(alpha) // make it see-through + .draw(drawX, drawY - this.texture.getHeight() + 32); // same anchor offset } @Override From 621b3bdf9a611dd48391edf18b2bba784bdd5abd Mon Sep 17 00:00:00 2001 From: Jamesp1989SL Date: Mon, 23 Feb 2026 00:30:20 +0000 Subject: [PATCH 7/7] Add many new recipes and remove example mob spawn Remove the unused Biome import and the example mob registration from ExampleMod. Extend ExampleModRecipes with multiple new recipes (examplerangedbow, examplearrow, examplepressureplate, examplecraftingstation, exampleworkstationduo, and several EXAMPLE_TECH items like exampleconfigobject, examplejobobject, exampleleveleventobject, and exampleobject). Adjust tech requirements for several recipes (e.g. exampleorerock now uses ADVANCED_WORKSTATION, many recipes explicitly use WORKSTATION) and tweak ingredient counts. Also minor comment/formatting cleanups throughout the recipe loader. --- src/main/java/examplemod/ExampleMod.java | 6 - .../examplemod/Loaders/ExampleModRecipes.java | 109 ++++++++++++++++-- 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/main/java/examplemod/ExampleMod.java b/src/main/java/examplemod/ExampleMod.java index a8a4b72..db0c7b5 100644 --- a/src/main/java/examplemod/ExampleMod.java +++ b/src/main/java/examplemod/ExampleMod.java @@ -5,7 +5,6 @@ import necesse.engine.modLoader.annotations.ModEntry; import necesse.engine.sound.SoundSettings; import necesse.engine.sound.gameSound.GameSound; -import necesse.level.maps.biomes.Biome; @ModEntry public class ExampleMod { @@ -69,11 +68,6 @@ public void postInit() { // load our recipes from the ExampleRecipes class so we can keep this class easy to read ExampleModRecipes.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"); } } diff --git a/src/main/java/examplemod/Loaders/ExampleModRecipes.java b/src/main/java/examplemod/Loaders/ExampleModRecipes.java index 3593e2d..807533a 100644 --- a/src/main/java/examplemod/Loaders/ExampleModRecipes.java +++ b/src/main/java/examplemod/Loaders/ExampleModRecipes.java @@ -25,7 +25,7 @@ public static void registerRecipes(){ ).showAfter("woodboat")); // Show recipe after wood boat recipe - //FORGE RECIPES + // FORGE RECIPES Recipes.registerModRecipe(new Recipe( "examplebar", 1, @@ -35,7 +35,7 @@ public static void registerRecipes(){ }) ); - //IRON ANVIL RECIPES + // IRON ANVIL RECIPES Recipes.registerModRecipe(new Recipe( "examplemeleesword", 1, @@ -66,6 +66,17 @@ public static void registerRecipes(){ } )); + Recipes.registerModRecipe(new Recipe( + "examplerangedbow", + 1, + RecipeTechRegistry.IRON_ANVIL, + new Ingredient[]{ + new Ingredient("examplelog", 8), + new Ingredient("examplebar", 2), + new Ingredient("exampleitem", 2) + } + )); + Recipes.registerModRecipe(new Recipe( "examplehelmet", 1, @@ -96,7 +107,7 @@ public static void registerRecipes(){ } )); - //WORKSTATION RECIPES + // WORKSTATION RECIPES Recipes.registerModRecipe(new Recipe( "examplewall", 1, @@ -116,16 +127,49 @@ public static void registerRecipes(){ )); Recipes.registerModRecipe(new Recipe( - "exampleobject", + "examplearrow", + 25, // output amount + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplelog", 1), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplepressureplate", 1, - ExampleModTech.EXAMPLE_TECH, + RecipeTechRegistry.WORKSTATION, new Ingredient[]{ - new Ingredient("examplestone", 7), - new Ingredient("exampleitem", 3) + new Ingredient("examplestone", 6), + new Ingredient("examplebar", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplecraftingstation", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplelog", 12), + new Ingredient("examplestone", 12), + new Ingredient("examplebar", 4) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleworkstationduo", + 1, + RecipeTechRegistry.WORKSTATION, + new Ingredient[]{ + new Ingredient("examplelog", 20), + new Ingredient("examplebar", 8) } )); - //COOKING POT RECIPES + + + // COOKING POT RECIPES Recipes.registerModRecipe(new Recipe( "examplefood", 1, @@ -137,7 +181,7 @@ public static void registerRecipes(){ } )); - //ALCHEMY RECIPES + // ALCHEMY RECIPES Recipes.registerModRecipe(new Recipe( "examplepotion", 1, @@ -147,7 +191,7 @@ public static void registerRecipes(){ } )); - //LANDSCAPING RECIPES + // LANDSCAPING RECIPES Recipes.registerModRecipe(new Recipe( "examplebaserock", 1, @@ -160,14 +204,14 @@ public static void registerRecipes(){ Recipes.registerModRecipe(new Recipe( "exampleorerock", 1, - RecipeTechRegistry.LANDSCAPING, + RecipeTechRegistry.ADVANCED_WORKSTATION, new Ingredient[]{ new Ingredient("examplestone", 5), new Ingredient("exampleore", 5), } )); - //CARPENTER RECIPES + // CARPENTER RECIPES Recipes.registerModRecipe(new Recipe( "examplechair", 1, @@ -177,5 +221,46 @@ public static void registerRecipes(){ } )); + // EXAMPLE TECH RECIPES + Recipes.registerModRecipe(new Recipe( + "exampleconfigobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[]{ + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "examplejobobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[]{ + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleleveleventobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[]{ + new Ingredient("examplestone", 4), + new Ingredient("exampleitem", 1) + } + )); + + Recipes.registerModRecipe(new Recipe( + "exampleobject", + 1, + ExampleModTech.EXAMPLE_TECH, + new Ingredient[]{ + new Ingredient("examplestone", 7), + new Ingredient("exampleitem", 3) + } + )); + } }