├── icon.png ├── assets ├── small_snails.png ├── tamed_snails.png └── snail_inventory.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── src └── main │ ├── resources │ ├── data │ │ └── lovely_snails │ │ │ └── tags │ │ │ ├── entity_type │ │ │ └── can_equip_saddle.json │ │ │ ├── worldgen │ │ │ └── biome │ │ │ │ ├── swamp_like_spawn.json │ │ │ │ └── snail_spawn.json │ │ │ ├── item │ │ │ ├── snail_breeding_items.json │ │ │ ├── snail_scary_items.json │ │ │ └── snail_food_items.json │ │ │ └── block │ │ │ └── snail_spawn_blocks.json │ ├── assets │ │ └── lovely_snails │ │ │ ├── icon.png │ │ │ ├── items │ │ │ └── snail_spawn_egg.json │ │ │ ├── textures │ │ │ ├── entity │ │ │ │ └── snail │ │ │ │ │ ├── snail.png │ │ │ │ │ ├── saddle.png │ │ │ │ │ ├── decor │ │ │ │ │ ├── blue.png │ │ │ │ │ ├── cyan.png │ │ │ │ │ ├── gray.png │ │ │ │ │ ├── lime.png │ │ │ │ │ ├── pink.png │ │ │ │ │ ├── red.png │ │ │ │ │ ├── black.png │ │ │ │ │ ├── brown.png │ │ │ │ │ ├── green.png │ │ │ │ │ ├── orange.png │ │ │ │ │ ├── purple.png │ │ │ │ │ ├── white.png │ │ │ │ │ ├── yellow.png │ │ │ │ │ ├── magenta.png │ │ │ │ │ ├── light_blue.png │ │ │ │ │ └── light_gray.png │ │ │ │ │ └── snail_template.png │ │ │ ├── gui │ │ │ │ ├── container │ │ │ │ │ └── snail.png │ │ │ │ └── sprites │ │ │ │ │ └── container │ │ │ │ │ └── snail │ │ │ │ │ ├── tab │ │ │ │ │ ├── 1.png │ │ │ │ │ ├── 2.png │ │ │ │ │ ├── 3.png │ │ │ │ │ ├── 1_highlighted.png │ │ │ │ │ ├── 2_highlighted.png │ │ │ │ │ └── 3_highlighted.png │ │ │ │ │ ├── ender_chest.png │ │ │ │ │ └── ender_chest_highlighted.png │ │ │ └── item │ │ │ │ └── snail_spawn_egg.png │ │ │ ├── models │ │ │ └── item │ │ │ │ └── snail_spawn_egg.json │ │ │ ├── lang │ │ │ ├── zh_cn.json │ │ │ ├── ru_ru.json │ │ │ ├── en_us.json │ │ │ ├── de_de.json │ │ │ ├── fr_ca.json │ │ │ ├── fr_fr.json │ │ │ └── uk_ua.json │ │ │ ├── sounds.json │ │ │ └── assets.md │ ├── lovely_snails.mixins.json │ └── fabric.mod.json │ └── java │ └── dev │ └── lambdaurora │ └── lovely_snails │ ├── mixin │ ├── AgeableMobAccessor.java │ ├── ShulkerAccessor.java │ ├── client │ │ └── ClientPlayerInteractionManagerMixin.java │ ├── ServerGamePacketListenerImplMixin.java │ └── AbstractThrownPotionMixin.java │ ├── network │ ├── SnailScreenHandlerPayload.java │ └── SnailSetStoragePagePayload.java │ ├── client │ ├── render │ │ ├── SnailEntityRenderState.java │ │ ├── SnailSaddleFeatureRenderer.java │ │ ├── SnailDecorFeatureRenderer.java │ │ ├── SnailEntityRenderer.java │ │ └── SnailChestFeatureRenderer.java │ ├── LovelySnailsClient.java │ ├── model │ │ └── SnailModel.java │ └── screen │ │ └── SnailInventoryScreen.java │ ├── item │ ├── SnailSpawnEggItem.java │ └── EquipmentContainer.java │ ├── entity │ ├── goal │ │ ├── SnailHideGoal.java │ │ └── SnailFollowParentGoal.java │ └── SnailEntity.java │ ├── LovelySnails.java │ ├── registry │ └── LovelySnailsRegistry.java │ └── screen │ └── SnailScreenHandler.java ├── codeformat └── HEADER ├── gradle.properties ├── settings.gradle.kts ├── .gitignore ├── .github └── workflows │ ├── gradle_build.yml │ └── release.yml ├── gradlew.bat ├── CHANGELOG.md ├── README.md ├── LICENSE ├── LICENSE.OLD └── gradlew /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/icon.png -------------------------------------------------------------------------------- /assets/small_snails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/assets/small_snails.png -------------------------------------------------------------------------------- /assets/tamed_snails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/assets/tamed_snails.png -------------------------------------------------------------------------------- /assets/snail_inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/assets/snail_inventory.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/entity_type/can_equip_saddle.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | "lovely_snails:snail" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/items/snail_spawn_egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "type": "model", 4 | "model": "lovely_snails:item/snail_spawn_egg" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/snail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/snail.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/models/item/snail_spawn_egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "lovely_snails:item/snail_spawn_egg" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/saddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/saddle.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/container/snail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/container/snail.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/item/snail_spawn_egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/item/snail_spawn_egg.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/blue.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/cyan.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/gray.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/lime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/lime.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/pink.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/red.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/black.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/brown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/brown.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/green.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/orange.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/purple.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/white.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/yellow.png -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/worldgen/biome/swamp_like_spawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | { 5 | "id": "#c:is_swamp", 6 | "required": false 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/magenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/magenta.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/snail_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/snail_template.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/light_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/light_blue.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/entity/snail/decor/light_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/entity/snail/decor/light_gray.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/1.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/2.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/3.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/ender_chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/ender_chest.png -------------------------------------------------------------------------------- /codeformat/HEADER: -------------------------------------------------------------------------------- 1 | Copyright © ${CREATION_YEAR} LambdAurora 2 | 3 | This file is part of Lovely Snails. 4 | 5 | Licensed under the Lambda License. For more information, 6 | see the LICENSE file. 7 | 8 | #year_selection file 9 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/1_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/1_highlighted.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/2_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/2_highlighted.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/3_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/tab/3_highlighted.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/ender_chest_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LambdAurora/lovely_snails/HEAD/src/main/resources/assets/lovely_snails/textures/gui/sprites/container/snail/ender_chest_highlighted.png -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "蜗牛刷怪蛋", 3 | "entity.lovely_snails.snail": "蜗牛", 4 | "subtitles.lovely_snails.entity.snail.death": "蜗牛:死亡", 5 | "subtitles.lovely_snails.entity.snail.hurt": "蜗牛:受伤" 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/item/snail_breeding_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "minecraft:brown_mushroom", 5 | "minecraft:crimson_fungus", 6 | "minecraft:red_mushroom", 7 | "minecraft:warped_fungus" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/block/snail_spawn_blocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "minecraft:grass_block", 5 | "minecraft:moss_block", 6 | "minecraft:moss_carpet", 7 | "minecraft:mud", 8 | "minecraft:mycelium", 9 | "minecraft:podzol" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Яйцо призыва улитки", 3 | "entity.lovely_snails.snail": "Улитка", 4 | "subtitles.lovely_snails.entity.snail.death": "Улитка погибает", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Улитка ранена" 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/item/snail_scary_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "minecraft:creeper_head", 5 | "minecraft:dragon_head", 6 | "minecraft:piglin_head", 7 | "minecraft:skeleton_skull", 8 | "minecraft:wither_skeleton_skull", 9 | "minecraft:zombie_head" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1G 2 | 3 | # Project properties 4 | group=dev.lambdaurora 5 | java_version=21 6 | 7 | # Mod properties 8 | mod_version=1.2.2 9 | mod_name=Lovely Snails 10 | mod_namespace=lovely_snails 11 | modrinth_id=hBVVhStr 12 | curseforge_id=499425 13 | 14 | fabric.loom.dropNonIntermediateRootMethods=true 15 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { 5 | name = "Fabric" 6 | url = uri("https://maven.fabricmc.net/") 7 | } 8 | maven { 9 | name = "Gegy" 10 | url = uri("https://maven.gegy.dev/releases/") 11 | } 12 | } 13 | } 14 | 15 | rootProject.name = "Lovely Snails" 16 | -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/worldgen/biome/snail_spawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | { 5 | "id": "minecraft:dark_forest", 6 | "required": false 7 | }, 8 | { 9 | "id": "minecraft:mushroom_fields", 10 | "required": false 11 | }, 12 | { 13 | "id": "#c:is_mushroom", 14 | "required": false 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/lovely_snails.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "dev.lambdaurora.lovely_snails.mixin", 4 | "compatibilityLevel": "JAVA_21", 5 | "client": [ 6 | "client.ClientPlayerInteractionManagerMixin" 7 | ], 8 | "mixins": [ 9 | "AbstractThrownPotionMixin", 10 | "AgeableMobAccessor", 11 | "ServerGamePacketListenerImplMixin", 12 | "ShulkerAccessor" 13 | ], 14 | "injectors": { 15 | "defaultRequire": 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | minecraft = "1.21.10" 3 | fabric-loader = "0.17.2" 4 | fabric-api = "0.134.1+1.21.10" 5 | mappings-yalmm = "2" 6 | mappings-parchment = "2024.07.28" 7 | 8 | [libraries] 9 | minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } 10 | fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } 11 | fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } 12 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Snail Spawn Egg", 3 | "entity.lovely_snails.snail": "Snail", 4 | "subtitles.lovely_snails.entity.snail.death": "Snail dies", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Snail hurts", 6 | "tag.item.lovely_snails.snail_breeding_items": "Snail Breeding Foods", 7 | "tag.item.lovely_snails.snail_food_items": "Snail Foods", 8 | "tag.item.lovely_snails.snail_scary_items": "Scary Items for Snails" 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/de_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Schnecken-Spawn-Ei", 3 | "entity.lovely_snails.snail": "Schnecke", 4 | "subtitles.lovely_snails.entity.snail.death": "Schnecke stirbt", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Schnecke ist verletzt", 6 | "tag.item.lovely_snails.snail_breeding_items": "Schneckenzuchtfutter", 7 | "tag.item.lovely_snails.snail_food_items": "Schnecken Futter", 8 | "tag.item.lovely_snails.snail_scary_items": "Beängstigende Dinge für Schnecken" 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/fr_ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Oeuf d'apparition d'escargot", 3 | "entity.lovely_snails.snail": "Escargot", 4 | "subtitles.lovely_snails.entity.snail.death": "Escargot qui meurt", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Escargot blessé", 6 | "tag.item.lovely_snails.snail_breeding_items": "Nourriture permettant la reproduction des escargots", 7 | "tag.item.lovely_snails.snail_food_items": "Nourriture pour escargots", 8 | "tag.item.lovely_snails.snail_scary_items": "Objets qui font peur aux escargots" 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/fr_fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Oeuf d'apparition d'escargot", 3 | "entity.lovely_snails.snail": "Escargot", 4 | "subtitles.lovely_snails.entity.snail.death": "Escargot qui meurt", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Escargot blessé", 6 | "tag.item.lovely_snails.snail_breeding_items": "Nourriture permettant la reproduction des escargots", 7 | "tag.item.lovely_snails.snail_food_items": "Nourriture pour escargots", 8 | "tag.item.lovely_snails.snail_scary_items": "Objets qui font peur aux escargots" 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/data/lovely_snails/tags/item/snail_food_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": false, 3 | "values": [ 4 | "#lovely_snails:snail_breeding_items", 5 | "minecraft:bush", 6 | "minecraft:fern", 7 | "minecraft:hanging_roots", 8 | "minecraft:kelp", 9 | "minecraft:large_fern", 10 | "minecraft:leaf_litter", 11 | "minecraft:melon_seeds", 12 | "minecraft:pumpkin_seeds", 13 | "minecraft:short_dry_grass", 14 | "minecraft:short_grass", 15 | "minecraft:tall_dry_grass", 16 | "minecraft:tall_grass", 17 | "minecraft:torchflower_seeds", 18 | "minecraft:wheat_seeds" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/sounds.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.lovely_snails.snail.death": { 3 | "sounds": [ 4 | "mob/slime/small1", 5 | "mob/slime/small2", 6 | "mob/slime/small3", 7 | "mob/slime/small4", 8 | "mob/slime/small5" 9 | ], 10 | "subtitle": "subtitles.lovely_snails.entity.snail.death" 11 | }, 12 | "entity.lovely_snails.snail.hurt": { 13 | "sounds": [ 14 | "mob/slime/small1", 15 | "mob/slime/small2", 16 | "mob/slime/small3", 17 | "mob/slime/small4", 18 | "mob/slime/small5" 19 | ], 20 | "subtitle": "subtitles.lovely_snails.entity.snail.hurt" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/lang/uk_ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "item.lovely_snails.snail_spawn_egg": "Яйце виклику равлика", 3 | "entity.lovely_snails.snail": "Равлик", 4 | "subtitles.lovely_snails.entity.snail.death": "Равлик гине", 5 | "subtitles.lovely_snails.entity.snail.hurt": "Равлика поранено", 6 | "tag.item.lovely_snails.snail_breeding_items": "Їжа розмноження равликів", 7 | "tag.item.lovely_snails.snail_food_items": "Їжа равликів", 8 | "tag.item.lovely_snails.snail_scary_items": "Лякають равликів", 9 | "modmenu.descriptionTranslation.lovely_snails": "Додає дещо великих равликів. Зроблено для Модфесту 1.17" 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/mixin/AgeableMobAccessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.mixin; 11 | 12 | import net.minecraft.network.syncher.TrackedEntityData; 13 | import net.minecraft.world.entity.AgeableMob; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.gen.Accessor; 16 | 17 | @Mixin(AgeableMob.class) 18 | public interface AgeableMobAccessor { 19 | @Accessor("DATA_BABY_ID") 20 | static TrackedEntityData lovely_snails$getChild() { 21 | throw new UnsupportedOperationException("Mixin injection failed."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/mixin/ShulkerAccessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.mixin; 11 | 12 | import net.minecraft.world.entity.ai.attributes.AttributeModifier; 13 | import net.minecraft.world.entity.monster.Shulker; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.gen.Accessor; 16 | 17 | @Mixin(Shulker.class) 18 | public interface ShulkerAccessor { 19 | @Accessor("COVERED_ARMOR_MODIFIER") 20 | static AttributeModifier lovely_snails$getCoveredArmorModifier() { 21 | throw new UnsupportedOperationException("Mixin injection failed."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/network/SnailScreenHandlerPayload.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2025 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.network; 11 | 12 | import net.minecraft.network.FriendlyByteBuf; 13 | import net.minecraft.network.codec.ByteBufCodecs; 14 | import net.minecraft.network.codec.StreamCodec; 15 | 16 | public record SnailScreenHandlerPayload(int snailId, byte storagePage) { 17 | public static final StreamCodec STREAM_CODEC = StreamCodec.composite( 18 | ByteBufCodecs.VAR_INT, SnailScreenHandlerPayload::snailId, 19 | ByteBufCodecs.BYTE, SnailScreenHandlerPayload::storagePage, 20 | SnailScreenHandlerPayload::new 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/render/SnailEntityRenderState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2025 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.render; 11 | 12 | import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; 13 | import net.minecraft.world.item.DyeColor; 14 | import net.minecraft.world.item.ItemStack; 15 | 16 | /** 17 | * Represents the required rendering data for snails. 18 | * 19 | * @author Patbox 20 | * @version 1.2.1 21 | * @since 1.2.1 22 | */ 23 | public class SnailEntityRenderState extends LivingEntityRenderState { 24 | public boolean isScared = false; 25 | public DyeColor carpetColor = null; 26 | public ItemStack[] chests = new ItemStack[3]; 27 | public boolean hasSaddle = false; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "lovely_snails", 4 | "version": "${version}", 5 | "name": "Lovely Snails", 6 | "description": "Adds some big snails. Made for Modfest 1.17.", 7 | "authors": [ 8 | "LambdAurora" 9 | ], 10 | "contributors": [ 11 | "Arathain", 12 | "Patbox", 13 | "Drex" 14 | ], 15 | "contact": { 16 | "homepage": "https://modrinth.com/mod/lovely_snails", 17 | "sources": "https://github.com/LambdAurora/lovely_snails.git", 18 | "issues": "https://github.com/LambdAurora/lovely_snails/issues" 19 | }, 20 | "license": "Lambda License", 21 | "icon": "assets/lovely_snails/icon.png", 22 | "environment": "*", 23 | "entrypoints": { 24 | "main": [ 25 | "dev.lambdaurora.lovely_snails.LovelySnails" 26 | ], 27 | "client": [ 28 | "dev.lambdaurora.lovely_snails.client.LovelySnailsClient" 29 | ] 30 | }, 31 | "mixins": [ 32 | "lovely_snails.mixins.json" 33 | ], 34 | "depends": { 35 | "minecraft": ">=1.21.9 <=1.21.10", 36 | "fabricloader": ">=0.17.0", 37 | "fabric-api": ">=0.134.0+1.21.9", 38 | "java": ">=21" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/item/SnailSpawnEggItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.item; 11 | 12 | import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents; 13 | import net.minecraft.world.entity.EntityType; 14 | import net.minecraft.world.entity.Mob; 15 | import net.minecraft.world.item.CreativeModeTabs; 16 | import net.minecraft.world.item.Item; 17 | import net.minecraft.world.item.SpawnEggItem; 18 | 19 | /** 20 | * Represents a spawn egg that will try to sneak in where the spawn eggs are. 21 | * 22 | * @author LambdAurora 23 | * @version 1.1.1 24 | * @since 1.1.0 25 | */ 26 | public class SnailSpawnEggItem extends SpawnEggItem { 27 | public SnailSpawnEggItem(EntityType entityType, Item.Properties properties) { 28 | this(properties.spawnEgg(entityType)); 29 | } 30 | 31 | public SnailSpawnEggItem(Item.Properties properties) { 32 | super(properties); 33 | 34 | ItemGroupEvents.modifyEntriesEvent(CreativeModeTabs.SPAWN_EGGS).register(entries -> { 35 | entries.accept(this); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # LambdAurora's ignore file 3 | # 4 | # v0.24 5 | 6 | # JetBrains 7 | .idea/ 8 | *.iml 9 | *.ipr 10 | *.iws 11 | ## Intellij IDEA 12 | out/ 13 | ## CLion 14 | cmake-build-debug*/ 15 | cmake-build-release*/ 16 | ## Eclipse 17 | eclipse 18 | *.launch 19 | .settings 20 | .metadata 21 | .classpath 22 | .project 23 | ## Visual Studio 24 | .vs/ 25 | CMakeSettings.json 26 | 27 | # Build system 28 | ## Cargo 29 | Cargo.lock 30 | ## CMake 31 | CMakeCache.txt 32 | CMakeLists.txt.user 33 | CMakeFiles/ 34 | ## QMake 35 | .qmake.stash 36 | ## Gradle 37 | .gradle/ 38 | ## Node.JS 39 | node_modules/ 40 | ## PHP composer 41 | vendor/ 42 | 43 | # Editors 44 | ## VSCode 45 | .vscode/ 46 | 47 | # Logging 48 | logs/ 49 | 50 | # Languages 51 | ## Java 52 | classes/ 53 | ## Kotlin 54 | .kotlin/ 55 | ## Python 56 | __pycache__/ 57 | venv/ 58 | ## Rust 59 | **/*.rs.bk 60 | 61 | # OS 62 | ## Windows 63 | desktop.ini 64 | ## MacOS 65 | .DS_Store 66 | 67 | # File types 68 | *.dll 69 | *.db 70 | *.tar.?z 71 | 72 | # Asset files automatic backup 73 | .*.png-autosave.kra 74 | *.png~ 75 | 76 | # Compilation artifacts/Binaries 77 | *.o 78 | *.so 79 | *.dylib 80 | *.lib 81 | lib*.a 82 | 83 | # Common 84 | bin/ 85 | build/ 86 | dist/ 87 | lib/ 88 | !/lib/ 89 | !src/lib/ 90 | !src/**/lib/ 91 | obj/ 92 | run/ 93 | target/ 94 | /local.properties 95 | -------------------------------------------------------------------------------- /src/main/resources/assets/lovely_snails/assets.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | ## Made by [Arathain](https://github.com/Arathain) and licensed under the [Creative Commons CC0 license][cc0]: 4 | 5 | - `textures/entity/snail/snail.png` 6 | 7 | ## Made by [LambdAurora](https://github.com/LambdAurora) and licensed under the [Creative Commons CC0 license][cc0]: 8 | 9 | - `textures/entity/snail/decor/black.png` 10 | - `textures/entity/snail/decor/blue.png` 11 | - `textures/entity/snail/decor/brown.png` 12 | - `textures/entity/snail/decor/cyan.png` 13 | - `textures/entity/snail/decor/gray.png` 14 | - `textures/entity/snail/decor/green.png` 15 | - `textures/entity/snail/decor/light_blue.png` 16 | - `textures/entity/snail/decor/light_gray.png` 17 | - `textures/entity/snail/decor/lime.png` 18 | - `textures/entity/snail/decor/magenta.png` 19 | - `textures/entity/snail/decor/orange.png` 20 | - `textures/entity/snail/decor/pink.png` 21 | - `textures/entity/snail/decor/purple.png` 22 | - `textures/entity/snail/decor/red.png` 23 | - `textures/entity/snail/decor/white.png` 24 | - `textures/entity/snail/decor/yellow.png` 25 | - `textures/entity/snail/saddle.png` 26 | - `textures/entity/snail/snail_template.png` 27 | - `textures/gui/container/snail.png` 28 | - `textures/gui/snail_ender_chest_button.png` 29 | 30 | [cc0]: https://creativecommons.org/publicdomain/zero/1.0/ -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/item/EquipmentContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2025 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | 11 | package dev.lambdaurora.lovely_snails.item; 12 | 13 | import net.minecraft.world.entity.EquipmentSlot; 14 | import net.minecraft.world.entity.LivingEntity; 15 | import net.minecraft.world.entity.player.Player; 16 | import net.minecraft.world.item.ItemStack; 17 | import net.minecraft.world.ticks.ContainerSingleItem; 18 | 19 | /** 20 | * Wraps equipment as a container. 21 | * 22 | * @author Patbox 23 | * @version 1.2.1 24 | * @since 1.2.1 25 | */ 26 | public record EquipmentContainer(LivingEntity entity, EquipmentSlot slot) implements ContainerSingleItem { 27 | @Override 28 | public ItemStack getTheItem() { 29 | return this.entity.getItemBySlot(this.slot); 30 | } 31 | 32 | @Override 33 | public void setTheItem(ItemStack stack) { 34 | this.entity.setItemSlot(this.slot, stack); 35 | } 36 | 37 | @Override 38 | public void setChanged() { 39 | } 40 | 41 | @Override 42 | public int getMaxStackSize() { 43 | return 1; 44 | } 45 | 46 | @Override 47 | public boolean stillValid(Player player) { 48 | return this.entity.isAlive(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/mixin/client/ClientPlayerInteractionManagerMixin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.mixin.client; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import net.minecraft.client.Minecraft; 14 | import net.minecraft.client.multiplayer.MultiPlayerGameMode; 15 | import org.spongepowered.asm.mixin.Final; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Shadow; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 21 | 22 | @Mixin(MultiPlayerGameMode.class) 23 | public class ClientPlayerInteractionManagerMixin { 24 | @Shadow 25 | @Final 26 | private Minecraft minecraft; 27 | 28 | @Inject(method = "isServerControlledInventory", at = @At("HEAD"), cancellable = true) 29 | private void lovely_snails$onHasRidingInventory(CallbackInfoReturnable cir) { 30 | //noinspection ConstantConditions 31 | if (this.minecraft.player.isPassenger() && this.minecraft.player.getVehicle() instanceof SnailEntity) { 32 | cir.setReturnValue(true); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/gradle_build.yml: -------------------------------------------------------------------------------- 1 | name: "Gradle Build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | pull_request: 8 | 9 | concurrency: build-${{ github.sha }} 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | java: [ 21, 25 ] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: "Checkout" 19 | uses: actions/checkout@v4 20 | - name: "Set up Java" 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: "temurin" 24 | java-version: ${{ matrix.java }} 25 | - name: "Set up Gradle" 26 | uses: gradle/actions/setup-gradle@v4 27 | with: 28 | cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/1.') && !startsWith(github.ref, 'refs/heads/dev/') && !startsWith(github.ref, 'refs/tags/v') }} 29 | - name: "Handle Loom Cache" 30 | uses: actions/cache@v4 31 | with: 32 | path: "**/.gradle/loom-cache" 33 | key: "${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}" 34 | restore-keys: "${{ runner.os }}-gradle-" 35 | - name: "Build with Gradle" 36 | run: ./gradlew build --parallel --stacktrace 37 | - name: "Upload artifacts" 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: Artifacts_j${{ matrix.java }} 41 | path: ./build/libs/ 42 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/mixin/ServerGamePacketListenerImplMixin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.mixin; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket; 14 | import net.minecraft.server.level.ServerPlayer; 15 | import net.minecraft.server.network.ServerGamePacketListenerImpl; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Shadow; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | 22 | @Mixin(ServerGamePacketListenerImpl.class) 23 | public class ServerGamePacketListenerImplMixin { 24 | @Shadow 25 | public ServerPlayer player; 26 | 27 | @Inject(method = "handlePlayerCommand", at = @At("RETURN")) 28 | private void lovely_snails$handlePlayerCommand(ServerboundPlayerCommandPacket packet, CallbackInfo ci) { 29 | if (packet.getAction() == ServerboundPlayerCommandPacket.Action.OPEN_INVENTORY && this.player.getVehicle() instanceof SnailEntity snail) { 30 | snail.openInventory(this.player); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/mixin/AbstractThrownPotionMixin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.mixin; 11 | 12 | import com.llamalad7.mixinextras.sugar.Local; 13 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 14 | import net.minecraft.server.level.ServerLevel; 15 | import net.minecraft.world.entity.EntityType; 16 | import net.minecraft.world.entity.projectile.AbstractThrownPotion; 17 | import net.minecraft.world.entity.projectile.ThrowableItemProjectile; 18 | import net.minecraft.world.level.Level; 19 | import net.minecraft.world.phys.AABB; 20 | import org.spongepowered.asm.mixin.Mixin; 21 | import org.spongepowered.asm.mixin.injection.At; 22 | import org.spongepowered.asm.mixin.injection.Inject; 23 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 24 | 25 | @Mixin(AbstractThrownPotion.class) 26 | public abstract class AbstractThrownPotionMixin extends ThrowableItemProjectile { 27 | public AbstractThrownPotionMixin(EntityType entityType, Level level) { 28 | super(entityType, level); 29 | } 30 | 31 | @Inject( 32 | method = "onHitAsWater", 33 | at = @At( 34 | value = "INVOKE", 35 | target = "Lnet/minecraft/world/level/Level;getEntitiesOfClass(Ljava/lang/Class;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;)Ljava/util/List;" 36 | ) 37 | ) 38 | private void onWaterSplash(ServerLevel level, CallbackInfo ci, @Local AABB box) { 39 | var snails = level.getEntitiesOfClass(SnailEntity.class, box); 40 | for (var snail : snails) { 41 | snail.onWaterSplashed(this.getOwner()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/network/SnailSetStoragePagePayload.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2025 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.network; 11 | 12 | import dev.lambdaurora.lovely_snails.LovelySnails; 13 | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; 14 | import net.minecraft.network.FriendlyByteBuf; 15 | import net.minecraft.network.codec.StreamCodec; 16 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 17 | import org.jetbrains.annotations.NotNull; 18 | 19 | /** 20 | * Represents the set storage page of a snail's container packet payload. 21 | * 22 | * @param syncId the synchronization identifier of the container 23 | * @param storagePage the selected storage page 24 | * @author LambdAurora 25 | * @version 1.2.0 26 | * @since 1.2.0 27 | */ 28 | public record SnailSetStoragePagePayload(int syncId, byte storagePage) implements CustomPacketPayload { 29 | public static Type TYPE = new Type<>(LovelySnails.id("snail_set_storage_page")); 30 | public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec( 31 | SnailSetStoragePagePayload::write, SnailSetStoragePagePayload::new 32 | ); 33 | 34 | @Override 35 | public @NotNull Type type() { 36 | return TYPE; 37 | } 38 | 39 | public SnailSetStoragePagePayload(FriendlyByteBuf buffer) { 40 | this(buffer.readVarInt(), buffer.readByte()); 41 | } 42 | 43 | public void write(FriendlyByteBuf buffer) { 44 | buffer.writeVarInt(this.syncId); 45 | buffer.writeByte(this.storagePage); 46 | } 47 | 48 | static { 49 | PayloadTypeRegistry.playC2S().register(TYPE, STREAM_CODEC); 50 | PayloadTypeRegistry.playS2C().register(TYPE, STREAM_CODEC); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/render/SnailSaddleFeatureRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.render; 11 | 12 | import com.mojang.blaze3d.vertex.MatrixStack; 13 | import dev.lambdaurora.lovely_snails.LovelySnails; 14 | import dev.lambdaurora.lovely_snails.client.LovelySnailsClient; 15 | import dev.lambdaurora.lovely_snails.client.model.SnailModel; 16 | import net.minecraft.client.renderer.RenderType; 17 | import net.minecraft.client.renderer.SubmitNodeCollector; 18 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 19 | import net.minecraft.client.renderer.entity.RenderLayerParent; 20 | import net.minecraft.client.renderer.entity.layers.RenderLayer; 21 | import net.minecraft.client.renderer.texture.OverlayTexture; 22 | import net.minecraft.resources.Identifier; 23 | 24 | /** 25 | * Renders decoration on a snail. 26 | * 27 | * @author LambdAurora 28 | * @version 1.2.0 29 | * @since 1.2.0 30 | */ 31 | public class SnailSaddleFeatureRenderer extends RenderLayer { 32 | private static final Identifier TEXTURE = LovelySnails.id("textures/entity/snail/saddle.png"); 33 | private final SnailModel model; 34 | 35 | public SnailSaddleFeatureRenderer(RenderLayerParent featureRendererContext, EntityRendererProvider.Context context) { 36 | super(featureRendererContext); 37 | this.model = new SnailModel(context.bakeLayer(LovelySnailsClient.SNAIL_SADDLE_MODEL_LAYER)); 38 | } 39 | 40 | @Override 41 | public void submit(MatrixStack matrices, SubmitNodeCollector submitNodeCollector, int light, SnailEntityRenderState state, float tickDelta, float animationProgress) { 42 | if (!state.hasSaddle) return; 43 | this.model.setupAnim(state); 44 | submitNodeCollector.submitModel(this.model, state, matrices, RenderType.entityCutoutNoCull(TEXTURE), light, OverlayTexture.NO_OVERLAY, 0xffffffff, null, state.outlineColor, null); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/entity/goal/SnailHideGoal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.entity.goal; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import dev.lambdaurora.lovely_snails.registry.LovelySnailsRegistry; 14 | import net.minecraft.world.entity.EntitySelector; 15 | import net.minecraft.world.entity.EquipmentSlot; 16 | import net.minecraft.world.entity.LivingEntity; 17 | import net.minecraft.world.entity.ai.goal.Goal; 18 | import net.minecraft.world.entity.monster.Monster; 19 | 20 | import java.util.EnumSet; 21 | 22 | /** 23 | * Makes the snail hides if it senses danger nearby. 24 | * 25 | * @author LambdAurora 26 | * @version 1.2.0 27 | * @since 1.0.0 28 | */ 29 | public class SnailHideGoal extends Goal { 30 | private final SnailEntity snail; 31 | private final double vitalSpaceDistance; 32 | 33 | public SnailHideGoal(SnailEntity snail, double distance) { 34 | this.snail = snail; 35 | this.vitalSpaceDistance = distance; 36 | 37 | this.setFlags(EnumSet.of(Flag.JUMP, Flag.MOVE, Flag.LOOK)); 38 | } 39 | 40 | private boolean isThereScaryEntitiesAround() { 41 | var scaryEntities = this.snail.level().getEntities( 42 | this.snail, 43 | this.snail.getBoundingBox().inflate(this.vitalSpaceDistance, 3, this.vitalSpaceDistance), 44 | EntitySelector.NO_CREATIVE_OR_SPECTATOR.and(entity -> entity instanceof Monster) 45 | .or(entity -> entity instanceof LivingEntity living 46 | && living.getItemBySlot(EquipmentSlot.HEAD).isIn(LovelySnailsRegistry.SNAIL_SCARY_ITEMS) 47 | ) 48 | ); 49 | return !scaryEntities.isEmpty(); 50 | } 51 | 52 | @Override 53 | public boolean canUse() { 54 | return this.snail.getLastHurtByMob() != null || this.isThereScaryEntitiesAround(); 55 | } 56 | 57 | @Override 58 | public boolean canContinueToUse() { 59 | return this.snail.getLastHurtByMob() != null || this.isThereScaryEntitiesAround(); 60 | } 61 | 62 | @Override 63 | public void start() { 64 | this.snail.getNavigation().stop(); 65 | this.snail.setScared(true); 66 | } 67 | 68 | @Override 69 | public void stop() { 70 | this.snail.setScared(false); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/render/SnailDecorFeatureRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.render; 11 | 12 | import com.mojang.blaze3d.vertex.MatrixStack; 13 | import dev.lambdaurora.lovely_snails.LovelySnails; 14 | import dev.lambdaurora.lovely_snails.client.LovelySnailsClient; 15 | import dev.lambdaurora.lovely_snails.client.model.SnailModel; 16 | import net.minecraft.client.renderer.RenderType; 17 | import net.minecraft.client.renderer.SubmitNodeCollector; 18 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 19 | import net.minecraft.client.renderer.entity.RenderLayerParent; 20 | import net.minecraft.client.renderer.entity.layers.RenderLayer; 21 | import net.minecraft.client.renderer.texture.OverlayTexture; 22 | import net.minecraft.resources.Identifier; 23 | import net.minecraft.world.item.DyeColor; 24 | 25 | /** 26 | * Renders decoration on a snail. 27 | * 28 | * @author LambdAurora 29 | * @version 1.2.0 30 | * @since 1.0.0 31 | */ 32 | public class SnailDecorFeatureRenderer extends RenderLayer { 33 | private static final Identifier[] TEXTURES; 34 | private final SnailModel model; 35 | 36 | public SnailDecorFeatureRenderer(RenderLayerParent featureRendererContext, EntityRendererProvider.Context context) { 37 | super(featureRendererContext); 38 | this.model = new SnailModel(context.bakeLayer(LovelySnailsClient.SNAIL_DECOR_MODEL_LAYER)); 39 | } 40 | 41 | @Override 42 | public void submit(MatrixStack matrices, SubmitNodeCollector submitNodeCollector, int light, SnailEntityRenderState state, float tickDelta, float animationProgress) { 43 | var dyeColor = state.carpetColor; 44 | if (dyeColor == null) return; 45 | var texture = TEXTURES[dyeColor.getId()]; 46 | 47 | this.model.setupAnim(state); 48 | submitNodeCollector.submitModel(this.model, state, matrices, RenderType.entityCutoutNoCull(texture), light, OverlayTexture.NO_OVERLAY, 0xffffffff, null, state.outlineColor, null); 49 | } 50 | 51 | static { 52 | var colors = DyeColor.values(); 53 | TEXTURES = new Identifier[colors.length]; 54 | for (var color : colors) { 55 | TEXTURES[color.getId()] = LovelySnails.id("textures/entity/snail/decor/" + color.getName() + ".png"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/render/SnailEntityRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.render; 11 | 12 | import com.mojang.blaze3d.vertex.MatrixStack; 13 | import dev.lambdaurora.lovely_snails.LovelySnails; 14 | import dev.lambdaurora.lovely_snails.client.LovelySnailsClient; 15 | import dev.lambdaurora.lovely_snails.client.model.SnailModel; 16 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 17 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 18 | import net.minecraft.client.renderer.entity.MobRenderer; 19 | import net.minecraft.resources.Identifier; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | /** 23 | * Represents the snail entity renderer. 24 | * 25 | * @author LambdAurora 26 | * @version 1.2.0 27 | * @since 1.0.0 28 | */ 29 | public class SnailEntityRenderer extends MobRenderer { 30 | public static final Identifier TEXTURE = LovelySnails.id("textures/entity/snail/snail.png"); 31 | 32 | public SnailEntityRenderer(EntityRendererProvider.Context context) { 33 | super(context, new SnailModel(context.bakeLayer(LovelySnailsClient.SNAIL_MODEL_LAYER)), .5f); 34 | 35 | this.addLayer(new SnailSaddleFeatureRenderer(this, context)); 36 | this.addLayer(new SnailDecorFeatureRenderer(this, context)); 37 | this.addLayer(new SnailChestFeatureRenderer(this, context)); 38 | } 39 | 40 | @Override 41 | protected void scale(SnailEntityRenderState state, MatrixStack matrices) { 42 | super.scale(state, matrices); 43 | this.getModel().getCurrentModel(state).updateMatrix(matrices); 44 | } 45 | 46 | @Override 47 | public @NotNull SnailEntityRenderState createRenderState() { 48 | return new SnailEntityRenderState(); 49 | } 50 | 51 | @Override 52 | public void extractRenderState(SnailEntity snail, SnailEntityRenderState state, float tickDelta) { 53 | super.extractRenderState(snail, state, tickDelta); 54 | state.isScared = snail.isScared(); 55 | state.hasSaddle = !snail.getSaddle().isEmpty(); 56 | state.carpetColor = snail.getCarpetColor(); 57 | for (int i = 0; i < 3; i++) { 58 | state.chests[i] = snail.getChest(i).copy(); 59 | } 60 | } 61 | 62 | @Override 63 | public @NotNull Identifier getTextureLocation(SnailEntityRenderState livingEntityRenderState) { 64 | return TEXTURE; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/entity/goal/SnailFollowParentGoal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.entity.goal; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import net.minecraft.world.entity.AgeableMob; 14 | import net.minecraft.world.entity.ai.goal.Goal; 15 | 16 | /** 17 | * Modified {@link net.minecraft.world.entity.ai.goal.FollowParentGoal}, 18 | * which uses a {@link SnailEntity#isBaby()} instead of {@link AgeableMob#getAge()}. 19 | * 20 | * @author LambdAurora 21 | * @version 1.0.0 22 | * @since 1.0.0 23 | */ 24 | public class SnailFollowParentGoal extends Goal { 25 | private final SnailEntity self; 26 | private final double speed; 27 | private SnailEntity parent; 28 | private int delay; 29 | 30 | public SnailFollowParentGoal(SnailEntity self, double speed) { 31 | this.self = self; 32 | this.speed = speed; 33 | } 34 | 35 | @Override 36 | public boolean canUse() { 37 | if (this.self.getAge() >= 0) { 38 | return false; 39 | } else { 40 | var closeSnails = this.self.level().getEntitiesOfClass(SnailEntity.class, 41 | this.self.getBoundingBox().inflate(8.0, 4.0, 8.0) 42 | ); 43 | SnailEntity closestParent = null; 44 | double closestParentDistance = Double.MAX_VALUE; 45 | 46 | for (var snail : closeSnails) { 47 | if (!snail.isBaby()) { 48 | double snailDistance = this.self.distanceToSqr(snail); 49 | if (!(snailDistance > closestParentDistance)) { 50 | closestParentDistance = snailDistance; 51 | closestParent = snail; 52 | } 53 | } 54 | } 55 | 56 | if (closestParent == null) { 57 | return false; 58 | } else if (closestParentDistance < 9.0) { 59 | return false; 60 | } else { 61 | this.parent = closestParent; 62 | return true; 63 | } 64 | } 65 | } 66 | 67 | @Override 68 | public boolean canContinueToUse() { 69 | if (!this.self.isBaby()) { 70 | return false; 71 | } else if (!this.parent.isAlive()) { 72 | return false; 73 | } else { 74 | double parentDistance = this.self.distanceToSqr(this.parent); 75 | return !(parentDistance < 9.0) && !(parentDistance > 256.0); 76 | } 77 | } 78 | 79 | @Override 80 | public void start() { 81 | this.delay = 0; 82 | } 83 | 84 | @Override 85 | public void stop() { 86 | this.parent = null; 87 | } 88 | 89 | @Override 90 | public void tick() { 91 | if (--this.delay <= 0) { 92 | this.delay = 10; 93 | this.self.getNavigation().moveTo(this.parent, this.speed); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/LovelySnailsClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client; 11 | 12 | import dev.lambdaurora.lovely_snails.LovelySnails; 13 | import dev.lambdaurora.lovely_snails.client.model.SnailModel; 14 | import dev.lambdaurora.lovely_snails.client.render.SnailEntityRenderer; 15 | import dev.lambdaurora.lovely_snails.client.screen.SnailInventoryScreen; 16 | import dev.lambdaurora.lovely_snails.network.SnailSetStoragePagePayload; 17 | import dev.lambdaurora.lovely_snails.registry.LovelySnailsRegistry; 18 | import dev.lambdaurora.lovely_snails.screen.SnailScreenHandler; 19 | import net.fabricmc.api.ClientModInitializer; 20 | import net.fabricmc.api.EnvType; 21 | import net.fabricmc.api.Environment; 22 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 23 | import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry; 24 | import net.minecraft.client.gui.screens.MenuScreens; 25 | import net.minecraft.client.model.geom.ModelLayerLocation; 26 | import net.minecraft.client.model.geom.builders.CubeDeformation; 27 | import net.minecraft.client.renderer.entity.EntityRenderers; 28 | 29 | /** 30 | * Represents the Lovely Snails client mod. 31 | * 32 | * @author LambdAurora 33 | * @version 1.2.0 34 | * @since 1.0.0 35 | */ 36 | @Environment(EnvType.CLIENT) 37 | public class LovelySnailsClient implements ClientModInitializer { 38 | public static final ModelLayerLocation SNAIL_MODEL_LAYER = new ModelLayerLocation(LovelySnails.id("snail"), "main"); 39 | public static final ModelLayerLocation SNAIL_SADDLE_MODEL_LAYER = new ModelLayerLocation(LovelySnails.id("snail"), "saddle"); 40 | public static final ModelLayerLocation SNAIL_DECOR_MODEL_LAYER = new ModelLayerLocation(LovelySnails.id("snail"), "decor"); 41 | 42 | @Override 43 | public void onInitializeClient() { 44 | EntityRenderers.register(LovelySnailsRegistry.SNAIL_ENTITY_TYPE, SnailEntityRenderer::new); 45 | EntityModelLayerRegistry.registerModelLayer(SNAIL_MODEL_LAYER, () -> SnailModel.model(CubeDeformation.NONE)); 46 | EntityModelLayerRegistry.registerModelLayer(SNAIL_SADDLE_MODEL_LAYER, () -> SnailModel.model(new CubeDeformation(0.5f))); 47 | EntityModelLayerRegistry.registerModelLayer(SNAIL_DECOR_MODEL_LAYER, () -> SnailModel.model(new CubeDeformation(0.25f))); 48 | 49 | MenuScreens.register(LovelySnailsRegistry.SNAIL_SCREEN_HANDLER_TYPE, SnailInventoryScreen::new); 50 | 51 | ClientPlayNetworking.registerGlobalReceiver(SnailSetStoragePagePayload.TYPE, 52 | (payload, context) -> { 53 | context.client().execute(() -> { 54 | if (context.player().containerMenu instanceof SnailScreenHandler snailScreenHandler 55 | && snailScreenHandler.syncId == payload.syncId()) { 56 | snailScreenHandler.setCurrentStoragePage(payload.storagePage()); 57 | } 58 | }); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/LovelySnails.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails; 11 | 12 | import dev.lambdaurora.lovely_snails.network.SnailSetStoragePagePayload; 13 | import dev.lambdaurora.lovely_snails.registry.LovelySnailsRegistry; 14 | import dev.lambdaurora.lovely_snails.screen.SnailScreenHandler; 15 | import net.fabricmc.api.ModInitializer; 16 | import net.fabricmc.fabric.api.biome.v1.BiomeModifications; 17 | import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; 18 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 19 | import net.minecraft.resources.Identifier; 20 | import net.minecraft.world.Container; 21 | import net.minecraft.world.ItemStackWithSlot; 22 | import net.minecraft.world.entity.MobCategory; 23 | import net.minecraft.world.level.storage.ValueInput; 24 | import net.minecraft.world.level.storage.ValueOutput; 25 | 26 | /** 27 | * Represents the Lovely Snails mod. 28 | * 29 | * @author LambdAurora 30 | * @version 1.2.0 31 | * @since 1.0.0 32 | */ 33 | public class LovelySnails implements ModInitializer { 34 | public static final String NAMESPACE = "lovely_snails"; 35 | 36 | @Override 37 | public void onInitialize() { 38 | LovelySnailsRegistry.init(); 39 | 40 | ServerPlayNetworking.registerGlobalReceiver(SnailSetStoragePagePayload.TYPE, 41 | (payload, context) -> { 42 | context.server().execute(() -> { 43 | if (context.player().containerMenu instanceof SnailScreenHandler snailScreenHandler 44 | && snailScreenHandler.syncId == payload.syncId()) { 45 | snailScreenHandler.setCurrentStoragePage(payload.storagePage()); 46 | } 47 | }); 48 | }); 49 | 50 | BiomeModifications.addSpawn(BiomeSelectors.tag(LovelySnailsRegistry.SNAIL_SWAMP_LIKE_SPAWN_BIOMES), 51 | MobCategory.CREATURE, LovelySnailsRegistry.SNAIL_ENTITY_TYPE, 10, 1, 3 52 | ); 53 | BiomeModifications.addSpawn(BiomeSelectors.tag(LovelySnailsRegistry.SNAIL_REGULAR_SPAWN_BIOMES), 54 | MobCategory.CREATURE, LovelySnailsRegistry.SNAIL_ENTITY_TYPE, 8, 1, 3 55 | ); 56 | } 57 | 58 | public static Identifier id(String path) { 59 | return Identifier.of(NAMESPACE, path); 60 | } 61 | 62 | public static void readInventory(ValueInput input, String key, Container stacks, int start) { 63 | var slots = input.list(key, ItemStackWithSlot.CODEC); 64 | if (slots.isEmpty()) return; 65 | 66 | 67 | for (var slot : slots.get()) { 68 | if (slot.isValidInContainer(stacks.size() - start)) { 69 | stacks.setItem(start + slot.slot(), slot.stack()); 70 | } 71 | } 72 | } 73 | 74 | public static void writeInventory(ValueOutput output, String key, Container stacks, int start, int end) { 75 | writeInventory(output, key, stacks, start, end, true); 76 | } 77 | 78 | public static void writeInventory(ValueOutput output, String key, Container stacks, int start, int end, boolean setIfEmpty) { 79 | var list = output.list(key, ItemStackWithSlot.CODEC); 80 | 81 | for (int i = start; i < end; ++i) { 82 | var slotStack = stacks.getItem(i); 83 | if (!slotStack.isEmpty()) { 84 | list.add(new ItemStackWithSlot(i - start, slotStack)); 85 | } 86 | } 87 | 88 | if (list.isEmpty() && !setIfEmpty) { 89 | output.remove(key); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Lovely Snails Changelog 2 | 3 | ## 1.0.0 4 | 5 | - Initial Release. 6 | - Added the Snail entity. 7 | - Can be tamed with mushrooms. 8 | - Once tamed, it can be taken care of to grow into an adult. 9 | - Adult snails can carry players and items if equipped with chests. 10 | - Items in a snail inventory are ordered by pages, which are associated to a chest. 11 | - Snail inventory pages can be browsed via scrolling in the storage space. 12 | - Added Snail Spawn Egg. 13 | 14 | ### 1.0.1 15 | 16 | - Added French translations. 17 | - Added Simplified Chinese translations ([#2](https://github.com/LambdAurora/lovely_snails/pull/2)). 18 | - Fixed food not being consumed by snails. 19 | 20 | ### 1.0.2 21 | 22 | - Fixed shift-click behavior in snail inventory. 23 | - Fixed chests being transferable to baby snails. 24 | 25 | ### 1.0.3 26 | 27 | - Added inventory page buttons to improve accessibility. 28 | - Fixed baby snails collision box. 29 | 30 | ### 1.0.4 31 | 32 | - Updated to 1.18.2. 33 | - Added snail spawn biome tags: 34 | - `lovely_snails:snail_spawn` for a spawn weight of 8 35 | - `lovely_snails:swamp_like_spawn` for a spawn weight of 10 36 | 37 | ## 1.1.0 38 | 39 | - Updated to 1.19. 40 | - Added `locked` NBT tag for snails, allow to entirely lock movement and the inventory to the owner only. 41 | - Added Russian translations ([#9](https://github.com/LambdAurora/lovely_snails/pull/9)). 42 | - Added Canadian French translations. 43 | - Moved the Snail Spawn Egg to be in the same place as Vanilla would put Spawn Eggs in the creative inventory. 44 | - Stopped rendering chests if the snail is a baby. Not naturally possible, but it happened. 45 | 46 | ### 1.1.1 47 | 48 | - Updated to 1.19.3. 49 | - Improved a little the spawn logic code. 50 | 51 | ### 1.1.2 52 | 53 | - Updated to 1.20.1. 54 | - Added moss blocks and moss carpets as valid spawn blocks for snails. 55 | - Added hanging roots, melon seeds, pumpkin seeds, torchflower seeds, and wheat seeds as snail food. 56 | 57 | ### 1.1.3 58 | 59 | - Fixed adult snails appearing small after reconnect due to network desynchronizations 60 | ([#13](https://github.com/LambdAurora/lovely_snails/issues/13), [#14](https://github.com/LambdAurora/lovely_snails/issues/14)). 61 | 62 | ### 1.1.4 (old) 63 | 64 | - Updated to 1.20.2 ([#16](https://github.com/LambdAurora/lovely_snails/pull/16)). 65 | - Fixed more snail client synchronization issues. 66 | 67 | ### 1.1.5 68 | 69 | - Fixed small snails having an over-sized hitbox ([#17](https://github.com/LambdAurora/lovely_snails/issues/17)). 70 | - Improved the despawn prevention of unloaded tamed snails. 71 | 72 | ## 1.2.0 73 | 74 | - Updated to Minecraft 1.21. 75 | - Made snails scared of entities that wear the heads or skulls of monsters. 76 | - Fixed minor texturing mistakes. 77 | 78 | ### 1.2.1 79 | 80 | - Updated to Minecraft 1.21.8 ([#19](https://github.com/LambdAurora/lovely_snails/pull/19)). 81 | - Large Snails can be quad-leashed to Happy Ghasts. 82 | - Bush, Leaf Litter, Short Dry Grass, Tall Dry Grass and Tall Grass can now be fed to snails. 83 | - Updated spawn egg texture. 84 | - Changed minimum sky light level required to allow snail spawning to 6. 85 | - This should improve spawn rates in Dark Forests. 86 | 87 | ### 1.2.2 88 | 89 | - Updated to Minecraft 1.21.10 ([#22](https://github.com/LambdAurora/lovely_snails/pull/22)). 90 | - Added German translations ([#23](https://github.com/LambdAurora/lovely_snails/pull/23)). 91 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/render/SnailChestFeatureRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.render; 11 | 12 | import com.mojang.blaze3d.vertex.MatrixStack; 13 | import com.mojang.math.Axis; 14 | import dev.lambdaurora.lovely_snails.client.LovelySnailsClient; 15 | import dev.lambdaurora.lovely_snails.client.model.SnailModel; 16 | import net.minecraft.client.Minecraft; 17 | import net.minecraft.client.renderer.SubmitNodeCollector; 18 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 19 | import net.minecraft.client.renderer.entity.RenderLayerParent; 20 | import net.minecraft.client.renderer.entity.layers.RenderLayer; 21 | import net.minecraft.client.renderer.item.ItemStackRenderState; 22 | import net.minecraft.client.renderer.texture.OverlayTexture; 23 | import net.minecraft.world.item.ItemDisplayContext; 24 | import net.minecraft.world.item.ItemStack; 25 | 26 | /** 27 | * Renders the chests on a snail. 28 | * 29 | * @author LambdAurora 30 | * @version 1.1.1 31 | * @since 1.0.0 32 | */ 33 | public class SnailChestFeatureRenderer extends RenderLayer { 34 | private final SnailModel model; 35 | 36 | public SnailChestFeatureRenderer(RenderLayerParent featureRendererContext, EntityRendererProvider.Context context) { 37 | super(featureRendererContext); 38 | 39 | this.model = new SnailModel(context.bakeLayer(LovelySnailsClient.SNAIL_MODEL_LAYER)); 40 | } 41 | 42 | @Override 43 | public void submit(MatrixStack matrices, SubmitNodeCollector submitNodeCollector, int light, SnailEntityRenderState state, float tickDelta, float animationProgress) { 44 | if (state.isBaby) return; 45 | 46 | float shellRotation = this.model.getCurrentModel(state).getShell().pitch; 47 | 48 | var rightChest = state.chests[0]; 49 | if (!rightChest.isEmpty()) { 50 | matrices.push(); 51 | matrices.rotate(Axis.XP.rotationDegrees(180)); 52 | matrices.rotate(Axis.XP.rotation(shellRotation)); 53 | matrices.rotate(Axis.YP.rotationDegrees(90)); 54 | matrices.translate(.65, 0.2, -.505); 55 | matrices.scale(1.25f, 1.25f, 1.25f); 56 | renderChest(matrices, submitNodeCollector, state, rightChest); 57 | matrices.pop(); 58 | } 59 | 60 | var backChest = state.chests[1]; 61 | if (!backChest.isEmpty()) { 62 | matrices.push(); 63 | matrices.rotate(Axis.XP.rotationDegrees(180)); 64 | matrices.rotate(Axis.XP.rotation(shellRotation)); 65 | matrices.translate(0, 0.2, -.94); 66 | matrices.scale(1.25f, 1.25f, 1.25f); 67 | renderChest(matrices, submitNodeCollector, state, backChest); 68 | matrices.pop(); 69 | } 70 | 71 | var leftChest = state.chests[2]; 72 | if (!leftChest.isEmpty()) { 73 | matrices.push(); 74 | matrices.rotate(Axis.XP.rotationDegrees(180)); 75 | matrices.rotate(Axis.XP.rotation(shellRotation)); 76 | matrices.rotate(Axis.YN.rotationDegrees(90)); 77 | matrices.translate(-.65, 0.2, -.505); 78 | matrices.scale(1.25f, 1.25f, 1.25f); 79 | renderChest(matrices, submitNodeCollector, state, leftChest); 80 | matrices.pop(); 81 | } 82 | } 83 | 84 | private void renderChest(MatrixStack matrices, SubmitNodeCollector submitNodeCollector, SnailEntityRenderState state, ItemStack chest) { 85 | var itemModelResolver = Minecraft.getInstance().getItemModelResolver(); 86 | 87 | ItemStackRenderState itemStackRenderState = new ItemStackRenderState(); 88 | itemModelResolver.updateForTopItem(itemStackRenderState, chest, ItemDisplayContext.FIXED, null, null, 0); 89 | itemStackRenderState.submit(matrices, submitNodeCollector, state.lightCoords, OverlayTexture.NO_OVERLAY, state.outlineColor); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lovely Snails 2 | 3 | ![Java 21](https://img.shields.io/badge/language-Java%2021-9115ff.svg?style=flat-square) 4 | [![GitHub license](https://img.shields.io/badge/license-Lambda%20License-c7136d?style=flat-square)](https://raw.githubusercontent.com/LambdAurora/lovely_snails/1.20/LICENSE) 5 | ![Environment: Both](https://img.shields.io/badge/environment-both-4caf50?style=flat-square) 6 | [![Mod loader: Fabric]][fabric] 7 | ![Version](https://img.shields.io/github/v/tag/LambdAurora/lovely_snails?label=version&style=flat-square) 8 | 9 | A Minecraft mod which adds some very cute snails. 10 | 11 | ## What's this mod? 12 | 13 | Lovely Snails adds a new entity: the snails. 14 | Snails are cute little creatures which can be tamed by players. 15 | 16 | Snails will get scared by hostile mobs or if they get attacked, they will then hide under their shell. 17 | 18 | Once a snail is tamed with mushrooms, they can have a cute little decor using carpets. 19 | Players can they take care of the snail, if it's well taken care of it mays grow into a big snail. 20 | 21 | Big snails can be ridden, they also can be equipped with chests to offer an inventory. 22 | They also can equip an Ender Chest to offer a portable Ender Chest. 23 | 24 | ## How do I take care of a snail? 25 | 26 | Once tamed you can help your snail to grow to adult size! 27 | 28 | You can feed your snail some grass, fern, or even kelp! 29 | You can also pet it by right-clicking on it. 30 | You can also give it a carpet, the snail will appreciate it even more if it feels cold. 31 | You can also hydrate it by throwing a water splash potion. 32 | 33 | But beware! Snails hate poisonous potatoes. 34 | 35 | ## Pictures 36 | 37 | Some little snails in their natural habitat. 38 | ![Small snails in their natural habitat](assets/small_snails.png) 39 | 40 | Tamed snails! 41 | ![Tamed snails](assets/tamed_snails.png) 42 | 43 | The inventory of a snail. 44 | Each chest represents a storage page, pages can be browsed by scrolling in the storage spage. 45 | ![Inventory](assets/snail_inventory.png) 46 | 47 | [fabric]: https://fabricmc.net 48 | [Mod loader: Fabric]: https://img.shields.io/badge/modloader-Fabric-1976d2?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTE2VDE2OjU0OjE3LTA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wNy0yOFQyMToxNzo0OC0wNzowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0wNy0yOFQyMToxNzo0OC0wNzowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowZWRiMWMyYy1mZjhjLWU0NDEtOTMxZi00OTVkNGYxNGM3NjAiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MGVkYjFjMmMtZmY4Yy1lNDQxLTkzMWYtNDk1ZDRmMTRjNzYwIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MGVkYjFjMmMtZmY4Yy1lNDQxLTkzMWYtNDk1ZDRmMTRjNzYwIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDowZWRiMWMyYy1mZjhjLWU0NDEtOTMxZi00OTVkNGYxNGM3NjAiIHN0RXZ0OndoZW49IjIwMTgtMTItMTZUMTY6NTQ6MTctMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IChXaW5kb3dzKSIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4/HiGMAAAAtUlEQVRYw+XXrQqAMBQF4D2P2eBL+QIG8RnEJFaNBjEum+0+zMQLtwwv+wV3ZzhhMDgfJ0wUSinxZUQWgKos1JP/AbD4OneIDyQPwCFniA+EJ4CaXm4TxAXCC0BNHgLhAdAnx9hC8PwGSRtAFVMQjF7cNTWED8B1cgwW20yfJgAvrssAsZ1cB3g/xckAxr6FmCDU5N6f488BrpCQ4rQBJkiMYh4ACmLzwOQF0CExinkCsvw7vgGikl+OotaKRwAAAABJRU5ErkJggg== 49 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/registry/LovelySnailsRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.registry; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import dev.lambdaurora.lovely_snails.item.SnailSpawnEggItem; 14 | import dev.lambdaurora.lovely_snails.network.SnailScreenHandlerPayload; 15 | import dev.lambdaurora.lovely_snails.screen.SnailScreenHandler; 16 | import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityType; 17 | import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType; 18 | import net.minecraft.core.Registry; 19 | import net.minecraft.core.registries.BuiltInRegistries; 20 | import net.minecraft.core.registries.Registries; 21 | import net.minecraft.resources.ResourceKey; 22 | import net.minecraft.sounds.SoundEvent; 23 | import net.minecraft.tags.TagKey; 24 | import net.minecraft.world.entity.EntityType; 25 | import net.minecraft.world.entity.MobCategory; 26 | import net.minecraft.world.entity.SpawnPlacementTypes; 27 | import net.minecraft.world.inventory.MenuType; 28 | import net.minecraft.world.item.Item; 29 | import net.minecraft.world.item.SpawnEggItem; 30 | import net.minecraft.world.level.biome.Biome; 31 | import net.minecraft.world.level.block.Block; 32 | import net.minecraft.world.level.levelgen.Heightmap; 33 | 34 | import java.util.function.Function; 35 | 36 | import static dev.lambdaurora.lovely_snails.LovelySnails.id; 37 | 38 | /** 39 | * Represents the Lovely Snails' registry. 40 | * 41 | * @author LambdAurora 42 | * @version 1.2.0 43 | * @since 1.0.0 44 | */ 45 | public final class LovelySnailsRegistry { 46 | private LovelySnailsRegistry() { 47 | throw new UnsupportedOperationException("Someone tried to instantiate a class only containing static definitions. How?"); 48 | } 49 | 50 | /* Items */ 51 | 52 | public static final SpawnEggItem SNAIL_SPAWN_EGG_ITEM; 53 | 54 | /* Screen handlers */ 55 | 56 | public static final MenuType SNAIL_SCREEN_HANDLER_TYPE = 57 | Registry.register(BuiltInRegistries.MENU, id("snail"), new ExtendedScreenHandlerType<>( 58 | SnailScreenHandler::new, SnailScreenHandlerPayload.STREAM_CODEC 59 | )); 60 | 61 | /* Entities */ 62 | 63 | public static final EntityType SNAIL_ENTITY_TYPE = Registry.register(BuiltInRegistries.ENTITY_TYPE, id("snail"), 64 | FabricEntityType.Builder.createMob( 65 | SnailEntity::new, MobCategory.CREATURE, builder -> 66 | builder.defaultAttributes(SnailEntity::createSnailAttributes) 67 | .spawnRestriction( 68 | SpawnPlacementTypes.NO_RESTRICTIONS, 69 | Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, 70 | SnailEntity::canSpawn 71 | ) 72 | ) 73 | .sized(1.5f, 2.f) 74 | .eyeHeight(1.f) 75 | .passengerAttachments(2.15f) 76 | .build(ResourceKey.of(Registries.ENTITY_TYPE, id("snail"))) 77 | ); 78 | 79 | /* Sounds */ 80 | 81 | public static final SoundEvent SNAIL_DEATH_SOUND_EVENT = registerSound("entity.lovely_snails.snail.death"); 82 | public static final SoundEvent SNAIL_HURT_SOUND_EVENT = registerSound("entity.lovely_snails.snail.hurt"); 83 | 84 | /* Tags */ 85 | 86 | public static final TagKey SNAIL_SPAWN_BLOCKS = TagKey.of(Registries.BLOCK, id("snail_spawn_blocks")); 87 | public static final TagKey SNAIL_BREEDING_ITEMS = TagKey.of(Registries.ITEM, id("snail_breeding_items")); 88 | public static final TagKey SNAIL_FOOD_ITEMS = TagKey.of(Registries.ITEM, id("snail_food_items")); 89 | public static final TagKey SNAIL_SCARY_ITEMS = TagKey.of(Registries.ITEM, id("snail_scary_items")); 90 | public static final TagKey SNAIL_REGULAR_SPAWN_BIOMES = TagKey.of(Registries.BIOME, id("snail_spawn")); 91 | public static final TagKey SNAIL_SWAMP_LIKE_SPAWN_BIOMES = TagKey.of(Registries.BIOME, id("swamp_like_spawn")); 92 | 93 | private static T register(String name, Function item) { 94 | return Registry.register(BuiltInRegistries.ITEM, id(name), item.apply(new Item.Properties().setId(ResourceKey.of(Registries.ITEM, id(name))))); 95 | } 96 | 97 | private static SoundEvent registerSound(String path) { 98 | var id = id(path); 99 | return Registry.register(BuiltInRegistries.SOUND_EVENT, id, SoundEvent.createVariableRangeEvent(id)); 100 | } 101 | 102 | public static void init() {} 103 | 104 | static { 105 | SNAIL_SPAWN_EGG_ITEM = register("snail_spawn_egg", 106 | (properties) -> new SnailSpawnEggItem(SNAIL_ENTITY_TYPE, properties) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "Checkout" 13 | uses: actions/checkout@v4 14 | - name: "Set up Java" 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: "temurin" 18 | java-version: 21 19 | - name: "Set up Gradle" 20 | uses: gradle/actions/setup-gradle@v4 21 | - name: "Handle Loom Cache" 22 | uses: actions/cache@v4 23 | with: 24 | path: "**/.gradle/loom-cache" 25 | key: "${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}" 26 | restore-keys: "${{ runner.os }}-gradle-" 27 | - name: "Build with Gradle" 28 | run: ./gradlew build packageModrinth --parallel --stacktrace 29 | env: 30 | MODRINTH_TOKEN: "dummy" 31 | - name: "Upload artifacts" 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: Artifacts 35 | path: ./build/libs/ 36 | - name: "Upload Modrinth artifacts" 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: Modrinth 40 | path: ./build/modrinth.zip 41 | retention-days: 1 42 | 43 | github: 44 | runs-on: ubuntu-latest 45 | needs: build 46 | permissions: 47 | contents: write 48 | steps: 49 | - name: "Download artifacts" 50 | uses: actions/download-artifact@v4 51 | with: 52 | name: Modrinth 53 | path: dist 54 | - name: "Extract files" 55 | run: unzip dist/modrinth.zip 56 | - id: files 57 | name: "Read which files to upload" 58 | uses: actions/github-script@v8 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { default: fs } = await import("node:fs/promises"); 63 | const json = JSON.parse(await fs.readFile("manifest.json", { encoding: "utf8" })); 64 | const response = await octokit.rest.repos.createRelease({ 65 | owner: context.repo.owner, 66 | repo: context.repo.repo, 67 | tag_name: "${{ github.ref_name }}", 68 | name: json.name, 69 | body: json.changelog, 70 | prerelease: json.type !== "release", 71 | }); 72 | const { data: { id: releaseId, html_url: htmlUrl, upload_url: uploadUrl } } = response; 73 | return json.files.map(it => `"${it}"`).join(" "); 74 | - name: "Upload artifacts to GitHub release" 75 | run: | 76 | gh release upload "${{ github.ref_name }}" ${{ steps.files.outputs.result }} --repo "${{ github.repository }}" 77 | env: 78 | GH_TOKEN: ${{ github.token }} 79 | 80 | modrinth: 81 | runs-on: ubuntu-latest 82 | needs: build 83 | steps: 84 | - name: "Download artifacts" 85 | uses: actions/download-artifact@v4 86 | with: 87 | name: Modrinth 88 | path: dist 89 | - name: "Extract files" 90 | run: unzip dist/modrinth.zip 91 | - name: "Release on Modrinth" 92 | uses: LambdAurora/modrinth_publish@v0.1.5 93 | with: 94 | token: ${{ secrets.MODRINTH_TOKEN }} 95 | project: "hBVVhStr" 96 | manifest: "manifest.json" 97 | readme_file: "README.md" 98 | 99 | curseforge: 100 | runs-on: ubuntu-latest 101 | needs: build 102 | steps: 103 | - name: "Download artifacts" 104 | uses: actions/download-artifact@v4 105 | with: 106 | name: Modrinth 107 | path: dist 108 | - name: "Extract files" 109 | run: unzip dist/modrinth.zip 110 | - id: manifest 111 | name: "Extract manifest data" 112 | uses: actions/github-script@v8 113 | with: 114 | script: | 115 | const { default: fs } = await import("node:fs/promises"); 116 | const json = JSON.parse(await fs.readFile("manifest.json", { encoding: "utf8" })); 117 | core.setOutput("version", json.version); 118 | core.setOutput("type", json.type); 119 | core.setOutput("name", json.name); 120 | core.setOutput("game_versions", json.game_versions.join("\n")); 121 | core.setOutput("loaders", json.loaders.join("\n")); 122 | await fs.writeFile("CHANGELOG.md", json.changelog); 123 | - uses: Kir-Antipov/mc-publish@v3.3 124 | with: 125 | curseforge-id: 499425 126 | curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} 127 | 128 | files: ./*.jar 129 | 130 | name: ${{ steps.manifest.outputs.name }} 131 | version: ${{ steps.manifest.outputs.version }} 132 | version-type: ${{ steps.manifest.outputs.type }} 133 | changelog-file: CHANGELOG.* 134 | 135 | loaders: ${{ steps.manifest.outputs.loaders }} 136 | game-versions: ${{ steps.manifest.outputs.game_versions }} 137 | game-version-filter: none 138 | dependencies: | 139 | fabric-api(required) 140 | java: | 141 | 21 142 | 22 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Lambda License 2 | 3 | Copyright (c) 2025 LambdAurora 4 | 5 | 6 | # 0. Foreword 7 | 8 | The Lambda License is an open license meant to better protect the rights of 9 | software authors. It does not adhere to the usual definition of "Free Open 10 | Source Software" (FOSS), but rather we aim to strike a middle ground between 11 | the freedom of consumers and the protection of the authors' work. 12 | 13 | We believe traditional FOSS licenses allow companies or other individuals to 14 | exploit software authors freely by having their work shared in a way they 15 | don't agree with or that is detrimental to their business. This license aims 16 | to give control of artifacts back to the authors and support an open code 17 | community, while still allowing source code to be easily modified and shared. 18 | 19 | In short, you may share or modify the source code as you wish, as long as the 20 | work stays under this license, and if you have modified the code, you must: 21 | (a) State your changes if you don't use an SVC like Git 22 | (b) List the original authors (as listed in the copyright at the top of this 23 | license) 24 | 25 | When sharing binaries, you must adhere to a few more rules. In particular, you 26 | will need written approval from the original authors. If they don't answer you 27 | after 60 days, they are considered to be implicitly agreeing. Please note that 28 | you have to request in good faith, spam won't be accepted. You will also need 29 | to make the source code available and document where to find it. 30 | 31 | If you are running a modified version of this software on your servers and 32 | users can freely interact with it, you will need to make your source code 33 | public the same way. 34 | 35 | The precise terms and conditions can be found below: 36 | 37 | 38 | # 1. Definitions 39 | 40 | 1.1. "Source Code": The Source Code is any digital files or tools that are 41 | part of the work covered by this text and/or used in the creation of Binaries, 42 | preferred for making modifications. This does not include any publicly 43 | available tools or libraries. This may include textual or binary digital 44 | files. 45 | 46 | 1.2. "Binaries": Binaries correspond to any form of the work other than the 47 | Source Code. 48 | 49 | 1.3. "This License": This License corresponds to the full text of this 50 | document. 51 | 52 | 1.4. "Copyright Notice": The Copyright Notice corresponds to the second line 53 | of This License, starting with "Copyright (c)". 54 | 55 | 1.5. "Project Owner": The Project Owners are all physical persons or 56 | legal entities mentioned in the Copyright Notice. They may be referred to by 57 | an alias. 58 | 59 | 1.6. "You": "You" corresponds to an individual or a legal entity exercising 60 | rights under this License. For legal entities, "You" includes any entity that 61 | controls, is controlled by, or is under common control with You. For purposes 62 | of this definition, "control" means (a) the power, direct or indirect, to 63 | cause the direction or management of such an entity, whether by contract or 64 | otherwise, or (b) ownership of more than fifty percent (50%) of the 65 | outstanding shares or beneficial ownership of such an entity. 66 | 67 | 68 | # 2. Using the Source Code 69 | 70 | You may copy, modify, merge, publish, distribute, and/or sell copies of the 71 | Source Code under the following conditions: 72 | 73 | 2.1. The Source Code and any derivative work of the Source Code are licensed 74 | under This License. 75 | 76 | 2.2. You must explicitly state your changes, if any, in a prominent place. 77 | 2.2.1. Commit history as provided by source version control software programs 78 | is considered to fulfill 2.2. 79 | 80 | 2.3. If you modify the Source Code in any way, You must list the original 81 | Project Owners in a prominent place. 82 | 2.3.1. Commit history as provided by source version control software programs 83 | is NOT considered to fulfill 2.3. 84 | 85 | 2.4. You may add your name to the Copyright Notice, but You must not remove 86 | any name from the existing Copyright Notice. 87 | 88 | 89 | # 3. Using Binaries 90 | 91 | You may share, publish, distribute, make derivative work, and/or sell copies 92 | of Binaries, or license the right to do so, under the following conditions: 93 | 94 | 3.1. The Binaries and any derivative work of the Binaries are licensed under 95 | This License. 96 | 97 | 3.2. You must make the Source Code used to create the Binaries publicly 98 | available, free of charge. 99 | 3.2.1. You must clearly document in the Binaries, and respond to requests 100 | asking how to obtain the aforementioned Source Code. 101 | 102 | 3.3. You must obtain written approval from all Project Owners to use their 103 | work as mentioned above. 104 | 3.3.1. This authorization is free of charge, royalty-free, global, 105 | non-exclusive, revocable and non-transferable. 106 | 3.3.1.1. Revocation can be pronounced at any time (known as the "Revocation 107 | Date") by any Project Owner, upon written notification to You. 108 | 3.3.1.2. If and when revocation is pronounced, You cannot make use of any 109 | rights under Section 3 for any and all work made after the Revocation Date. 110 | The revocation does not retroactively apply. 111 | 3.3.2. The Project Owner may impose any additional clause on their 112 | authorization within the limits provided in 3.3.1. 113 | 3.3.3. If a Project Owner fails to reply within 60 calendar days upon 114 | receiving a good-faith written request sent through their primary email, 115 | an irrevocable authorization from that Project Owner is implicitly granted. 116 | 3.3.3.1. A written request made within 180 calendar days upon receiving a negative 117 | answer is considered implicitly rejected and does not qualify for 3.3.3. 118 | 119 | 120 | # 4. Remote network interaction 121 | 122 | If you offer users the ability to interact remotely through a computer network 123 | with a Binary created from a modification of the Source Code, you must make 124 | the modified Source Code publicly available, free of charge. You must also 125 | clearly document and respond to requests asking how to obtain the 126 | aforementioned Source Code. 127 | 128 | 129 | # 5. Disclaimer 130 | 131 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 132 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 133 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 134 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 135 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 136 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 137 | SOFTWARE. 138 | 139 | In case of a conflict between the Section 0 ("Foreword") and any other Section, the latter 140 | Section shall prevail. -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/model/SnailModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.model; 11 | 12 | import com.mojang.blaze3d.vertex.MatrixStack; 13 | import dev.lambdaurora.lovely_snails.client.render.SnailEntityRenderState; 14 | import net.minecraft.client.model.EntityModel; 15 | import net.minecraft.client.model.geom.ModelPart; 16 | import net.minecraft.client.model.geom.PartPose; 17 | import net.minecraft.client.model.geom.builders.*; 18 | 19 | import static net.minecraft.client.model.geom.PartNames.*; 20 | 21 | /** 22 | * Represents the snail entity model. 23 | * 24 | * @author LambdAurora 25 | * @version 1.2.0 26 | * @since 1.0.0 27 | */ 28 | public class SnailModel extends EntityModel { 29 | public static final String SHELL = "shell"; 30 | 31 | public static final float ADULT_SHELL_ROTATION = -0.0436f; 32 | private static final float ADULT_FRONT_WIDTH = 12.f; 33 | private static final float ADULT_SHELL_DIAMETER = 16.f; 34 | private static final float ADULT_EYE_YAW = 0.1745f; 35 | private static final float ADULT_EYE_LENGTH = 12.f; 36 | private static final float ADULT_EYE_DIAMETER = 2.f; 37 | 38 | public static final float BABY_SHELL_ROTATION = -0.080f; 39 | private static final float BABY_FRONT_WIDTH = 4.f; 40 | private static final float BABY_SHELL_DIAMETER = 10.f; 41 | private static final float BABY_EYE_YAW = ADULT_EYE_YAW; 42 | private static final float BABY_EYE_LENGTH = 7.f; 43 | private static final float BABY_EYE_DIAMETER = 1.f; 44 | 45 | private final Model adultModel; 46 | private final Model babyModel; 47 | 48 | public SnailModel(ModelPart root) { 49 | super(root); 50 | this.adultModel = new Model(root.getChild("adult"), ADULT_SHELL_ROTATION); 51 | this.babyModel = new Model(root.getChild("baby"), BABY_SHELL_ROTATION); 52 | } 53 | 54 | public static LayerDefinition model(CubeDeformation deformation) { 55 | var modelData = new MeshDefinition(); 56 | var root = modelData.getRoot(); 57 | buildAdultModel(root.addOrReplaceChild("adult", new ModelPartBuilder(), PartPose.ZERO), deformation); 58 | buildBabyModel(root.addOrReplaceChild("baby", new ModelPartBuilder(), PartPose.ZERO), deformation); 59 | return LayerDefinition.create(modelData, 128, 96); 60 | } 61 | 62 | private static void buildAdultModel(PartDefinition root, CubeDeformation deformation) { 63 | var body = root.addOrReplaceChild(BODY, new ModelPartBuilder() 64 | .uv(0, 32) 65 | .cuboid(-(ADULT_FRONT_WIDTH / 2.f), 5.f, -20.f, ADULT_FRONT_WIDTH, 3.f, 40.f, deformation), 66 | PartPose.offset(0.f, 16.f, -2.f)); 67 | var upperBody = body.addOrReplaceChild("upper_body", new ModelPartBuilder() 68 | .uv(64, 16) 69 | .cuboid(-(ADULT_FRONT_WIDTH / 2.f), -7.f, 0.f, ADULT_FRONT_WIDTH, 12.f, 8.f, 70 | deformation), 71 | PartPose.offset(0.f, 0.f, -20.f)); 72 | upperBody.addOrReplaceChild("left_tentacle", new ModelPartBuilder() 73 | .uv(0, 2) 74 | .cuboid(-ADULT_FRONT_WIDTH / 2.f, 0.f, -2.f, 4.f, 4.f, 2.f, deformation), 75 | PartPose.ZERO 76 | ); 77 | upperBody.addOrReplaceChild("right_tentacle", new ModelPartBuilder() 78 | .uv(0, 2) 79 | .mirrored() 80 | .cuboid(ADULT_FRONT_WIDTH / 2.f - 4.f, 0.f, -2.f, 4.f, 4.f, 2.f, deformation), 81 | PartPose.ZERO 82 | ); 83 | 84 | root.addOrReplaceChild(SHELL, new ModelPartBuilder() 85 | .cuboid(-(ADULT_FRONT_WIDTH / 2.f), 0.f, -2.f, ADULT_FRONT_WIDTH, ADULT_SHELL_DIAMETER, ADULT_SHELL_DIAMETER, 86 | deformation.extend(4.f, 8.f, 8.f), 87 | 1.f, 1.f), 88 | PartPose.offsetAndRotation(0.f, -2.f, -5.f, ADULT_SHELL_ROTATION, 0.f, 0.f)); 89 | 90 | body.addOrReplaceChild(LEFT_EYE, new ModelPartBuilder() 91 | .uv(42, 0) 92 | .cuboid(-2.8336f, -15.849f, -3.8272f, ADULT_EYE_DIAMETER, ADULT_EYE_LENGTH, ADULT_EYE_DIAMETER, deformation), 93 | PartPose.offsetAndRotation(-1.5f, -4.f, -15f, 0.4363f, ADULT_EYE_YAW, 0.f)); 94 | body.addOrReplaceChild(RIGHT_EYE, new ModelPartBuilder() 95 | .uv(42, 0) 96 | .mirrored() 97 | .cuboid(0.8336f, -15.849f, -3.8272f, ADULT_EYE_DIAMETER, ADULT_EYE_LENGTH, ADULT_EYE_DIAMETER, deformation), 98 | PartPose.offsetAndRotation(1.5f, -4.f, -15f, 0.4363f, -ADULT_EYE_YAW, 0.f)); 99 | } 100 | 101 | private static void buildBabyModel(PartDefinition babyRoot, CubeDeformation deformation) { 102 | var body = babyRoot.addOrReplaceChild(BODY, new ModelPartBuilder() 103 | .uv(56, 0) 104 | .cuboid(-(BABY_FRONT_WIDTH / 2.f), 22.f, -7.f, BABY_FRONT_WIDTH, 2.f, 14.f, deformation) 105 | .uv(0, 10) 106 | .cuboid(-(BABY_FRONT_WIDTH / 2.f), 20.f, -7.f, BABY_FRONT_WIDTH, 2.f, 4.f, deformation) 107 | .uv(0, 0) 108 | .cuboid(-(BABY_FRONT_WIDTH / 2.f), 22.f, -8.f, 1.f, 1.f, 1.f, deformation) 109 | .cuboid(BABY_FRONT_WIDTH / 2.f - 1.f, 22.f, -8.f, 1.f, 1.f, 1.f, deformation), 110 | PartPose.offset(0, 0, -2.f)); 111 | babyRoot.addOrReplaceChild(SHELL, new ModelPartBuilder() 112 | .uv(0, 32) 113 | .cuboid(-3.f, 10.f, -1.f, 6.f, BABY_SHELL_DIAMETER, BABY_SHELL_DIAMETER, deformation), 114 | PartPose.offsetAndRotation(0.f, 2.2f, -3.f, BABY_SHELL_ROTATION, 0.f, 0.f)); 115 | body.addOrReplaceChild(LEFT_EYE, new ModelPartBuilder() 116 | .uv(0, 32) 117 | .cuboid(-1.1664f, 19.f, -3.8272f, BABY_EYE_DIAMETER, BABY_EYE_LENGTH, BABY_EYE_DIAMETER, deformation), 118 | PartPose.offsetAndRotation(-1.5f, -4.f, -14.2f, 0.4363f, BABY_EYE_YAW, 0.f)); 119 | body.addOrReplaceChild(RIGHT_EYE, new ModelPartBuilder() 120 | .uv(0, 32) 121 | .mirrored() 122 | .cuboid(0.1664f, 19.f, -3.8272f, BABY_EYE_DIAMETER, BABY_EYE_LENGTH, BABY_EYE_DIAMETER, deformation), 123 | PartPose.offsetAndRotation(1.5f, -4.f, -14.2f, 0.4363f, -BABY_EYE_YAW, 0.f)); 124 | } 125 | 126 | public Model getCurrentModel(SnailEntityRenderState state) { 127 | return state.isBaby ? this.babyModel : this.adultModel; 128 | } 129 | 130 | @Override 131 | public void setupAnim(SnailEntityRenderState entity) { 132 | super.setupAnim(entity); 133 | var model = this.getCurrentModel(entity); 134 | model.root.visible = true; 135 | (entity.isBaby ? this.adultModel : this.babyModel).root.visible = false; 136 | if (entity.isScared) model.hideSnail(); 137 | else model.uncover(); 138 | } 139 | 140 | public static class Model { 141 | private final ModelPart root; 142 | private final ModelPart body; 143 | private final ModelPart shell; 144 | private final float idleShellYaw; 145 | 146 | public Model(ModelPart root, float idleShellYaw) { 147 | this.root = root; 148 | this.idleShellYaw = idleShellYaw; 149 | this.body = root.getChild(BODY); 150 | this.shell = root.getChild(SHELL); 151 | } 152 | 153 | /** 154 | * Returns the shell of the snail. 155 | * 156 | * @return the shell 157 | */ 158 | public ModelPart getShell() { 159 | return this.shell; 160 | } 161 | 162 | /** 163 | * Puts the snail in hiding. 164 | */ 165 | public void hideSnail() { 166 | this.body.visible = false; 167 | this.getShell().setRotation(0.f, 0.f, 0.f); 168 | } 169 | 170 | /** 171 | * Puts the snail in idle position. 172 | */ 173 | public void uncover() { 174 | this.body.visible = true; 175 | this.getShell().setRotation(this.idleShellYaw, 0.f, 0.f); 176 | } 177 | 178 | public void updateMatrix(MatrixStack matrices) { 179 | if (!this.body.visible) matrices.translate(0, 2.f / 16.f, 0); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /LICENSE.OLD: -------------------------------------------------------------------------------- 1 | Commits before 2b911551efb020857df8f5fc1307a2cada8bfffb included were licensed under LGPLv3, full text below. 2 | 3 | Any further contribution will be made under the text inside the LICENSE file. 4 | 5 | --- 6 | 7 | GNU LESSER GENERAL PUBLIC LICENSE 8 | Version 3, 29 June 2007 9 | 10 | Copyright (C) 2007 Free Software Foundation, Inc. 11 | Everyone is permitted to copy and distribute verbatim copies 12 | of this license document, but changing it is not allowed. 13 | 14 | 15 | This version of the GNU Lesser General Public License incorporates 16 | the terms and conditions of version 3 of the GNU General Public 17 | License, supplemented by the additional permissions listed below. 18 | 19 | 0. Additional Definitions. 20 | 21 | As used herein, "this License" refers to version 3 of the GNU Lesser 22 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 23 | General Public License. 24 | 25 | "The Library" refers to a covered work governed by this License, 26 | other than an Application or a Combined Work as defined below. 27 | 28 | An "Application" is any work that makes use of an interface provided 29 | by the Library, but which is not otherwise based on the Library. 30 | Defining a subclass of a class defined by the Library is deemed a mode 31 | of using an interface provided by the Library. 32 | 33 | A "Combined Work" is a work produced by combining or linking an 34 | Application with the Library. The particular version of the Library 35 | with which the Combined Work was made is also called the "Linked 36 | Version". 37 | 38 | The "Minimal Corresponding Source" for a Combined Work means the 39 | Corresponding Source for the Combined Work, excluding any source code 40 | for portions of the Combined Work that, considered in isolation, are 41 | based on the Application, and not on the Linked Version. 42 | 43 | The "Corresponding Application Code" for a Combined Work means the 44 | object code and/or source code for the Application, including any data 45 | and utility programs needed for reproducing the Combined Work from the 46 | Application, but excluding the System Libraries of the Combined Work. 47 | 48 | 1. Exception to Section 3 of the GNU GPL. 49 | 50 | You may convey a covered work under sections 3 and 4 of this License 51 | without being bound by section 3 of the GNU GPL. 52 | 53 | 2. Conveying Modified Versions. 54 | 55 | If you modify a copy of the Library, and, in your modifications, a 56 | facility refers to a function or data to be supplied by an Application 57 | that uses the facility (other than as an argument passed when the 58 | facility is invoked), then you may convey a copy of the modified 59 | version: 60 | 61 | a) under this License, provided that you make a good faith effort to 62 | ensure that, in the event an Application does not supply the 63 | function or data, the facility still operates, and performs 64 | whatever part of its purpose remains meaningful, or 65 | 66 | b) under the GNU GPL, with none of the additional permissions of 67 | this License applicable to that copy. 68 | 69 | 3. Object Code Incorporating Material from Library Header Files. 70 | 71 | The object code form of an Application may incorporate material from 72 | a header file that is part of the Library. You may convey such object 73 | code under terms of your choice, provided that, if the incorporated 74 | material is not limited to numerical parameters, data structure 75 | layouts and accessors, or small macros, inline functions and templates 76 | (ten or fewer lines in length), you do both of the following: 77 | 78 | a) Give prominent notice with each copy of the object code that the 79 | Library is used in it and that the Library and its use are 80 | covered by this License. 81 | 82 | b) Accompany the object code with a copy of the GNU GPL and this license 83 | document. 84 | 85 | 4. Combined Works. 86 | 87 | You may convey a Combined Work under terms of your choice that, 88 | taken together, effectively do not restrict modification of the 89 | portions of the Library contained in the Combined Work and reverse 90 | engineering for debugging such modifications, if you also do each of 91 | the following: 92 | 93 | a) Give prominent notice with each copy of the Combined Work that 94 | the Library is used in it and that the Library and its use are 95 | covered by this License. 96 | 97 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 98 | document. 99 | 100 | c) For a Combined Work that displays copyright notices during 101 | execution, include the copyright notice for the Library among 102 | these notices, as well as a reference directing the user to the 103 | copies of the GNU GPL and this license document. 104 | 105 | d) Do one of the following: 106 | 107 | 0) Convey the Minimal Corresponding Source under the terms of this 108 | License, and the Corresponding Application Code in a form 109 | suitable for, and under terms that permit, the user to 110 | recombine or relink the Application with a modified version of 111 | the Linked Version to produce a modified Combined Work, in the 112 | manner specified by section 6 of the GNU GPL for conveying 113 | Corresponding Source. 114 | 115 | 1) Use a suitable shared library mechanism for linking with the 116 | Library. A suitable mechanism is one that (a) uses at run time 117 | a copy of the Library already present on the user's computer 118 | system, and (b) will operate properly with a modified version 119 | of the Library that is interface-compatible with the Linked 120 | Version. 121 | 122 | e) Provide Installation Information, but only if you would otherwise 123 | be required to provide such information under section 6 of the 124 | GNU GPL, and only to the extent that such information is 125 | necessary to install and execute a modified version of the 126 | Combined Work produced by recombining or relinking the 127 | Application with a modified version of the Linked Version. (If 128 | you use option 4d0, the Installation Information must accompany 129 | the Minimal Corresponding Source and Corresponding Application 130 | Code. If you use option 4d1, you must provide the Installation 131 | Information in the manner specified by section 6 of the GNU GPL 132 | for conveying Corresponding Source.) 133 | 134 | 5. Combined Libraries. 135 | 136 | You may place library facilities that are a work based on the 137 | Library side by side in a single library together with other library 138 | facilities that are not Applications and are not covered by this 139 | License, and convey such a combined library under terms of your 140 | choice, if you do both of the following: 141 | 142 | a) Accompany the combined library with a copy of the same work based 143 | on the Library, uncombined with any other library facilities, 144 | conveyed under the terms of this License. 145 | 146 | b) Give prominent notice with the combined library that part of it 147 | is a work based on the Library, and explaining where to find the 148 | accompanying uncombined form of the same work. 149 | 150 | 6. Revised Versions of the GNU Lesser General Public License. 151 | 152 | The Free Software Foundation may publish revised and/or new versions 153 | of the GNU Lesser General Public License from time to time. Such new 154 | versions will be similar in spirit to the present version, but may 155 | differ in detail to address new problems or concerns. 156 | 157 | Each version is given a distinguishing version number. If the 158 | Library as you received it specifies that a certain numbered version 159 | of the GNU Lesser General Public License "or any later version" 160 | applies to it, you have the option of following the terms and 161 | conditions either of that published version or of any later version 162 | published by the Free Software Foundation. If the Library as you 163 | received it does not specify a version number of the GNU Lesser 164 | General Public License, you may choose any version of the GNU Lesser 165 | General Public License ever published by the Free Software Foundation. 166 | 167 | If the Library as you received it specifies that a proxy can decide 168 | whether future versions of the GNU Lesser General Public License shall 169 | apply, that proxy's public statement of acceptance of any version is 170 | permanent authorization for you to choose that version for the 171 | Library. -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/client/screen/SnailInventoryScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.client.screen; 11 | 12 | import dev.lambdaurora.lovely_snails.LovelySnails; 13 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 14 | import dev.lambdaurora.lovely_snails.network.SnailSetStoragePagePayload; 15 | import dev.lambdaurora.lovely_snails.screen.SnailScreenHandler; 16 | import net.fabricmc.api.EnvType; 17 | import net.fabricmc.api.Environment; 18 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 19 | import net.minecraft.client.Minecraft; 20 | import net.minecraft.client.gui.GuiGraphics; 21 | import net.minecraft.client.gui.components.ImageButton; 22 | import net.minecraft.client.gui.components.WidgetSprites; 23 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 24 | import net.minecraft.client.gui.screens.inventory.InventoryScreen; 25 | import net.minecraft.client.renderer.RenderPipelines; 26 | import net.minecraft.client.resources.sounds.SimpleSoundInstance; 27 | import net.minecraft.client.sounds.SoundManager; 28 | import net.minecraft.network.chat.Text; 29 | import net.minecraft.resources.Identifier; 30 | import net.minecraft.sounds.SoundEvents; 31 | import net.minecraft.util.math.MathHelper; 32 | import net.minecraft.world.Container; 33 | import net.minecraft.world.ContainerListener; 34 | import net.minecraft.world.entity.EquipmentSlot; 35 | import net.minecraft.world.entity.player.Inventory; 36 | 37 | /** 38 | * Represents the snail inventory screen. 39 | * 40 | * @author LambdAurora 41 | * @version 1.2.0 42 | * @since 1.0.0 43 | */ 44 | @Environment(EnvType.CLIENT) 45 | public class SnailInventoryScreen extends AbstractContainerScreen { 46 | private static final Identifier TEXTURE = LovelySnails.id("textures/gui/container/snail.png"); 47 | private static final WidgetSprites ENDER_CHEST_SPRITES = new WidgetSprites( 48 | LovelySnails.id("container/snail/ender_chest"), 49 | LovelySnails.id("container/snail/ender_chest_highlighted") 50 | ); 51 | private static final WidgetSprites[] PAGE_TAB_SPRITES = new WidgetSprites[]{ 52 | new WidgetSprites( 53 | LovelySnails.id("container/snail/tab/1"), 54 | LovelySnails.id("container/snail/tab/1_highlighted") 55 | ), 56 | new WidgetSprites( 57 | LovelySnails.id("container/snail/tab/2"), 58 | LovelySnails.id("container/snail/tab/2_highlighted") 59 | ), 60 | new WidgetSprites( 61 | LovelySnails.id("container/snail/tab/3"), 62 | LovelySnails.id("container/snail/tab/3_highlighted") 63 | ), 64 | }; 65 | private final SnailEntity entity; 66 | private float mouseX; 67 | private float mouseY; 68 | private EnderChestButton enderChestButton; 69 | private final PageButton[] pageButtons = new PageButton[3]; 70 | 71 | public SnailInventoryScreen(SnailScreenHandler handler, Inventory inventory, Text title) { 72 | super(handler, inventory, handler.snail().getDisplayName()); 73 | this.imageWidth += 19; 74 | this.entity = handler.snail(); 75 | } 76 | 77 | private void clearListeners() { 78 | if (this.enderChestButton != null) { 79 | this.getMenu().getInventory().removeListener(this.enderChestButton); 80 | } 81 | this.enderChestButton = null; 82 | 83 | for (int page = 0; page < 3; page++) { 84 | if (this.pageButtons[page] != null) { 85 | this.getMenu().getInventory().removeListener(this.pageButtons[page]); 86 | this.getMenu().removePageChangeListener(this.pageButtons[page]); 87 | } 88 | 89 | this.pageButtons[page] = null; 90 | } 91 | } 92 | 93 | @Override 94 | protected void init() { 95 | super.init(); 96 | this.clearListeners(); 97 | 98 | int x = (this.width - this.imageWidth) / 2; 99 | int y = (this.height - this.imageHeight) / 2; 100 | this.addRenderableWidget(this.enderChestButton = new EnderChestButton(x + 7 + 18, y + 35 + 18)); 101 | this.getMenu().getInventory().addListener(this.enderChestButton); 102 | 103 | int buttonX = x + this.imageWidth - 3; 104 | int buttonY = y + 17; 105 | for (int page = 0; page < PAGE_TAB_SPRITES.length; page++) { 106 | this.addRenderableWidget(this.pageButtons[page] = new PageButton(buttonX, buttonY, page)); 107 | this.getMenu().getInventory().addListener(this.pageButtons[page]); 108 | this.getMenu().addPageChangeListener(this.pageButtons[page]); 109 | } 110 | } 111 | 112 | @Override 113 | public void removed() { 114 | super.removed(); 115 | this.clearListeners(); 116 | } 117 | 118 | @Override 119 | public void onClose() { 120 | super.onClose(); 121 | this.clearListeners(); 122 | } 123 | 124 | /** 125 | * Requests the server to switch to the given storage page. 126 | * 127 | * @param page the storage page to switch to 128 | */ 129 | public void requestStoragePage(int page) { 130 | ClientPlayNetworking.send(new SnailSetStoragePagePayload(this.menu.syncId, (byte) page)); 131 | } 132 | 133 | /* Input */ 134 | 135 | @Override 136 | public boolean mouseScrolled(double mouseX, double mouseY, double amountX, double amountY) { 137 | int x = (this.width - this.imageWidth) / 2; 138 | int y = (this.height - this.imageHeight) / 2; 139 | if (mouseX > x + 98 && mouseY > y + 17 && mouseX <= x + 98 + 5 * 18 && mouseY <= y + 17 + 54) { 140 | int oldPage = this.getMenu().getCurrentStoragePage(); 141 | int newPage = MathHelper.clamp(oldPage + (amountY > 0 ? -1 : 1), 0, 2); 142 | if (oldPage == newPage) 143 | return true; 144 | 145 | if (!this.getMenu().hasChest(newPage)) { 146 | int otherNewPage = MathHelper.clamp(newPage + (amountY > 0 ? -1 : 1), 0, 2); 147 | if (newPage == otherNewPage || !this.getMenu().hasChest(otherNewPage)) 148 | return true; 149 | 150 | newPage = otherNewPage; 151 | } 152 | 153 | this.requestStoragePage(newPage); 154 | return true; 155 | } 156 | return super.mouseScrolled(mouseX, mouseY, amountX, amountY); 157 | } 158 | 159 | /* Rendering */ 160 | 161 | @Override 162 | protected void renderBackground(GuiGraphics graphics, float delta, int mouseX, int mouseY) { 163 | int x = (this.width - this.imageWidth) / 2; 164 | int y = (this.height - this.imageHeight) / 2; 165 | graphics.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 256, 256); 166 | 167 | if (this.entity.canUseSlot(EquipmentSlot.SADDLE)) { 168 | graphics.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, x + 7 + 18, y + 35 - 18, 18, this.imageHeight + 54, 18, 18, 256, 256); 169 | } 170 | 171 | graphics.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, x + 7 + 18, y + 35, 36, this.imageHeight + 54, 18, 18, 256, 256); 172 | 173 | if (!this.entity.isBaby()) { 174 | for (int row = y + 17; row <= y + 35 + 18; row += 18) { 175 | graphics.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, x + 7, row, 54, this.imageHeight + 54, 18, 18, 256, 256); 176 | } 177 | } 178 | 179 | if (this.getMenu().hasChests()) { 180 | graphics.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, x + 98, y + 17, 0, this.imageHeight, 5 * 18, 54, 256, 256); 181 | } 182 | 183 | InventoryScreen.renderEntityInInventoryFollowsMouse( 184 | graphics, 185 | x + 40, y + 8, 186 | x + 100, y + 70, 187 | 17, 0.35f, 188 | this.mouseX, this.mouseY, 189 | this.entity 190 | ); 191 | } 192 | 193 | @Override 194 | public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { 195 | this.mouseX = mouseX; 196 | this.mouseY = mouseY; 197 | super.render(graphics, mouseX, mouseY, delta); 198 | this.renderTooltip(graphics, mouseX, mouseY); 199 | } 200 | 201 | private class EnderChestButton extends ImageButton implements ContainerListener { 202 | public EnderChestButton(int x, int y) { 203 | super(x, y, 18, 18, ENDER_CHEST_SPRITES, 204 | btn -> { 205 | var client = Minecraft.getInstance(); 206 | var screenHandler = SnailInventoryScreen.this.getMenu(); 207 | client.gameMode.handleInventoryButtonClick(screenHandler.syncId, 0); 208 | } 209 | ); 210 | } 211 | 212 | @Override 213 | public void playDownSound(SoundManager soundManager) { 214 | var snail = SnailInventoryScreen.this.getMenu().snail(); 215 | soundManager.play(SimpleSoundInstance.forUI(SoundEvents.ENDER_CHEST_OPEN, snail.getRandom().nextFloat() * .1f + .9f, .5f)); 216 | } 217 | 218 | @Override 219 | public void onContainerChanged(Container sender) { 220 | this.visible = this.active = SnailInventoryScreen.this.getMenu().hasEnderChest(); 221 | } 222 | } 223 | 224 | private class PageButton extends ImageButton implements ContainerListener, SnailScreenHandler.InventoryPageChangeListener { 225 | private final int page; 226 | 227 | public PageButton(int x, int y, int page) { 228 | super(x, y + page * 18 + 1, 15, 16, PAGE_TAB_SPRITES[page], 229 | btn -> { 230 | SnailInventoryScreen.this.requestStoragePage(page); 231 | } 232 | ); 233 | this.page = page; 234 | 235 | this.visible = SnailInventoryScreen.this.getMenu().hasChest(this.page); 236 | this.onCurrentPageSet(SnailInventoryScreen.this.getMenu().getCurrentStoragePage()); 237 | } 238 | 239 | @Override 240 | public void onContainerChanged(Container sender) { 241 | this.visible = SnailInventoryScreen.this.getMenu().hasChest(page); 242 | } 243 | 244 | @Override 245 | public void onCurrentPageSet(int page) { 246 | this.active = this.page != page; 247 | this.setFocused(this.page == page); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/screen/SnailScreenHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.screen; 11 | 12 | import dev.lambdaurora.lovely_snails.entity.SnailEntity; 13 | import dev.lambdaurora.lovely_snails.item.EquipmentContainer; 14 | import dev.lambdaurora.lovely_snails.network.SnailScreenHandlerPayload; 15 | import dev.lambdaurora.lovely_snails.network.SnailSetStoragePagePayload; 16 | import dev.lambdaurora.lovely_snails.registry.LovelySnailsRegistry; 17 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 18 | import net.minecraft.server.level.ServerPlayer; 19 | import net.minecraft.world.Container; 20 | import net.minecraft.world.ContainerListener; 21 | import net.minecraft.world.SimpleContainer; 22 | import net.minecraft.world.entity.EquipmentSlot; 23 | import net.minecraft.world.entity.player.Inventory; 24 | import net.minecraft.world.entity.player.Player; 25 | import net.minecraft.world.inventory.AbstractContainerMenu; 26 | import net.minecraft.world.inventory.ClickType; 27 | import net.minecraft.world.inventory.Slot; 28 | import net.minecraft.world.item.ItemStack; 29 | import net.minecraft.world.item.Items; 30 | import org.jetbrains.annotations.Nullable; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | public class SnailScreenHandler extends AbstractContainerMenu implements ContainerListener { 36 | private final Player player; 37 | private final SimpleContainer inventory; 38 | private final SnailEntity entity; 39 | private final ChestSlot[] chestSlots = new ChestSlot[3]; 40 | private final List pageChangeListeners = new ArrayList<>(); 41 | private int currentStoragePage; 42 | 43 | public SnailScreenHandler(int syncId, Inventory playerInventory, SnailScreenHandlerPayload payload) { 44 | this(syncId, playerInventory, 45 | playerInventory.player.level().getEntity(payload.snailId()) instanceof SnailEntity snail ? snail : null, 46 | payload.storagePage() 47 | ); 48 | } 49 | 50 | public SnailScreenHandler(int syncId, Inventory playerInventory, SnailEntity snail, int currentStoragePage) { 51 | this(syncId, playerInventory, new SimpleContainer(snail.getInventorySize()), snail, currentStoragePage); 52 | } 53 | 54 | public SnailScreenHandler(int syncId, Inventory playerInventory, SimpleContainer inventory, SnailEntity entity, int currentStoragePage) { 55 | super(LovelySnailsRegistry.SNAIL_SCREEN_HANDLER_TYPE, syncId); 56 | checkContainerSize(inventory, entity.getInventorySize()); 57 | this.player = playerInventory.player; 58 | this.inventory = inventory; 59 | this.entity = entity; 60 | this.currentStoragePage = currentStoragePage; 61 | 62 | inventory.onOpen(playerInventory.player); 63 | this.inventory.addListener(this); 64 | 65 | this.addSlot(new SaddleSlot(new EquipmentContainer(entity, EquipmentSlot.SADDLE), 0, 26, 18)); 66 | this.addSlot(new DecorSlot(new EquipmentContainer(entity, EquipmentSlot.BODY), 0, 26, 36)); 67 | this.addSlot(this.chestSlots[0] = new ChestSlot(inventory, SnailEntity.FIRST_CHEST_SLOT, 8, 18, 0)); 68 | this.addSlot(this.chestSlots[1] = new ChestSlot(inventory, SnailEntity.SECOND_CHEST_SLOT, 8, 36, 1)); 69 | this.addSlot(this.chestSlots[2] = new ChestSlot(inventory, SnailEntity.THIRD_CHEST_SLOT, 8, 54, 2)); 70 | 71 | for (int page = 0; page < 3; page++) { 72 | for (int row = 0; row < 3; row++) { 73 | for (int column = 0; column < 5; column++) { 74 | this.addSlot(new StorageSlot(inventory, 3 + page * 15 + column + row * 5, 75 | 80 + 19 + column * 18, 18 + row * 18, page)); 76 | } 77 | } 78 | } 79 | 80 | // Player inventory. 81 | for (int row = 0; row < 3; ++row) { 82 | for (int column = 0; column < 9; ++column) { 83 | this.addSlot(new Slot(playerInventory, column + row * 9 + 9, 27 + column * 18, 102 + row * 18 + -18)); 84 | } 85 | } 86 | 87 | for (int column = 0; column < 9; ++column) { 88 | this.addSlot(new Slot(playerInventory, column, 27 + column * 18, 142)); 89 | } 90 | } 91 | 92 | /** 93 | * Returns the associated snail entity. 94 | * 95 | * @return the snail entity 96 | */ 97 | public SnailEntity snail() { 98 | return this.entity; 99 | } 100 | 101 | public SimpleContainer getInventory() { 102 | return this.inventory; 103 | } 104 | 105 | /** 106 | * Returns whether this snail holds an ender chest. 107 | * 108 | * @return {@code true} if this snails holds an ender chest, else {@code false} 109 | */ 110 | public boolean hasEnderChest() { 111 | for (int i = 0; i < 3; i++) { 112 | if (this.inventory.getItem(i).is(Items.ENDER_CHEST)) 113 | return true; 114 | } 115 | return false; 116 | } 117 | 118 | /** 119 | * Returns whether this snail has any chest to expand storage. 120 | * 121 | * @return {@code true} if this snail has any chest, else {@code false} 122 | */ 123 | public boolean hasChests() { 124 | for (int i = 0; i < 3; i++) { 125 | if (this.hasChest(i)) 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | /** 132 | * Returns whether this snail has a chest for the given storage page. 133 | * 134 | * @param page the storage page 135 | * @return {@code true} if there is a chest for the given storage page, else {@code false} 136 | */ 137 | public boolean hasChest(int page) { 138 | return this.inventory.getItem(page).is(Items.CHEST); 139 | } 140 | 141 | /** 142 | * Returns whether there is items in the specified storage page. 143 | * 144 | * @param page the storage page 145 | * @return {@code true} if there is items, else {@code false} 146 | */ 147 | public boolean hasItemsInStoragePage(int page) { 148 | for (int slot = 3 + page * 15; slot < 3 + page * 15 + 15; slot++) { 149 | if (!this.inventory.getItem(slot).isEmpty()) 150 | return true; 151 | } 152 | return false; 153 | } 154 | 155 | /** 156 | * Returns the current storage page. 157 | * 158 | * @return the storage page 159 | */ 160 | public int getCurrentStoragePage() { 161 | return this.currentStoragePage; 162 | } 163 | 164 | public void setCurrentStoragePage(int page) { 165 | this.currentStoragePage = page; 166 | if (this.player instanceof ServerPlayer serverPlayerEntity) { 167 | ServerPlayNetworking.send(serverPlayerEntity, new SnailSetStoragePagePayload(this.syncId, (byte) page)); 168 | } 169 | 170 | for (var listener : this.pageChangeListeners) { 171 | listener.onCurrentPageSet(page); 172 | } 173 | } 174 | 175 | /** 176 | * Returns which page should be selected on opening of the given inventory. 177 | * 178 | * @param inventory the inventory 179 | * @return the page to select 180 | */ 181 | public static int getOpeningStoragePage(Container inventory) { 182 | for (int page = 0; page < 3; page++) { 183 | if (inventory.getItem(page).is(Items.CHEST)) { 184 | return page; 185 | } 186 | } 187 | return 0; 188 | } 189 | 190 | public void addPageChangeListener(InventoryPageChangeListener listener) { 191 | this.pageChangeListeners.add(listener); 192 | } 193 | 194 | public void removePageChangeListener(InventoryPageChangeListener listener) { 195 | this.pageChangeListeners.remove(listener); 196 | } 197 | 198 | @Override 199 | public boolean stillValid(Player player) { 200 | return !this.entity.isInventoryDifferent(this.inventory) 201 | && this.inventory.stillValid(player) 202 | && this.entity.isAlive() 203 | && this.entity.distanceTo(player) < 8.f; 204 | } 205 | 206 | private boolean attemptToTransferSlotToCurrentPage(ItemStack currentStack) { 207 | int page = this.getCurrentStoragePage(); 208 | return this.moveItemStackTo(currentStack, 3 + page * 15, 3 + page * 15 + 15, false); 209 | } 210 | 211 | private @Nullable ItemStack attemptToTransferSlotToChestSlots(ItemStack currentStack) { 212 | for (int i = 0; i < this.chestSlots.length; i++) { 213 | int slot = SnailEntity.FIRST_CHEST_SLOT + i; 214 | 215 | if (this.chestSlots[i].mayPlace(currentStack) && !this.chestSlots[i].hasItem() 216 | && !this.moveItemStackTo(currentStack, slot, slot + 1, false)) { 217 | return ItemStack.EMPTY; 218 | } 219 | } 220 | 221 | return null; 222 | } 223 | 224 | private @Nullable ItemStack attemptToTransferToSnail(Player player, ItemStack currentStack) { 225 | if (!this.snail().canUseSnail(player)) return null; 226 | 227 | ItemStack chestResult; 228 | 229 | if ((chestResult = this.attemptToTransferSlotToChestSlots(currentStack)) != null) { 230 | return chestResult; 231 | } else if (this.getSlot(1).mayPlace(currentStack) && !this.getSlot(1).hasItem()) { 232 | if (!this.moveItemStackTo(currentStack, 1, 2, false)) { 233 | return ItemStack.EMPTY; 234 | } 235 | } else if (this.getSlot(0).mayPlace(currentStack)) { 236 | if (!this.moveItemStackTo(currentStack, 0, 1, false)) { 237 | return ItemStack.EMPTY; 238 | } 239 | } else if (!this.attemptToTransferSlotToCurrentPage(currentStack)) { 240 | return ItemStack.EMPTY; 241 | } 242 | 243 | return null; 244 | } 245 | 246 | @Override 247 | public void clicked(int slotIndex, int button, ClickType actionType, Player player) { 248 | if (slotIndex < this.inventory.size() && !this.snail().canUseSnail(player)) 249 | return; 250 | 251 | super.clicked(slotIndex, button, actionType, player); 252 | } 253 | 254 | @Override 255 | public ItemStack quickMoveStack(Player player, int fromIndex) { 256 | var stack = ItemStack.EMPTY; 257 | var slot = this.slots.get(fromIndex); 258 | 259 | if (slot.hasItem()) { 260 | var currentStack = slot.getItem(); 261 | stack = currentStack.copy(); 262 | int inventorySize = this.inventory.size(); 263 | 264 | ItemStack insertionIntoSnail; 265 | 266 | if (fromIndex < inventorySize) { 267 | if (this.snail().canUseSnail(player) && !this.moveItemStackTo(currentStack, inventorySize, this.slots.size(), true)) { 268 | return ItemStack.EMPTY; 269 | } 270 | } else if ((insertionIntoSnail = this.attemptToTransferToSnail(player, currentStack)) != null) { 271 | return insertionIntoSnail; 272 | } else { 273 | int playerInventoryEnd = inventorySize + 27; 274 | int hotbarEnd = playerInventoryEnd + 9; 275 | if (fromIndex >= playerInventoryEnd && fromIndex < hotbarEnd) { 276 | if (!this.moveItemStackTo(currentStack, inventorySize, playerInventoryEnd, false)) { 277 | return ItemStack.EMPTY; 278 | } 279 | } else if (fromIndex < playerInventoryEnd) { 280 | if (!this.moveItemStackTo(currentStack, playerInventoryEnd, hotbarEnd, false)) { 281 | return ItemStack.EMPTY; 282 | } 283 | } else if (!this.moveItemStackTo(currentStack, playerInventoryEnd, playerInventoryEnd, false)) { 284 | return ItemStack.EMPTY; 285 | } 286 | 287 | return ItemStack.EMPTY; 288 | } 289 | 290 | if (currentStack.isEmpty()) { 291 | slot.set(ItemStack.EMPTY); 292 | } else { 293 | slot.setChanged(); 294 | } 295 | 296 | if (currentStack.getCount() == stack.getCount()) { 297 | return ItemStack.EMPTY; 298 | } 299 | 300 | slot.onTake(player, currentStack); 301 | } 302 | 303 | return stack; 304 | } 305 | 306 | @Override 307 | public boolean onButtonClick(Player player, int id) { 308 | if (id == 0 && this.hasEnderChest()) { 309 | this.snail().openEnderChestInventory(player); 310 | return true; 311 | } 312 | return super.onButtonClick(player, id); 313 | } 314 | 315 | @Override 316 | public void removed(Player player) { 317 | super.removed(player); 318 | this.inventory.onClose(player); 319 | this.inventory.removeListener(this); 320 | } 321 | 322 | @Override 323 | public void onContainerChanged(Container sender) { 324 | if (this.hasChests() && !this.hasChest(this.currentStoragePage)) { 325 | this.currentStoragePage = switch (this.currentStoragePage) { 326 | case 2 -> { 327 | if (this.hasChest(1)) 328 | yield 1; 329 | else 330 | yield 0; 331 | } 332 | default -> getOpeningStoragePage(this.getInventory()); 333 | }; 334 | 335 | for (var listener : this.pageChangeListeners) { 336 | listener.onCurrentPageSet(this.currentStoragePage); 337 | } 338 | } 339 | } 340 | 341 | public interface InventoryPageChangeListener { 342 | void onCurrentPageSet(int page); 343 | } 344 | 345 | private class SnailSlot extends Slot { 346 | public SnailSlot(Container inventory, int index, int x, int y) { 347 | super(inventory, index, x, y); 348 | } 349 | 350 | @Override 351 | public boolean mayPickup(Player playerEntity) { 352 | return this.snail().canUseSnail(playerEntity); 353 | } 354 | 355 | protected SnailScreenHandler screenHandler() { 356 | return SnailScreenHandler.this; 357 | } 358 | 359 | protected SnailEntity snail() { 360 | return this.screenHandler().snail(); 361 | } 362 | } 363 | 364 | private class SaddleSlot extends SnailSlot { 365 | public SaddleSlot(Container inventory, int index, int x, int y) { 366 | super(inventory, index, x, y); 367 | } 368 | 369 | @Override 370 | public boolean mayPlace(ItemStack stack) { 371 | return stack.is(Items.SADDLE) && !this.hasItem() && this.isEnabled(); 372 | } 373 | 374 | @Override 375 | public boolean isEnabled() { 376 | return this.snail().canUseSlot(EquipmentSlot.SADDLE); 377 | } 378 | } 379 | 380 | private class DecorSlot extends SnailSlot { 381 | public DecorSlot(Container inventory, int index, int x, int y) { 382 | super(inventory, index, x, y); 383 | } 384 | 385 | @Override 386 | public boolean isEnabled() { 387 | return true; 388 | } 389 | 390 | @Override 391 | public boolean mayPlace(ItemStack stack) { 392 | return SnailEntity.getColorFromCarpet(stack) != null; 393 | } 394 | 395 | @Override 396 | public int getMaxStackSize() { 397 | return 1; 398 | } 399 | } 400 | 401 | private class ChestSlot extends SnailSlot { 402 | private final int storagePage; 403 | 404 | public ChestSlot(Container inventory, int index, int x, int y, int storagePage) { 405 | super(inventory, index, x, y); 406 | this.storagePage = storagePage; 407 | } 408 | 409 | @Override 410 | public boolean isEnabled() { 411 | return !this.snail().isBaby(); 412 | } 413 | 414 | @Override 415 | public boolean mayPlace(ItemStack stack) { 416 | return (stack.is(Items.CHEST) || stack.is(Items.ENDER_CHEST)) && this.isEnabled(); 417 | } 418 | 419 | @Override 420 | public boolean mayPickup(Player player) { 421 | return super.mayPickup(player) && !this.screenHandler().hasItemsInStoragePage(this.storagePage); 422 | } 423 | 424 | @Override 425 | public int getMaxStackSize() { 426 | return 1; 427 | } 428 | } 429 | 430 | private class StorageSlot extends SnailSlot { 431 | private final int storagePage; 432 | 433 | public StorageSlot(Container inventory, int index, int x, int y, int storagePage) { 434 | super(inventory, index, x, y); 435 | this.storagePage = storagePage; 436 | } 437 | 438 | @Override 439 | public boolean isEnabled() { 440 | return this.screenHandler().hasChest(this.storagePage) && this.screenHandler().currentStoragePage == this.storagePage; 441 | } 442 | 443 | @Override 444 | public boolean mayPlace(ItemStack stack) { 445 | return this.isEnabled(); 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/main/java/dev/lambdaurora/lovely_snails/entity/SnailEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 LambdAurora 3 | * 4 | * This file is part of Lovely Snails. 5 | * 6 | * Licensed under the Lambda License. For more information, 7 | * see the LICENSE file. 8 | */ 9 | 10 | package dev.lambdaurora.lovely_snails.entity; 11 | 12 | import dev.lambdaurora.lovely_snails.LovelySnails; 13 | import dev.lambdaurora.lovely_snails.entity.goal.SnailFollowParentGoal; 14 | import dev.lambdaurora.lovely_snails.entity.goal.SnailHideGoal; 15 | import dev.lambdaurora.lovely_snails.mixin.AgeableMobAccessor; 16 | import dev.lambdaurora.lovely_snails.mixin.ShulkerAccessor; 17 | import dev.lambdaurora.lovely_snails.network.SnailScreenHandlerPayload; 18 | import dev.lambdaurora.lovely_snails.registry.LovelySnailsRegistry; 19 | import dev.lambdaurora.lovely_snails.screen.SnailScreenHandler; 20 | import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; 21 | import net.minecraft.core.BlockPos; 22 | import net.minecraft.core.Direction; 23 | import net.minecraft.core.particles.ParticleTypes; 24 | import net.minecraft.network.chat.Text; 25 | import net.minecraft.network.syncher.EntityDataTracker; 26 | import net.minecraft.network.syncher.TrackedEntityData; 27 | import net.minecraft.network.syncher.TrackedEntityDataSerializers; 28 | import net.minecraft.server.level.ServerLevel; 29 | import net.minecraft.server.level.ServerPlayer; 30 | import net.minecraft.sounds.SoundEvent; 31 | import net.minecraft.sounds.SoundEvents; 32 | import net.minecraft.util.RandomSource; 33 | import net.minecraft.world.*; 34 | import net.minecraft.world.damagesource.DamageSource; 35 | import net.minecraft.world.entity.*; 36 | import net.minecraft.world.entity.ai.attributes.AttributeModifier; 37 | import net.minecraft.world.entity.ai.attributes.AttributeSupplier; 38 | import net.minecraft.world.entity.ai.attributes.Attributes; 39 | import net.minecraft.world.entity.ai.goal.*; 40 | import net.minecraft.world.entity.animal.Animal; 41 | import net.minecraft.world.entity.player.Inventory; 42 | import net.minecraft.world.entity.player.Player; 43 | import net.minecraft.world.entity.vehicle.DismountHelper; 44 | import net.minecraft.world.inventory.AbstractContainerMenu; 45 | import net.minecraft.world.inventory.ChestMenu; 46 | import net.minecraft.world.item.DyeColor; 47 | import net.minecraft.world.item.ItemStack; 48 | import net.minecraft.world.item.Items; 49 | import net.minecraft.world.item.enchantment.EnchantmentEffectComponents; 50 | import net.minecraft.world.item.enchantment.EnchantmentHelper; 51 | import net.minecraft.world.level.GameRules; 52 | import net.minecraft.world.level.Level; 53 | import net.minecraft.world.level.LightLayer; 54 | import net.minecraft.world.level.ServerLevelAccessor; 55 | import net.minecraft.world.level.block.Block; 56 | import net.minecraft.world.level.block.WoolCarpetBlock; 57 | import net.minecraft.world.level.block.state.BlockBehaviour; 58 | import net.minecraft.world.level.gameevent.GameEvent; 59 | import net.minecraft.world.level.storage.ValueInput; 60 | import net.minecraft.world.level.storage.ValueOutput; 61 | import net.minecraft.world.phys.AABB; 62 | import net.minecraft.world.phys.Vec3; 63 | import org.jetbrains.annotations.NotNull; 64 | import org.jetbrains.annotations.Nullable; 65 | 66 | import java.util.function.Predicate; 67 | 68 | /** 69 | * Represents the snail entity. 70 | * 71 | * @author LambdAurora 72 | * @version 1.2.1 73 | * @since 1.0.0 74 | */ 75 | public class SnailEntity extends TamableAnimal implements ContainerListener { 76 | private static final AttributeModifier SCARED_ARMOR_BONUS = ShulkerAccessor.lovely_snails$getCoveredArmorModifier(); 77 | 78 | private static final TrackedEntityData CHILD = AgeableMobAccessor.lovely_snails$getChild(); 79 | private static final TrackedEntityData SNAIL_FLAGS 80 | = EntityDataTracker.registerData(SnailEntity.class, TrackedEntityDataSerializers.BYTE); 81 | private static final TrackedEntityData CHEST_FLAGS 82 | = EntityDataTracker.registerData(SnailEntity.class, TrackedEntityDataSerializers.BYTE); 83 | private static final int SCARED_FLAG = 0b0000_0001; 84 | private static final int INTERACTION_COOLDOWN_FLAG = 0b0000_0010; 85 | private static final int LOCKED_FLAG = 0b0000_0100; 86 | 87 | public static final int FIRST_CHEST_SLOT = 0; 88 | public static final int SECOND_CHEST_SLOT = 1; 89 | public static final int THIRD_CHEST_SLOT = 2; 90 | private static final int SATISFACTION_START = -256; 91 | 92 | private SimpleContainer inventory; 93 | private int satisfaction; 94 | private short interactionCooldown; 95 | private boolean reading; 96 | 97 | public SnailEntity(EntityType entityType, Level level) { 98 | super(entityType, level); 99 | this.updateInventory(); 100 | //this.setMaxUpStep(1.f); 101 | } 102 | 103 | public static AttributeSupplier.Builder createSnailAttributes() { 104 | return Mob.createMobAttributes() 105 | .add(Attributes.MAX_HEALTH, 20.0) 106 | .add(Attributes.MOVEMENT_SPEED, .3f) 107 | .add(Attributes.ATTACK_DAMAGE, 2.0) 108 | .add(Attributes.FOLLOW_RANGE, 48.0); 109 | } 110 | 111 | public static boolean canSpawn( 112 | EntityType type, 113 | ServerLevelAccessor level, 114 | EntitySpawnReason spawnReason, 115 | BlockPos pos, 116 | RandomSource random 117 | ) { 118 | var spawnBlock = level.getBlockState(pos.below()); 119 | return level.getBrightness(LightLayer.SKY, pos) > 6 && spawnBlock.is(LovelySnailsRegistry.SNAIL_SPAWN_BLOCKS); 120 | } 121 | 122 | @Override 123 | public @NotNull SpawnGroupData finalizeSpawn( 124 | ServerLevelAccessor world, DifficultyInstance difficulty, EntitySpawnReason spawnReason, 125 | @Nullable SpawnGroupData entityData 126 | ) { 127 | this.satisfaction = SATISFACTION_START + this.random.nextInt(10); 128 | this.setBaby(true); 129 | return super.finalizeSpawn(world, difficulty, spawnReason, entityData); 130 | } 131 | 132 | protected boolean getSnailFlag(int bitmask) { 133 | return (this.dataTracker.get(SNAIL_FLAGS) & bitmask) != 0; 134 | } 135 | 136 | protected void setSnailFlag(int bitmask, boolean flag) { 137 | byte b = this.dataTracker.get(SNAIL_FLAGS); 138 | if (flag) { 139 | this.dataTracker.set(SNAIL_FLAGS, (byte) (b | bitmask)); 140 | } else { 141 | this.dataTracker.set(SNAIL_FLAGS, (byte) (b & ~bitmask)); 142 | } 143 | } 144 | 145 | /** 146 | * Returns whether this snail is scared of something. 147 | * 148 | * @return {@code true} if this snail is scared, else {@code false} 149 | */ 150 | public boolean isScared() { 151 | return this.getSnailFlag(SCARED_FLAG); 152 | } 153 | 154 | /** 155 | * Sets whether this snail is scared of something. 156 | * 157 | * @param scared {@code true} if this snail is scared, else {@code false} 158 | */ 159 | public void setScared(boolean scared) { 160 | this.setSnailFlag(SCARED_FLAG, scared); 161 | 162 | if (!this.level().isClientSide()) { 163 | this.getAttribute(Attributes.ARMOR).removeModifier(SCARED_ARMOR_BONUS); 164 | if (scared) { 165 | this.getAttribute(Attributes.ARMOR).addPermanentModifier(SCARED_ARMOR_BONUS); 166 | } 167 | } 168 | } 169 | 170 | public int getSatisfaction() { 171 | if (this.level().isClientSide()) { 172 | return this.dataTracker.get(CHILD) ? -1 : 1; 173 | } else { 174 | return this.satisfaction; 175 | } 176 | } 177 | 178 | public void setSatisfaction(int satisfaction) { 179 | this.satisfaction = satisfaction; 180 | 181 | this.setBaby(this.shouldBeBaby()); 182 | } 183 | 184 | /** 185 | * Satisfies by the specified amount this snail. 186 | * 187 | * @param baseSatisfaction the base satisfaction amount 188 | */ 189 | public void satisfies(int baseSatisfaction) { 190 | Level level = this.level(); 191 | 192 | if (this.isBaby()) { 193 | this.putInteractionOnCooldown(); 194 | int newSatisfaction = this.getSatisfaction() + baseSatisfaction + this.random.nextInt(10); 195 | 196 | if (newSatisfaction >= 0) { 197 | var adultDimensions = this.getType().getDimensions(); 198 | float width = adultDimensions.width() * .8f; 199 | float eyeHeight = adultDimensions.eyeHeight(); 200 | var pos = BlockPos.ofFloored(this.getX(), this.getY() + eyeHeight, this.getZ()); 201 | var box = AABB.ofSize(new Vec3(this.getX(), this.getY() + eyeHeight, this.getZ()), width, 1.0E-6, width); 202 | 203 | // Adult form will suffocate, so we must prevent the growth until the player moves the snail. 204 | boolean willSuffocate = level.getBlockStates(box) 205 | .filter(Predicate.not(BlockBehaviour.BlockStateBase::isAir)) 206 | .anyMatch(state -> state.isSuffocating(this.level(), pos)); 207 | if (willSuffocate) { 208 | level.broadcastEntityEvent(this, (byte) 10); 209 | return; 210 | } 211 | } 212 | 213 | this.setSatisfaction(newSatisfaction); 214 | } 215 | 216 | level.broadcastEntityEvent(this, (byte) 8); 217 | } 218 | 219 | public short getInteractionCooldown() { 220 | if (this.level().isClientSide()) { 221 | return (short) (this.getSnailFlag(INTERACTION_COOLDOWN_FLAG) ? 1 : 0); 222 | } else { 223 | return this.interactionCooldown; 224 | } 225 | } 226 | 227 | public boolean canSatisfy() { 228 | return this.getInteractionCooldown() == 0; 229 | } 230 | 231 | public void setInteractionCooldown(int interactionCooldown) { 232 | boolean onCooldown = this.interactionCooldown > 0; 233 | if (onCooldown == (interactionCooldown == 0)) 234 | this.setSnailFlag(INTERACTION_COOLDOWN_FLAG, interactionCooldown != 0); 235 | 236 | this.interactionCooldown = (short) interactionCooldown; 237 | } 238 | 239 | /** 240 | * Puts interactions that brings satisfaction on cool-down. 241 | */ 242 | public void putInteractionOnCooldown() { 243 | this.setInteractionCooldown(75 + this.random.nextInt(10)); 244 | } 245 | 246 | /** 247 | * {@return {@code true} if this snail is locked, otherwise {@code false}} 248 | */ 249 | public boolean isLocked() { 250 | return this.getSnailFlag(LOCKED_FLAG); 251 | } 252 | 253 | /** 254 | * Sets whether this is locked. 255 | * 256 | * @param locked {@code true} if this snail is locked, otherwise {@code false} 257 | */ 258 | public void setLocked(boolean locked) { 259 | this.setSnailFlag(LOCKED_FLAG, locked); 260 | } 261 | 262 | /** 263 | * {@return {@code true} if the player is allowed to interact in any meaningful way with this snail, otherwise {@code false}} 264 | * 265 | * @param entity the entity that attempts to interact in any meaningful way with the snail 266 | */ 267 | public boolean canUseSnail(Entity entity) { 268 | return !this.isLocked() || (entity instanceof LivingEntity living && this.isOwnedBy(living)); 269 | } 270 | 271 | public static @Nullable DyeColor getColorFromCarpet(ItemStack color) { 272 | var block = Block.byItem(color.getItem()); 273 | return block instanceof WoolCarpetBlock dyedCarpetBlock ? dyedCarpetBlock.getColor() : null; 274 | } 275 | 276 | public @Nullable DyeColor getCarpetColor() { 277 | return getColorFromCarpet(this.equipment.get(EquipmentSlot.BODY)); 278 | } 279 | 280 | @Override 281 | public void handleEntityEvent(byte event) { 282 | if (event == 8) { 283 | for (int i = 0; i < 7; ++i) { 284 | double xOffset = this.random.nextGaussian() * 0.02; 285 | double yOffset = this.random.nextGaussian() * 0.02; 286 | double zOffset = this.random.nextGaussian() * 0.02; 287 | this.level().addParticle(this.random.nextBoolean() ? ParticleTypes.HAPPY_VILLAGER : ParticleTypes.HEART, 288 | this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), 289 | xOffset, yOffset, zOffset); 290 | } 291 | } else if (event == 9) { 292 | for (int i = 0; i < 7; ++i) { 293 | double xOffset = this.random.nextGaussian() * 0.02; 294 | double yOffset = this.random.nextGaussian() * 0.02; 295 | double zOffset = this.random.nextGaussian() * 0.02; 296 | this.level().addParticle(ParticleTypes.ANGRY_VILLAGER, 297 | this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), 298 | xOffset, yOffset, zOffset); 299 | } 300 | } else if (event == 10) { 301 | for (int i = 0; i < 7; ++i) { 302 | double xOffset = this.random.nextGaussian() * 0.02; 303 | double yOffset = this.random.nextGaussian() * 0.02; 304 | double zOffset = this.random.nextGaussian() * 0.02; 305 | this.level().addParticle(this.random.nextBoolean() ? ParticleTypes.ANGRY_VILLAGER : ParticleTypes.SMOKE, 306 | this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), 307 | xOffset, yOffset, zOffset); 308 | } 309 | } else 310 | super.handleEntityEvent(event); 311 | } 312 | 313 | @Override 314 | public boolean requiresCustomPersistence() { 315 | return super.requiresCustomPersistence() || this.isTame(); 316 | } 317 | 318 | /* Data Tracker Stuff */ 319 | 320 | @Override 321 | protected void initDataTracker(EntityDataTracker.Builder builder) { 322 | super.initDataTracker(builder); 323 | builder.define(SNAIL_FLAGS, (byte) 0); 324 | builder.define(CHEST_FLAGS, (byte) 0); 325 | } 326 | 327 | /* Serialization */ 328 | 329 | @Override 330 | public void readCustomSaveData(ValueInput input) { 331 | this.reading = true; 332 | super.readCustomSaveData(input); 333 | 334 | this.setSatisfaction(input.getIntOr("satisfaction", SATISFACTION_START)); 335 | this.setInteractionCooldown(input.getShortOr("interaction_cooldown", (short) 0)); 336 | this.setLocked(input.getBooleanOr("locked", false)); 337 | 338 | input.read("saddle", ItemStack.CODEC).ifPresent(stack -> this.equipment.set(EquipmentSlot.SADDLE, stack)); 339 | input.read("decor", ItemStack.CODEC).ifPresent(stack -> this.equipment.set(EquipmentSlot.BODY, stack)); 340 | 341 | LovelySnails.readInventory(input, "chests", this.inventory, 0); 342 | LovelySnails.readInventory(input, "inventory", this.inventory, 3); 343 | 344 | this.syncInventoryToFlags(); 345 | this.reading = false; 346 | } 347 | 348 | @Override 349 | public void writeCustomSaveData(ValueOutput output) { 350 | super.writeCustomSaveData(output); 351 | output.remove("Sitting"); // We don't actually need that as you can't make a snail sit. 352 | 353 | output.putInt("satisfaction", this.getSatisfaction()); 354 | output.putShort("interaction_cooldown", this.getInteractionCooldown()); 355 | output.putBoolean("locked", this.isLocked()); 356 | 357 | LovelySnails.writeInventory(output, "chests", this.inventory, 0, 3); 358 | LovelySnails.writeInventory(output, "inventory", this.inventory, 3, this.inventory.size()); 359 | } 360 | 361 | /* AI */ 362 | 363 | @Override 364 | protected void registerGoals() { 365 | super.registerGoals(); 366 | 367 | this.goalSelector.addGoal(0, new FloatGoal(this)); 368 | this.goalSelector.addGoal(1, new PanicGoal(this, 1.2)); 369 | this.goalSelector.addGoal(1, new SnailHideGoal(this, 5)); 370 | this.goalSelector.addGoal(2, new BreedGoal(this, 1.0, SnailEntity.class)); 371 | this.goalSelector.addGoal(4, new SnailFollowParentGoal(this, 1.0)); 372 | this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 0.7)); 373 | this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.f)); 374 | this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); 375 | } 376 | 377 | /* Inventory */ 378 | 379 | public int getChestFlags() { 380 | return this.dataTracker.get(CHEST_FLAGS); 381 | } 382 | 383 | public ItemStack getChest(int slot) { 384 | int flags = this.getChestFlags(); 385 | 386 | int chest = (flags >> slot * 2) & 3; 387 | 388 | return switch (chest) { 389 | case 1 -> new ItemStack(Items.CHEST); 390 | case 2 -> new ItemStack(Items.ENDER_CHEST); 391 | default -> ItemStack.EMPTY; 392 | }; 393 | } 394 | 395 | public int getInventorySize() { 396 | return 48; 397 | } 398 | 399 | public ItemStack getSaddle() { 400 | return this.equipment.get(EquipmentSlot.SADDLE); 401 | } 402 | 403 | /** 404 | * Syncs the flags with the inventory. 405 | */ 406 | public void syncInventoryToFlags() { 407 | if (!this.level().isClientSide()) { 408 | int chestFlags = 0; 409 | for (int chest = 0; chest < 3; chest++) { 410 | var chestStack = this.inventory.getItem(chest); 411 | int flag = 0; 412 | 413 | if (chestStack.is(Items.CHEST)) 414 | flag = 1; 415 | else if (chestStack.is(Items.ENDER_CHEST)) 416 | flag = 2; 417 | 418 | chestFlags |= flag << chest * 2; 419 | } 420 | this.dataTracker.set(CHEST_FLAGS, (byte) chestFlags); 421 | } 422 | } 423 | 424 | public void openInventory(Player player) { 425 | if (!this.level().isClientSide() && (!this.isVehicle() || this.hasPassenger(player)) && this.isTame()) { 426 | player.openMenu(new SnailScreenHandlerFactory()); 427 | } 428 | } 429 | 430 | public void openEnderChestInventory(Player player) { 431 | if (!this.level().isClientSide() && (!this.isVehicle() || this.hasPassenger(player)) && this.isTame()) { 432 | player.openMenu(new SimpleMenuProvider((syncId, playerInventory, playerEntity) -> { 433 | return ChestMenu.threeRows(syncId, playerInventory, player.getEnderChestInventory()); 434 | }, Text.translatable("container.enderchest"))); 435 | } 436 | } 437 | 438 | public boolean isInventoryDifferent(Container inventory) { 439 | return this.inventory != inventory; 440 | } 441 | 442 | @Override 443 | protected void dropEquipment(ServerLevel level) { 444 | super.dropEquipment(level); 445 | 446 | if (this.inventory != null) { 447 | for (int slot = 0; slot < this.inventory.size(); ++slot) { 448 | var stack = this.inventory.getItem(slot); 449 | if (!stack.isEmpty() && !EnchantmentHelper.has(stack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) 450 | this.spawnAtLocation(level, stack); 451 | } 452 | } 453 | } 454 | 455 | protected void updateInventory() { 456 | var previousInventory = this.inventory; 457 | this.inventory = new SimpleContainer(this.getInventorySize()); 458 | if (previousInventory != null) { 459 | previousInventory.removeListener(this); 460 | int maxSize = Math.min(previousInventory.size(), this.inventory.size()); 461 | 462 | for (int slot = 0; slot < maxSize; ++slot) { 463 | var stack = previousInventory.getItem(slot); 464 | if (!stack.isEmpty()) { 465 | this.inventory.setItem(slot, stack.copy()); 466 | } 467 | } 468 | } 469 | 470 | this.inventory.addListener(this); 471 | this.syncInventoryToFlags(); 472 | } 473 | 474 | @Override 475 | public void onContainerChanged(Container sender) { 476 | boolean previouslySaddled = this.isSaddled(); 477 | boolean hadDecor = this.getCarpetColor() != null; 478 | this.syncInventoryToFlags(); 479 | if (this.age > 20 && !previouslySaddled && this.isSaddled()) { 480 | this.playSound(SoundEvents.HORSE_SADDLE.value(), .5f, 1.f); 481 | } 482 | 483 | if (!this.reading && !this.level().isClientSide() && !hadDecor && this.getCarpetColor() != null && this.canSatisfy()) { 484 | var biome = this.level().getBiome(this.getBlockPos()); 485 | 486 | int baseSatisfaction; 487 | if (biome.value().warmEnoughToRain(this.getBlockPos(), this.level().getSeaLevel())) baseSatisfaction = 15; 488 | else baseSatisfaction = 5; 489 | this.satisfies(baseSatisfaction); 490 | } 491 | } 492 | 493 | /* Interaction */ 494 | 495 | @Override 496 | public InteractionResult mobInteract(Player player, InteractionHand hand) { 497 | Level level = this.level(); 498 | var handStack = player.getItemInHand(hand); 499 | 500 | if (this.isTame() && player.isSecondaryUseActive()) { 501 | this.openInventory(player); 502 | return InteractionResult.SUCCESS; 503 | } 504 | 505 | if (this.isVehicle()) { 506 | return super.mobInteract(player, hand); 507 | } 508 | 509 | if (!handStack.isEmpty()) { 510 | var itemResult = handStack.interactLivingEntity(player, this, hand); 511 | if (itemResult.isAccepted()) { 512 | return itemResult; 513 | } 514 | 515 | if (this.isFood(handStack) && !this.isScared()) { 516 | if (this.isTame() && this.canUseSnail(player)) { 517 | int age = this.getAge(); 518 | 519 | if (!level.isClientSide() && age == 0 && this.canFallInLove()) { 520 | this.usePlayerItem(player, hand, handStack); 521 | this.setInLove(player); 522 | this.emitGameEvent(GameEvent.EAT); 523 | return InteractionResult.SUCCESS; 524 | } else if (level.isClientSide()) { 525 | return InteractionResult.CONSUME; 526 | } 527 | } else { 528 | this.usePlayerItem(player, hand, handStack); 529 | 530 | if (!this.isLocked() && this.random.nextInt(3) == 0) { 531 | this.tame(player); 532 | level.broadcastEntityEvent(this, (byte) 7); 533 | } else { 534 | level.broadcastEntityEvent(this, (byte) 6); 535 | } 536 | 537 | return InteractionResult.SUCCESS; 538 | } 539 | } 540 | 541 | if (!this.isTame()) { 542 | return InteractionResult.CONSUME; 543 | } 544 | 545 | boolean saddle = !this.isBaby() && !this.isSaddled() && handStack.is(Items.SADDLE); 546 | if (getColorFromCarpet(handStack) != null || saddle) { 547 | this.openInventory(player); 548 | return InteractionResult.SUCCESS; 549 | } 550 | } 551 | 552 | if (this.isTame()) { 553 | if (!this.isBaby()) { 554 | if (!level.isClientSide()) { 555 | player.setYaw(this.getYaw()); 556 | player.setPitch(this.getPitch()); 557 | player.startRiding(this); 558 | } 559 | 560 | return InteractionResult.SUCCESS; 561 | } else if (this.canSatisfy() && this.getOwner() == player) { 562 | boolean likeItem = handStack.isIn(LovelySnailsRegistry.SNAIL_FOOD_ITEMS); 563 | if (handStack.isEmpty() || likeItem) { 564 | if (likeItem) this.usePlayerItem(player, hand, handStack); 565 | // What about petting a snail? 566 | if (!level.isClientSide()) 567 | this.satisfies(likeItem ? 20 : 10); 568 | 569 | return InteractionResult.SUCCESS; 570 | } else if (handStack.is(Items.POISONOUS_POTATO)) { 571 | // Watch me break one of Jeb's rule. 572 | // Also why the fuck would you give a poisonous potato to a snail? 573 | if (!level.isClientSide()) { 574 | this.usePlayerItem(player, hand, handStack); 575 | this.setSatisfaction(this.getSatisfaction() - 4000); 576 | this.putInteractionOnCooldown(); 577 | 578 | level.broadcastEntityEvent(this, (byte) 9); 579 | } 580 | 581 | return InteractionResult.SUCCESS; 582 | } 583 | } 584 | } 585 | 586 | return super.mobInteract(player, hand); 587 | } 588 | 589 | public void onWaterSplashed(Entity waterOwner) { 590 | if (this.getOwner() != waterOwner) 591 | return; 592 | 593 | if (this.canSatisfy()) { 594 | var biome = this.level().getBiome(this.getBlockPos()); 595 | 596 | int baseSatisfaction; 597 | if (!biome.value().hasPrecipitation()) baseSatisfaction = 20; 598 | else if (biome.value().warmEnoughToRain(this.getBlockPos(), this.level().getSeaLevel())) baseSatisfaction = 10; 599 | else baseSatisfaction = 15; 600 | this.satisfies(baseSatisfaction); 601 | } 602 | } 603 | 604 | /* Saddle Stuff */ 605 | 606 | @Override 607 | public boolean canUseSlot(EquipmentSlot equipmentSlot) { 608 | return switch (equipmentSlot) { 609 | case SADDLE -> this.isAlive() && !this.isBaby() && this.isTame(); 610 | case BODY -> this.isAlive() && this.isTame(); 611 | default -> false; 612 | }; 613 | } 614 | 615 | @Override 616 | public boolean isSaddled() { 617 | return !this.getItemBySlot(EquipmentSlot.SADDLE).isEmpty(); 618 | } 619 | 620 | /* Leashing */ 621 | 622 | @Override 623 | public boolean supportQuadLeash() { 624 | return !this.isBaby(); 625 | } 626 | 627 | @Override 628 | public Vec3 @NotNull [] getQuadLeashOffsets() { 629 | return Leashable.createQuadLeashOffsets(this, -0.06, 0.64, 0.38, 1); 630 | } 631 | 632 | /* Riding */ 633 | 634 | @Override 635 | public @Nullable LivingEntity getControllingPassenger() { 636 | if (this.isLocked()) return null; 637 | if (!this.isSaddled()) return null; 638 | 639 | Entity passenger = this.getFirstPassenger(); 640 | 641 | if (passenger instanceof LivingEntity livingPassenger) { 642 | return livingPassenger; 643 | } else { 644 | return null; 645 | } 646 | } 647 | 648 | private @Nullable Vec3 tryDismountTowards(Vec3 vec3d, LivingEntity livingEntity) { 649 | double targetX = this.getX() + vec3d.x; 650 | double targetY = this.getBoundingBox().minY; 651 | double targetZ = this.getZ() + vec3d.z; 652 | var pos = new BlockPos.Mutable(); 653 | 654 | for (var pose : livingEntity.getDismountPoses()) { 655 | pos.set(targetX, targetY, targetZ); 656 | double maxDismountY = this.getBoundingBox().maxY + 0.75; 657 | 658 | while (true) { 659 | double dismountHeight = this.level().getBlockFloorHeight(pos); 660 | if (pos.getY() + dismountHeight > maxDismountY) { 661 | break; 662 | } 663 | 664 | if (DismountHelper.isBlockFloorValid(dismountHeight)) { 665 | var poseBoundingBox = livingEntity.getLocalBoundsForPose(pose); 666 | var dismountPos = new Vec3(targetX, pos.getY() + dismountHeight, targetZ); 667 | if (DismountHelper.canDismountTo(this.level(), livingEntity, poseBoundingBox.move(dismountPos))) { 668 | livingEntity.setPose(pose); 669 | return dismountPos; 670 | } 671 | } 672 | 673 | pos.move(Direction.UP); 674 | if (!(pos.getY() < maxDismountY)) { 675 | break; 676 | } 677 | } 678 | } 679 | 680 | return null; 681 | } 682 | 683 | @Override 684 | public @NotNull Vec3 getDismountLocationForPassenger(LivingEntity passenger) { 685 | var rightDismountOffset = getCollisionHorizontalEscapeVector(this.getBoundingWidth(), passenger.getBoundingWidth(), 686 | this.getYaw() + (passenger.getMainArm() == HumanoidArm.RIGHT ? 90.f : -90.f)); 687 | var dismountPos = this.tryDismountTowards(rightDismountOffset, passenger); 688 | 689 | if (dismountPos != null) { 690 | return dismountPos; 691 | } else { 692 | var leftDismountOffset = getCollisionHorizontalEscapeVector(this.getBoundingWidth(), passenger.getBoundingWidth(), 693 | this.getYaw() + (passenger.getMainArm() == HumanoidArm.LEFT ? 90.f : -90.f)); 694 | dismountPos = this.tryDismountTowards(leftDismountOffset, passenger); 695 | return dismountPos != null ? dismountPos : this.position(); 696 | } 697 | } 698 | 699 | /* Movement */ 700 | 701 | @Override 702 | public void aiStep() { 703 | super.aiStep(); 704 | 705 | if (!this.level().isClientSide() && this.isAlive()) { 706 | if (this.random.nextInt(900) == 0 && this.deathTime == 0) { 707 | this.heal(1.f); 708 | } 709 | 710 | short interactionCooldown = this.getInteractionCooldown(); 711 | if (interactionCooldown != 0) { 712 | this.setInteractionCooldown(interactionCooldown - 1); 713 | } 714 | } 715 | } 716 | 717 | @Override 718 | public void travel(Vec3 movementInput) { 719 | if (this.isAlive()) { 720 | Entity primaryPassenger = this.getControllingPassenger(); 721 | 722 | if (primaryPassenger != null && this.isSaddled() && this.canUseSnail(primaryPassenger)) { 723 | if (this.isScared()) { // When the snail is scared, the snail is paralyzed. 724 | return; 725 | } 726 | 727 | var rider = (LivingEntity) primaryPassenger; 728 | //noinspection ConstantConditions 729 | this.setYaw(rider.getYaw()); 730 | this.prevYaw = this.getYaw(); 731 | this.setPitch(rider.getPitch() * .5f); 732 | this.setRotation(this.getYaw(), this.getPitch()); 733 | this.bodyYaw = this.getYaw(); 734 | this.headYaw = this.bodyYaw; 735 | float sidewaysSpeed = rider.sidewaysSpeed * .25f; 736 | float forwardSpeed = rider.forwardSpeed * .4f; 737 | if (forwardSpeed <= 0.f) { 738 | forwardSpeed *= .25f; 739 | } 740 | 741 | if (this.isLocalClientAuthoritative()) { 742 | this.setSpeed((float) this.getAttributeValue(Attributes.MOVEMENT_SPEED)); 743 | super.travel(new Vec3(sidewaysSpeed, movementInput.y, forwardSpeed)); 744 | } else if (rider instanceof Player) { 745 | this.setVelocity(Vec3.ZERO); 746 | } 747 | 748 | this.calculateEntityAnimation(false); 749 | } else { 750 | super.travel(movementInput); 751 | } 752 | } 753 | } 754 | 755 | @Override 756 | public boolean isPushable() { 757 | return !this.isVehicle(); 758 | } 759 | 760 | @Override 761 | protected boolean isImmobile() { 762 | return super.isImmobile() || (this.isVehicle() && this.isSaddled() && this.canUseSnail(this.getControllingPassenger())); 763 | } 764 | 765 | /* Sounds */ 766 | 767 | @Override 768 | protected SoundEvent getHurtSound(DamageSource source) { 769 | return LovelySnailsRegistry.SNAIL_HURT_SOUND_EVENT; 770 | } 771 | 772 | @Override 773 | protected SoundEvent getDeathSound() { 774 | return LovelySnailsRegistry.SNAIL_DEATH_SOUND_EVENT; 775 | } 776 | 777 | /* Passive Stuff */ 778 | 779 | @Override 780 | public void setAge(int age) { 781 | this.age = age; 782 | } 783 | 784 | @Override 785 | protected void onGrowUp() { 786 | if (this.level() instanceof ServerLevel level && !this.isBaby() && level.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { 787 | this.spawnAtLocation(level, new ItemStack(Items.SLIME_BALL, 1 + this.random.nextInt(2))); 788 | } 789 | } 790 | 791 | protected boolean shouldBeBaby() { 792 | return this.satisfaction < 0; 793 | } 794 | 795 | @Override 796 | public boolean isBaby() { 797 | return this.dataTracker.get(CHILD); 798 | } 799 | 800 | @Override 801 | public void setBaby(boolean baby) { 802 | var wasBaby = this.dataTracker.get(CHILD); 803 | this.dataTracker.set(CHILD, baby); 804 | 805 | if (wasBaby && !baby && !this.reading) { 806 | this.onGrowUp(); 807 | } 808 | } 809 | 810 | /* Animal Stuff */ 811 | 812 | @Override 813 | public SnailEntity getBreedOffspring(ServerLevel level, AgeableMob otherParent) { 814 | var child = LovelySnailsRegistry.SNAIL_ENTITY_TYPE.create(level, EntitySpawnReason.BREEDING); 815 | 816 | if (otherParent instanceof SnailEntity) { 817 | if (this.isTame()) { 818 | child.setOwnerReference(this.getOwnerReference()); 819 | child.setTame(true, false); 820 | } 821 | } 822 | 823 | return child; 824 | } 825 | 826 | @Override 827 | public boolean isFood(ItemStack stack) { 828 | return stack.isIn(LovelySnailsRegistry.SNAIL_BREEDING_ITEMS); 829 | } 830 | 831 | @Override 832 | public float getAgeScale() { 833 | return this.isBaby() ? 0.35f : 1.f; 834 | } 835 | 836 | private class SnailScreenHandlerFactory implements ExtendedScreenHandlerFactory { 837 | private SnailEntity snail() { 838 | return SnailEntity.this; 839 | } 840 | 841 | @Override 842 | public @NotNull Text getDisplayName() { 843 | return this.snail().getDisplayName(); 844 | } 845 | 846 | @Override 847 | public SnailScreenHandlerPayload getScreenOpeningData(ServerPlayer player) { 848 | return new SnailScreenHandlerPayload( 849 | this.snail().getId(), 850 | (byte) SnailScreenHandler.getOpeningStoragePage(this.snail().inventory) 851 | ); 852 | } 853 | 854 | @Override 855 | public AbstractContainerMenu createMenu(int syncId, Inventory inv, Player player) { 856 | var snailInv = this.snail().inventory; 857 | return new SnailScreenHandler(syncId, inv, snailInv, this.snail(), SnailScreenHandler.getOpeningStoragePage(snailInv)); 858 | } 859 | } 860 | } 861 | --------------------------------------------------------------------------------