From 122ff939aa72131431d29e923f1971d388018cea Mon Sep 17 00:00:00 2001 From: koka Date: Sat, 29 Aug 2026 15:17:37 +0200 Subject: [PATCH] feat(ouch): restaurer le projectile Abjection --- CHANGELOG.md | 5 +- build.gradle | 18 +++-- gradle.properties | 4 +- onlyfun/ABJECTION.md | 8 +++ .../abjection/AbjectionRegistries.java | 2 +- .../onlyfun/abjection/ShitBallItem.java | 22 ++++++ .../onlyfun/api/abjection/AbjectionItems.java | 19 +++++ .../api/abjection/AbjectionProjectileApi.java | 39 +++++++++++ ouch/build.gradle | 46 +++++++++++- .../sanctuary26/ouch/client/OuchClient.java | 4 ++ .../ouch/entity/ShitBallEntity.java | 62 ++++++++++++++++ .../registry/OuchAbjectionProjectiles.java | 70 +++++++++++++++++++ .../ouch/registry/OuchRegistries.java | 1 + ouch/src/main/resources/fabric.mod.json | 1 + .../ouch/entity/ShitBallContractSmoke.java | 27 +++++++ 15 files changed, 314 insertions(+), 14 deletions(-) create mode 100644 onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitBallItem.java create mode 100644 onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionItems.java create mode 100644 onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java create mode 100644 ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java create mode 100644 ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java create mode 100644 ouch/src/test/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallContractSmoke.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eab01a..fe673de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve ## Non distribué -Modules modifiés : **I Like To Move It `0.0.0-alpha.15`**, **It's Alive `0.0.0-alpha.35`** et **Only Fun `0.0.0-alpha.21`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release. +Modules modifiés : **I Like To Move It `0.0.0-alpha.15`**, **It's Alive `0.0.0-alpha.35`**, **Only Fun `0.0.0-alpha.22`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release. - double la vitesse de croisière du Happy Ghast lorsqu’un joueur le pilote, sans accélérer ses déplacements autonomes ; - conserve les données v3 et le protocole réseau 5, sans migration de sauvegarde ni nouvel ID. @@ -18,6 +18,9 @@ Modules modifiés : **I Like To Move It `0.0.0-alpha.15`**, **It's Alive `0.0.0- - importe le cœur GPL historique d’Abjection dans Only Fun, sans onzième JAR : `/piss`, `/vomit`, `/shit` et `/smoke` retrouvent leurs durées, jets, impacts et messages ; - restaure `abjection:shit_ball` ainsi que le bloc boueux `abjection:shit_cube`, sa recette, son butin et ses ressources originales en FR/EN/RU ; - expose une API serveur unique pour les commandes et le futur raccourci, nettoie les actions temporaires à la mort, en spectateur ou à la déconnexion, et conserve les données v2 ainsi que le protocole réseau 2 d’Only Fun. +- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer l’objet possédé par Only Fun ; +- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu d’objet, son impact boueux et ses sept secondes de nausée ajoutées à la durée existante ; +- déclare la dépendance sans cycle `ouch -> onlyfun` ; les données et protocoles réseau d’Ouch et d’Only Fun restent inchangés. ## `26.2.0-alpha.209` diff --git a/build.gradle b/build.gradle index 14ccd69..8ce9b43 100644 --- a/build.gradle +++ b/build.gradle @@ -3430,7 +3430,7 @@ tasks.register("verifyProgressivePauseMenuRelease") { } if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.sanctuary_version != "0.0.0-alpha.117" - || rootProject.onlyfun_version != "0.0.0-alpha.21" + || rootProject.onlyfun_version != "0.0.0-alpha.22" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"] || migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"] @@ -3597,6 +3597,7 @@ project(":ouch") { dependencies { implementation project(":anotherworld") implementation project(":ambiance") + implementation project(":onlyfun") implementation project(":sanctuary") } } @@ -3637,7 +3638,7 @@ project(":onlyfun") { tasks.register("verifyOnlyFun") { group = "verification" - description = "Checks Only Fun alpha.21, including the historical Abjection server actions." + description = "Checks Only Fun alpha.22, including its public Abjection projectile contract." inputs.files(fileTree("src/main/java")) inputs.files(fileTree("src/main/resources")) inputs.file(file("art/shop/shop_catalog_editable.csv")) @@ -3731,7 +3732,7 @@ project(":onlyfun") { def shitCubeRecipe = new JsonSlurper().parse(file( "src/main/resources/data/abjection/recipe/shit_cube.json")) if (manifest.custom.sanctuary26.data_version != 2 - || project.version.toString() != "0.0.0-alpha.21" + || project.version.toString() != "0.0.0-alpha.22" || manifest.depends?.sanctuary != "*" || manifest.provides != ["abjection"] || manifest.custom.sanctuary26.network_protocol != 2 @@ -3744,6 +3745,9 @@ project(":onlyfun") { || !javaText.contains('ServerPlayConnectionEvents.DISCONNECT') || !javaText.contains('new ItemStack(AbjectionRegistries.SHIT_BALL)') || !javaText.contains('BlockBehaviour.Properties.ofFullCopy(Blocks.MUD)') + || !javaText.contains('class ShitBallItem extends Item implements ProjectileItem') + || !javaText.contains('public static void registerFactory(Factory implementation)') + || !javaText.contains('return AbjectionProjectileApi.create(level, position, stack, direction)') || shitCubeTexture == null || shitCubeTexture.width != 16 || shitCubeTexture.height != 16 || shitBallTexture == null || shitBallTexture.width != 16 || shitBallTexture.height != 16 || shitCubeRecipe.key?.get("#") != "abjection:shit_ball" @@ -4171,7 +4175,7 @@ tasks.register("verifyShopPricingLayoutRelease") { def release = new JsonSlurper().parse(file("pack/release.json")) def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) if (rootProject.pack_version != "26.2.0-alpha.209" - || rootProject.onlyfun_version != "0.0.0-alpha.21" + || rootProject.onlyfun_version != "0.0.0-alpha.22" || rootProject.sanctuary_version != "0.0.0-alpha.117" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.192" @@ -4265,7 +4269,7 @@ tasks.register("verifyShopCsvBossGatesRelease") { def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text if (rootProject.pack_version != "26.2.0-alpha.209" - || rootProject.onlyfun_version != "0.0.0-alpha.21" + || rootProject.onlyfun_version != "0.0.0-alpha.22" || rootProject.sanctuary_version != "0.0.0-alpha.117" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.194" @@ -4513,7 +4517,7 @@ tasks.register("verifyWeatherTntAmbianceRelease") { def rootBuild = file("build.gradle").text if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.ambiance_version != "0.0.0-alpha.14" - || rootProject.ouch_version != "0.0.0-alpha.9" + || rootProject.ouch_version != "0.0.0-alpha.10" || migration.source?.pack_version != "26.2.0-alpha.199" || migration.source?.modules != [ambiance: "0.0.0-alpha.11", ouch: "0.0.0-alpha.8"] || migration.target?.pack_version != "26.2.0-alpha.200" @@ -5533,7 +5537,7 @@ tasks.register("verifyCanapliaAlpha209Release") { if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.ambiance_version != expectedTarget.ambiance || rootProject.itsalive_version != "0.0.0-alpha.35" - || rootProject.onlyfun_version != "0.0.0-alpha.21" + || rootProject.onlyfun_version != "0.0.0-alpha.22" || migration.source?.pack_version != "26.2.0-alpha.207" || migration.source?.modules != expectedSource || migration.target?.pack_version != "26.2.0-alpha.208" diff --git a/gradle.properties b/gradle.properties index 9a3548f..7bff6f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,13 +20,13 @@ anotherworld_version=0.0.0-alpha.55 anotherworld_lifecycle=active itsalive_version=0.0.0-alpha.35 itsalive_lifecycle=active -ouch_version=0.0.0-alpha.9 +ouch_version=0.0.0-alpha.10 ouch_lifecycle=active masterkey_version=0.0.0-alpha.8 masterkey_lifecycle=active iliketomoveit_version=0.0.0-alpha.15 iliketomoveit_lifecycle=active -onlyfun_version=0.0.0-alpha.21 +onlyfun_version=0.0.0-alpha.22 onlyfun_lifecycle=active sanctuary_version=0.0.0-alpha.117 sanctuary_lifecycle=active diff --git a/onlyfun/ABJECTION.md b/onlyfun/ABJECTION.md index 65cf2ba..c07513e 100644 --- a/onlyfun/ABJECTION.md +++ b/onlyfun/ABJECTION.md @@ -19,3 +19,11 @@ sauvegardé et est supprimé à la mort, au passage spectateur ou à la déconne Le bloc `abjection:shit_cube` reprend les propriétés de la boue, l’apparence 16×16, la recette neuf boules, le butin et l’outil pelle historiques. Les textes sont disponibles en français, anglais et russe. Only Fun reste en données v2 et protocole réseau 2 pour ABJ-01. + +## ABJ-02 — projectile Ouch + +Only Fun expose l’objet et le point d’installation `AbjectionProjectileApi`. Ouch, qui dépend +explicitement d’Only Fun sans cycle inverse, enregistre le type d’entité sous le même ID historique +`abjection:shit_ball` et installe la fabrique commune au lancer manuel et au dispenser. Le +projectile mesure 0,25 bloc, consomme une unité hors créatif, joue le son de boue à l’impact et +ajoute sept secondes à la nausée déjà présente sans dupliquer d’objet. diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/AbjectionRegistries.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/AbjectionRegistries.java index 63d4e2c..73126f8 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/AbjectionRegistries.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/AbjectionRegistries.java @@ -18,7 +18,7 @@ public final class AbjectionRegistries { public static final Block SHIT_CUBE = registerBlock("shit_cube", Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.MUD)); public static final Item SHIT_BALL = registerItem("shit_ball", - properties -> new Item(properties.stacksTo(16).useItemDescriptionPrefix())); + properties -> new ShitBallItem(properties.stacksTo(16).useItemDescriptionPrefix())); public static final Item SHIT_CUBE_ITEM = registerBlockItem("shit_cube", SHIT_CUBE); private AbjectionRegistries() { diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitBallItem.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitBallItem.java new file mode 100644 index 0000000..b835186 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitBallItem.java @@ -0,0 +1,22 @@ +package fr.koka99cab.sanctuary26.onlyfun.abjection; + +import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi; +import net.minecraft.core.Direction; +import net.minecraft.core.Position; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ProjectileItem; +import net.minecraft.world.level.Level; + +/** Historical item identity with a projectile contract implemented by the owning Ouch module. */ +public final class ShitBallItem extends Item implements ProjectileItem { + public ShitBallItem(Properties properties) { + super(properties); + } + + @Override + public Projectile asProjectile(Level level, Position position, ItemStack stack, Direction direction) { + return AbjectionProjectileApi.create(level, position, stack, direction); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionItems.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionItems.java new file mode 100644 index 0000000..7c3f708 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionItems.java @@ -0,0 +1,19 @@ +package fr.koka99cab.sanctuary26.onlyfun.api.abjection; + +import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +/** Minimal public object contract consumed by Ouch's Abjection projectile implementation. */ +public final class AbjectionItems { + private AbjectionItems() { + } + + public static Item shitBall() { + return AbjectionRegistries.SHIT_BALL; + } + + public static boolean isShitBall(ItemStack stack) { + return stack.is(AbjectionRegistries.SHIT_BALL); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java new file mode 100644 index 0000000..4cb07b1 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java @@ -0,0 +1,39 @@ +package fr.koka99cab.sanctuary26.onlyfun.api.abjection; + +import java.util.Objects; +import net.minecraft.core.Direction; +import net.minecraft.core.Position; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +/** + * Owner-neutral projectile factory contract: Only Fun owns the item, while Ouch installs its + * combat implementation without creating a reverse dependency. + */ +public final class AbjectionProjectileApi { + @FunctionalInterface + public interface Factory { + Projectile create(Level level, Position position, ItemStack stack, Direction direction); + } + + private static Factory factory; + + private AbjectionProjectileApi() { + } + + public static void registerFactory(Factory implementation) { + Objects.requireNonNull(implementation, "implementation"); + if (factory != null) throw new IllegalStateException("Abjection projectile factory already registered"); + factory = implementation; + } + + public static Projectile create(Level level, Position position, ItemStack stack, Direction direction) { + if (factory == null) throw new IllegalStateException("Ouch has not registered the Abjection projectile factory"); + return factory.create(level, position, stack, direction); + } + + public static boolean isReady() { + return factory != null; + } +} diff --git a/ouch/build.gradle b/ouch/build.gradle index 4f85b23..eac4fed 100644 --- a/ouch/build.gradle +++ b/ouch/build.gradle @@ -12,9 +12,10 @@ def miningRifleTextureHash = "4e4295c5c8a8f71d03307809448ff64363eec2d2aefdc538c4 tasks.register("verifyOuch") { group = "verification" - description = "Checks Ouch alpha.9, including the Ambiance-backed Weather TNT and migrated Gunz arsenal." + description = "Checks Ouch alpha.10, including the shared Abjection projectile implementation." inputs.files(fileTree("src/main/java")) inputs.files(fileTree("src/main/resources")) + inputs.files(rootProject.fileTree("onlyfun/src/main/java")) inputs.file(rootProject.file("pack/migrations/26.1.2-gunz-to-26.2.0-alpha.41.json")) inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.99-canon-weather-ownership-to-alpha.100.json")) inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json")) @@ -25,6 +26,32 @@ tasks.register("verifyOuch") { def weatherRegistry = file("src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchWeatherTnt.java").text def weatherEntity = file("src/main/java/fr/koka99cab/sanctuary26/ouch/entity/WeatherTntEntity.java").text def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) + def abjectionRegistry = file( + "src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java").text + def abjectionEntity = file( + "src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java").text + def abjectionClient = file( + "src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java").text + def onlyFunProjectileApi = rootProject.file( + "onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java").text + if (manifest.depends?.onlyfun != ">=0.0.0-alpha.22" + || manifest.custom?.sanctuary26?.data_version != 1 + || manifest.custom?.sanctuary26?.network_protocol != 1 + || !rootProject.file("build.gradle").text.contains('implementation project(":onlyfun")') + || !onlyFunProjectileApi.contains("public interface Factory") + || !abjectionRegistry.contains('AbjectionProjectileApi.registerFactory(') + || !abjectionRegistry.contains('DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall())') + || !abjectionRegistry.contains('UseItemCallback.EVENT.register') + || !abjectionRegistry.contains('stack.consume(1, player)') + || !abjectionRegistry.contains('Projectile.spawnProjectileFromRotation(') + || !abjectionRegistry.contains('.sized(0.25F, 0.25F)') + || !abjectionEntity.contains('NAUSEA_DURATION_TICKS = 7 * 20') + || !abjectionEntity.contains('current == null ? 0 : current.getDuration()') + || !abjectionEntity.contains('SoundEvents.MUD_PLACE') + || !abjectionEntity.contains('discard()') + || !abjectionClient.contains('new ThrownItemRenderer<>(context)')) { + throw new GradleException("Ouch alpha.10 Abjection projectile contract is incomplete") + } if (!itemRegistry.contains("MIGRATED_WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR)") || !itemRegistry.contains("WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR, MINING_RIFLE, BETA_BOW)") || !creativeTab.contains('OuchMod.id("arsenal")') @@ -89,7 +116,7 @@ tasks.register("verifyOuch") { def creeperlockRecipe = new JsonSlurper().parse(file("src/main/resources/data/ouch/recipe/creeperlock.json")) def silverEngineeringMigration = new JsonSlurper().parse(rootProject.file( "pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json")) - if (project.version.toString() != "0.0.0-alpha.9" + if (project.version.toString() != "0.0.0-alpha.10" || mitrailletteRecipe.pattern != ["RRP", "IGI", " B "] || mitrailletteRecipe.key != [B: "minecraft:stone_button", G: "minecraft:redstone", I: "anotherworld:silver_ingot", P: "anotherworld:silver_plate", R: "anotherworld:silver_rod"] @@ -317,10 +344,23 @@ tasks.register("verifyOuch") { throw new GradleException("Runtime Ouch resources must not re-register the legacy gunz namespace: ${forbiddenNamespaceReferences}") } - logger.lifecycle("Ouch verified: 6 enchantable items, 4 custom enchantments, debuffed bases, shared headshots, 6 recipes and FR/EN/RU translations.") + logger.lifecycle("Ouch verified: arsenal, Weather TNT and shared Abjection projectile contract.") } } +tasks.named("test") { + failOnNoDiscoveredTests = false +} + +tasks.register("shitBallContractSmoke", JavaExec) { + group = "verification" + description = "Checks additive and overflow-safe Shit Ball nausea." + dependsOn tasks.named("testClasses") + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.ouch.entity.ShitBallContractSmoke" +} + tasks.named("check") { + dependsOn tasks.named("shitBallContractSmoke") dependsOn tasks.named("verifyOuch") } diff --git a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java index 96d53db..3dd5d8f 100644 --- a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java +++ b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java @@ -2,16 +2,20 @@ package fr.koka99cab.sanctuary26.ouch.client; import fr.koka99cab.sanctuary26.ouch.OuchMod; import fr.koka99cab.sanctuary26.ouch.entity.WeatherTntEntity; +import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles; import fr.koka99cab.sanctuary26.ouch.registry.OuchWeatherTnt; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.entity.TntRenderer; +import net.minecraft.client.renderer.entity.ThrownItemRenderer; import net.minecraft.world.entity.EntityType; public final class OuchClient implements ClientModInitializer { @Override public void onInitializeClient() { + EntityRendererRegistry.register(OuchAbjectionProjectiles.ENTITY, + context -> new ThrownItemRenderer<>(context)); registerWeatherRenderer(OuchWeatherTnt.ENTITY); registerWeatherRenderer(OuchWeatherTnt.REDSTONER_COMPAT_ENTITY); registerWeatherRenderer(OuchWeatherTnt.LEGACY_ENTITY); diff --git a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java new file mode 100644 index 0000000..f0168e4 --- /dev/null +++ b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java @@ -0,0 +1,62 @@ +package fr.koka99cab.sanctuary26.ouch.entity; + +import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems; +import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrowableItemProjectile; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; + +/** Server-authoritative historical Abjection projectile, owned by Ouch. */ +public final class ShitBallEntity extends ThrowableItemProjectile { + public static final int NAUSEA_DURATION_TICKS = 7 * 20; + + public ShitBallEntity(EntityType type, Level level) { + super(type, level); + } + + public ShitBallEntity(Level level, LivingEntity owner, ItemStack stack) { + super(OuchAbjectionProjectiles.ENTITY, owner, level, stack.copyWithCount(1)); + } + + public ShitBallEntity(Level level, double x, double y, double z, ItemStack stack) { + super(OuchAbjectionProjectiles.ENTITY, x, y, z, level, stack.copyWithCount(1)); + } + + @Override + protected Item getDefaultItem() { + return AbjectionItems.shitBall(); + } + + @Override + protected void onHitEntity(EntityHitResult hit) { + super.onHitEntity(hit); + if (!(level() instanceof ServerLevel) || !(hit.getEntity() instanceof LivingEntity target)) return; + MobEffectInstance current = target.getEffect(MobEffects.NAUSEA); + int duration = extendedNauseaDuration(current == null ? 0 : current.getDuration()); + target.addEffect(new MobEffectInstance(MobEffects.NAUSEA, duration), getOwner()); + } + + @Override + protected void onHit(HitResult hit) { + super.onHit(hit); + if (level() instanceof ServerLevel level) { + level.playSound(null, getX(), getY(), getZ(), SoundEvents.MUD_PLACE, + SoundSource.PLAYERS, 1.0F, 0.75F); + discard(); + } + } + + static int extendedNauseaDuration(int currentDuration) { + return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, currentDuration) + NAUSEA_DURATION_TICKS); + } +} diff --git a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java new file mode 100644 index 0000000..4ab641b --- /dev/null +++ b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java @@ -0,0 +1,70 @@ +package fr.koka99cab.sanctuary26.ouch.registry; + +import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries; +import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems; +import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi; +import fr.koka99cab.sanctuary26.ouch.entity.ShitBallEntity; +import net.fabricmc.fabric.api.event.player.UseItemCallback; +import net.minecraft.core.Direction; +import net.minecraft.core.Position; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; + +/** Shared manual/dispenser creation path for {@code abjection:shit_ball}. */ +public final class OuchAbjectionProjectiles { + private static final float SHOOT_POWER = 1.5F; + private static final ResourceKey> KEY = ResourceKey.create( + Registries.ENTITY_TYPE, AbjectionRegistries.id("shit_ball")); + public static final EntityType ENTITY = Registry.register( + BuiltInRegistries.ENTITY_TYPE, KEY, + EntityType.Builder.of(ShitBallEntity::new, MobCategory.MISC) + .sized(0.25F, 0.25F).clientTrackingRange(4).updateInterval(10).build(KEY)); + private static boolean initialized; + + private OuchAbjectionProjectiles() { + } + + public static void initialize() { + if (initialized) return; + initialized = true; + AbjectionProjectileApi.registerFactory(OuchAbjectionProjectiles::createFromDispenser); + DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall()); + UseItemCallback.EVENT.register((player, level, hand) -> { + ItemStack stack = player.getItemInHand(hand); + if (!AbjectionItems.isShitBall(stack) || player.isSpectator()) return InteractionResult.PASS; + if (level.isClientSide()) return InteractionResult.SUCCESS; + if (!(level instanceof ServerLevel serverLevel)) return InteractionResult.FAIL; + serverLevel.playSound(null, player.getX(), player.getY(), player.getZ(), + SoundEvents.SNOWBALL_THROW, SoundSource.NEUTRAL, 0.5F, + 0.4F / (serverLevel.getRandom().nextFloat() * 0.4F + 0.8F)); + Projectile.spawnProjectileFromRotation(OuchAbjectionProjectiles::createFromPlayer, + serverLevel, stack, player, 0.0F, SHOOT_POWER, 1.0F); + player.awardStat(Stats.ITEM_USED.get(AbjectionItems.shitBall())); + stack.consume(1, player); + return InteractionResult.SUCCESS_SERVER; + }); + } + + public static ShitBallEntity createFromPlayer(ServerLevel level, LivingEntity owner, ItemStack stack) { + return new ShitBallEntity(level, owner, stack); + } + + public static Projectile createFromDispenser(Level level, Position position, ItemStack stack, + Direction direction) { + return new ShitBallEntity(level, position.x(), position.y(), position.z(), stack); + } +} diff --git a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchRegistries.java b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchRegistries.java index 276277c..2352350 100644 --- a/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchRegistries.java +++ b/ouch/src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchRegistries.java @@ -19,6 +19,7 @@ public final class OuchRegistries { initialized = true; OuchItems.initialize(); OuchWeatherTnt.initialize(); + OuchAbjectionProjectiles.initialize(); OuchCreativeTab.initialize(); OuchMod.LOGGER.debug("[{}] Weapon registry hub initialized.", OuchMod.DISPLAY_NAME); } diff --git a/ouch/src/main/resources/fabric.mod.json b/ouch/src/main/resources/fabric.mod.json index 6a18936..7f9bfe0 100644 --- a/ouch/src/main/resources/fabric.mod.json +++ b/ouch/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "fabric-api": ">=0.154.2+26.2", "anotherworld": ">=0.0.0-alpha.46", "ambiance": ">=0.0.0-alpha.12", + "onlyfun": ">=0.0.0-alpha.22", "sanctuary": ">=0.0.0-alpha.110" }, "custom": { diff --git a/ouch/src/test/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallContractSmoke.java b/ouch/src/test/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallContractSmoke.java new file mode 100644 index 0000000..878afcd --- /dev/null +++ b/ouch/src/test/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallContractSmoke.java @@ -0,0 +1,27 @@ +package fr.koka99cab.sanctuary26.ouch.entity; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; + +/** Executable regression test for additive and overflow-safe nausea duration. */ +public final class ShitBallContractSmoke { + private ShitBallContractSmoke() { + } + + public static void main(String[] args) { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + require(ShitBallEntity.NAUSEA_DURATION_TICKS == 140, "historical duration changed"); + require(ShitBallEntity.extendedNauseaDuration(0) == 140, "first hit duration is wrong"); + require(ShitBallEntity.extendedNauseaDuration(60) == 200, + "a new impact did not add seven seconds"); + require(ShitBallEntity.extendedNauseaDuration(-10) == 140, + "negative input was not normalized"); + require(ShitBallEntity.extendedNauseaDuration(Integer.MAX_VALUE) == Integer.MAX_VALUE, + "duration overflow was not saturated"); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +} -- 2.54.0