feat(onlyfun): intégrer les actions Abjection #76

Merged
koka merged 2 commits from feature/abjection-actions into main 2026-08-29 16:37:00 +00:00
25 changed files with 717 additions and 9 deletions
+4 -1
View File
@@ -8,7 +8,7 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
## Non distribué
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`** et **Ambiance `0.0.0-alpha.15`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`** et **Only Fun `0.0.0-alpha.22`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
- remplace les pages noires du Livre interdit par laffichage clair du livre vanilla ;
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
@@ -17,6 +17,9 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11
- ajoute le disque blanc, à fabriquer puis renommer avec une URL YouTube HTTPS avant de linsérer dans un jukebox ;
- valide strictement lhôte et lidentifiant vidéo côté serveur, ne transmet quun identifiant borné de onze caractères et limite chaque client à quatre lectures de trente minutes maximum ;
- diffuse le son en mono positionnel dans le canal des disques, sans nouvelle donnée persistante ; Ambiance conserve ses données v2 et passe son protocole réseau de 5 à 6.
- importe le cœur GPL historique dAbjection 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 dOnly Fun.
## `26.2.0-alpha.210`
+52 -7
View File
@@ -3439,7 +3439,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
}
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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"]
@@ -3636,9 +3636,17 @@ project(":onlyfun") {
mainClass = "fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunGeneratorSmoke"
}
tasks.register("abjectionActionContractSmoke", JavaExec) {
group = "verification"
description = "Checks the bounded historical Abjection action contract."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActionContractSmoke"
}
tasks.register("verifyOnlyFun") {
group = "verification"
description = "Checks Only Fun alpha.19 lootboxes, CSV Shop catalog and anchored Cobble Dungeons."
description = "Checks Only Fun alpha.21, including the historical Abjection server actions."
inputs.files(fileTree("src/main/java"))
inputs.files(fileTree("src/main/resources"))
inputs.file(file("art/shop/shop_catalog_editable.csv"))
@@ -3721,9 +3729,45 @@ project(":onlyfun") {
def shopCsv = file("art/shop/shop_catalog_editable.csv")
def shopCsvText = shopCsv.getText("UTF-8")
def shopCsvLines = shopCsv.readLines("UTF-8")
def abjectionLanguages = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file(
"src/main/resources/assets/abjection/lang/${locale}.json"))]
}
def shitCubeTexture = javax.imageio.ImageIO.read(file(
"src/main/resources/assets/abjection/textures/block/shit_cube.png"))
def shitBallTexture = javax.imageio.ImageIO.read(file(
"src/main/resources/assets/abjection/textures/item/shit_ball.png"))
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
|| !javaText.contains('NAMESPACE = "abjection"')
|| !javaText.contains('public static StartResult start(ServerPlayer player, Action action)')
|| !javaText.contains('PISS(15 * 20, "piss")')
|| !javaText.contains('VOMIT(8 * 20, "vomit")')
|| !javaText.contains('SHIT(6 * 20, "shit")')
|| !javaText.contains('SMOKE(10 * 20, "smoke")')
|| !javaText.contains('ServerPlayConnectionEvents.DISCONNECT')
|| !javaText.contains('action == Action.SHIT && !restarted')
|| !javaText.contains('new ItemStack(AbjectionRegistries.SHIT_BALL)')
|| !javaText.contains('BlockBehaviour.Properties.ofFullCopy(Blocks.MUD)')
|| shitCubeTexture == null || shitCubeTexture.width != 16 || shitCubeTexture.height != 16
|| shitBallTexture == null || shitBallTexture.width != 16 || shitBallTexture.height != 16
|| shitCubeRecipe.key?.get("#") != "abjection:shit_ball"
|| shitCubeRecipe.result?.id != "abjection:shit_cube"
|| !file("src/main/resources/data/abjection/loot_table/blocks/shit_cube.json").isFile()
|| !file("src/main/resources/data/minecraft/tags/block/mineable/shovel.json").text
.contains('"abjection:shit_cube"')
|| abjectionLanguages.any { locale, translations ->
!["item.abjection.shit_ball", "block.abjection.shit_cube",
"command.abjection.piss.started", "command.abjection.vomit.started",
"command.abjection.shit.started", "command.abjection.smoke.started"].every {
translations.containsKey(it)
}
}
|| !javaText.contains("ANOMALY_DENOMINATOR = 256")
|| !javaText.contains("PITY_START = 24")
|| !javaText.contains("class LootboxItem")
@@ -3870,6 +3914,7 @@ project(":onlyfun") {
}
tasks.named("check") {
dependsOn tasks.named("abjectionActionContractSmoke")
dependsOn tasks.named("onlyFunGeneratorSmoke")
dependsOn tasks.named("verifyOnlyFun")
}
@@ -4136,7 +4181,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.210"
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|| rootProject.onlyfun_version != "0.0.0-alpha.22"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.192"
@@ -4230,7 +4275,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.210"
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|| rootProject.onlyfun_version != "0.0.0-alpha.22"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.194"
@@ -5499,7 +5544,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| 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"
@@ -5607,7 +5652,7 @@ tasks.register("verifyAlpha210Release") {
}
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| rootProject.onlyfun_version != expectedTarget.onlyfun
|| rootProject.onlyfun_version != "0.0.0-alpha.22"
|| rootProject.iliketomoveit_version != expectedTarget.iliketomoveit
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| migration.source?.pack_version != "26.2.0-alpha.209"
+1 -1
View File
@@ -26,7 +26,7 @@ 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.119
sanctuary_lifecycle=active
+22
View File
@@ -0,0 +1,22 @@
# Abjection dans Only Fun
Le runtime historique GPL-3.0-or-later de `Fun/abjection` est intégré au JAR **Only Fun**. Le
namespace `abjection` et les IDs `abjection:shit_ball` et `abjection:shit_cube` sont conservés ; le
manifeste Only Fun fournit lalias Fabric `abjection`, sans créer un onzième module.
## ABJ-01 — actions serveur
- `/piss` émet son jet pendant 15 secondes et peut activer un bouton touché ;
- `/vomit` émet son jet pendant 8 secondes ;
- `/shit` émet son jet pendant 6 secondes et crée régulièrement la même
`abjection:shit_ball` que les lots suivants ;
- `/smoke` émet de la fumée pendant 10 secondes.
`AbjectionActions.start` est lunique API publique de déclenchement. Chaque nouvelle demande
redémarre seulement laction correspondante, sans dupliquer la boule initiale de `/shit`. L’état
reste en mémoire serveur, nest jamais sauvegardé et est supprimé à la mort, au passage spectateur
ou à la déconnexion.
Le bloc `abjection:shit_cube` reprend les propriétés de la boue, lapparence 16×16, la recette
neuf boules, le butin et loutil 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.
@@ -1,6 +1,9 @@
package fr.koka99cab.sanctuary26.onlyfun;
import fr.koka99cab.sanctuary26.onlyfun.chickencarry.ChickenCarryController;
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
import fr.koka99cab.sanctuary26.onlyfun.command.AbjectionCommands;
import fr.koka99cab.sanctuary26.onlyfun.network.OnlyFunNetworking;
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunCreativeTab;
@@ -31,6 +34,7 @@ public final class OnlyFunMod implements ModInitializer {
@Override
public void onInitialize() {
AbjectionRegistries.initialize();
OnlyFunRegistries.initialize();
OnlyFunCreativeTab.initialize();
OnlyFunGenerator.initialize();
@@ -42,6 +46,8 @@ public final class OnlyFunMod implements ModInitializer {
OnlyFunNetworking.initialize();
ChickenCarryController.register();
OnlyFunCommands.register();
AbjectionActions.initialize();
AbjectionCommands.register();
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
}
@@ -0,0 +1,52 @@
package fr.koka99cab.sanctuary26.onlyfun.abjection;
import java.util.function.Function;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
/** Historical Abjection registry IDs, owned and distributed by Only Fun. */
public final class AbjectionRegistries {
public static final String NAMESPACE = "abjection";
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()));
public static final Item SHIT_CUBE_ITEM = registerBlockItem("shit_cube", SHIT_CUBE);
private AbjectionRegistries() {
}
public static Identifier id(String path) {
return Identifier.fromNamespaceAndPath(NAMESPACE, path);
}
public static void initialize() {
// Class initialization owns registration; this method makes the startup order explicit.
}
private static Item registerItem(String path, Function<Item.Properties, Item> factory) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
return Registry.register(BuiltInRegistries.ITEM, key,
factory.apply(new Item.Properties().setId(key)));
}
private static Block registerBlock(String path, Function<BlockBehaviour.Properties, Block> factory,
BlockBehaviour.Properties properties) {
ResourceKey<Block> key = ResourceKey.create(Registries.BLOCK, id(path));
return Registry.register(BuiltInRegistries.BLOCK, key, factory.apply(properties.setId(key)));
}
private static Item registerBlockItem(String path, Block block) {
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
return Registry.register(BuiltInRegistries.ITEM, key,
new BlockItem(block, new Item.Properties().setId(key).useBlockDescriptionPrefix()));
}
}
@@ -0,0 +1,383 @@
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ColorParticleOption;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.core.particles.ItemParticleOption;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.ButtonBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
/** Public server API shared by Abjection commands and bounded client requests. */
public final class AbjectionActions {
public enum Action {
PISS(15 * 20, "piss"),
VOMIT(8 * 20, "vomit"),
SHIT(6 * 20, "shit"),
SMOKE(10 * 20, "smoke");
private final int durationTicks;
private final String id;
Action(int durationTicks, String id) {
this.durationTicks = durationTicks;
this.id = id;
}
public int durationTicks() {
return durationTicks;
}
public String id() {
return id;
}
}
public enum StartResult {
STARTED,
RESTARTED,
REJECTED
}
private static final Map<UUID, EnumMap<Action, Integer>> ACTIVE = new HashMap<>();
private static final int STREAM_SEGMENTS = 64;
private static final double STREAM_STEP_LENGTH = 0.05D;
private static final double STREAM_STEP_SCALE = STREAM_STEP_LENGTH / 0.28D;
private static final DustParticleOptions PISS_BRIGHT = new DustParticleOptions(0xEDD62E, 0.24F);
private static final DustParticleOptions PISS_GOLD = new DustParticleOptions(0xD8B825, 0.2F);
private static final DustParticleOptions PISS_AMBER = new DustParticleOptions(0xC6941A, 0.19F);
private static final DustParticleOptions PISS_DEEP_GOLD = new DustParticleOptions(0xB88714, 0.17F);
private static final DustParticleOptions VOMIT_GREEN = new DustParticleOptions(0x88A436, 0.24F);
private static final DustParticleOptions VOMIT_YELLOW = new DustParticleOptions(0xB6B342, 0.2F);
private static final DustParticleOptions VOMIT_ORANGE = new DustParticleOptions(0xA96A32, 0.19F);
private static final DustParticleOptions VOMIT_BROWN = new DustParticleOptions(0x6E5828, 0.17F);
private static final DustParticleOptions SHIT_SPLAT = new DustParticleOptions(0x4A2B14, 1.4F);
private static final DustParticleOptions SHIT_MUD = new DustParticleOptions(0x6A4521, 0.22F);
private static final ColorParticleOption PISS_BRIGHT_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xF0E35A);
private static final ColorParticleOption PISS_GOLD_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xD8C43A);
private static final ColorParticleOption PISS_AMBER_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xCDA128);
private static final ColorParticleOption VOMIT_GREEN_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0x95B33F);
private static final ColorParticleOption VOMIT_YELLOW_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xC4C35B);
private static final ColorParticleOption VOMIT_ORANGE_MIST =
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xB97A3D);
private static final ItemParticleOption VOMIT_FLESH =
new ItemParticleOption(ParticleTypes.ITEM, Items.ROTTEN_FLESH);
private static final ItemParticleOption VOMIT_EYE =
new ItemParticleOption(ParticleTypes.ITEM, Items.SPIDER_EYE);
private static final ItemParticleOption VOMIT_POTATO =
new ItemParticleOption(ParticleTypes.ITEM, Items.POISONOUS_POTATO);
private static boolean initialized;
private AbjectionActions() {
}
public static void initialize() {
if (initialized) return;
initialized = true;
ServerTickEvents.END_SERVER_TICK.register(AbjectionActions::tick);
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> clear(handler.player.getUUID()));
}
public static StartResult start(ServerPlayer player, Action action) {
if (player == null || action == null || !player.isAlive() || player.isSpectator()
|| player.level().getServer() == null) return StartResult.REJECTED;
boolean restarted = restart(player.getUUID(), action);
if (action == Action.SHIT && !restarted) spawnShitDrop(player.level(), player);
return restarted ? StartResult.RESTARTED : StartResult.STARTED;
}
static boolean restart(UUID playerId, Action action) {
EnumMap<Action, Integer> actions = ACTIVE.computeIfAbsent(playerId, ignored ->
new EnumMap<>(Action.class));
return actions.put(action, action.durationTicks()) != null;
}
public static void clear(UUID playerId) {
ACTIVE.remove(playerId);
}
static int remaining(UUID playerId, Action action) {
return ACTIVE.getOrDefault(playerId, new EnumMap<>(Action.class)).getOrDefault(action, 0);
}
private static void tick(MinecraftServer server) {
Iterator<Map.Entry<UUID, EnumMap<Action, Integer>>> players = ACTIVE.entrySet().iterator();
while (players.hasNext()) {
Map.Entry<UUID, EnumMap<Action, Integer>> entry = players.next();
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
if (player == null || !player.isAlive() || player.isSpectator()) {
players.remove();
continue;
}
Iterator<Map.Entry<Action, Integer>> actions = entry.getValue().entrySet().iterator();
while (actions.hasNext()) {
Map.Entry<Action, Integer> action = actions.next();
emit(player, action.getKey(), action.getValue());
int remaining = action.getValue() - 1;
if (remaining <= 0) actions.remove();
else action.setValue(remaining);
}
if (entry.getValue().isEmpty()) players.remove();
}
}
private static void emit(ServerPlayer player, Action action, int remaining) {
switch (action) {
case PISS -> spawnPissStream(player);
case VOMIT -> spawnVomitStream(player);
case SHIT -> spawnShitStream(player, remaining);
case SMOKE -> spawnSmoke(player);
}
}
private static void spawnPissStream(ServerPlayer player) {
ServerLevel level = player.level();
Vec3 position = pissOrigin(player);
Vec3 velocity = pissVelocity(player);
for (int step = 0; step < STREAM_SEGMENTS; step++) {
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
Vec3 midpoint = position.lerp(next, 0.5D);
level.sendParticles(pissCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
0.012D, 0.012D, 0.012D, 0.0D);
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? PISS_DEEP_GOLD : PISS_BRIGHT,
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
if (step % 3 == 0) level.sendParticles(pissMist(step), midpoint.x, midpoint.y, midpoint.z,
2, 0.012D, 0.012D, 0.012D, 0.0D);
HitResult hit = clip(level, player, position, next);
if (hit.getType() != HitResult.Type.MISS) {
activateButton(level, player, hit);
pissImpact(level, hit.getLocation());
return;
}
position = next;
velocity = velocity.add(0.0D, -0.03D * STREAM_STEP_SCALE, 0.0D)
.scale(Math.pow(0.985D, STREAM_STEP_SCALE));
if (position.y < level.getMinY()) return;
}
}
private static void spawnVomitStream(ServerPlayer player) {
ServerLevel level = player.level();
Vec3 position = vomitOrigin(player);
Vec3 velocity = vomitVelocity(player);
level.sendParticles(ParticleTypes.WHITE_SMOKE, position.x, position.y, position.z,
2, 0.012D, 0.01D, 0.012D, 0.002D);
for (int step = 0; step < STREAM_SEGMENTS; step++) {
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
Vec3 midpoint = position.lerp(next, 0.5D);
level.sendParticles(vomitCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
0.012D, 0.012D, 0.012D, 0.0D);
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? VOMIT_BROWN : VOMIT_GREEN,
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
HitResult hit = clip(level, player, position, next);
if (hit.getType() != HitResult.Type.MISS) {
vomitImpact(level, hit.getLocation());
return;
}
position = next;
velocity = velocity.add(0.0D, -0.038D * STREAM_STEP_SCALE, 0.0D)
.scale(Math.pow(0.982D, STREAM_STEP_SCALE));
if (position.y < level.getMinY()) return;
}
}
private static void spawnShitStream(ServerPlayer player, int remaining) {
ServerLevel level = player.level();
Vec3 position = shitOrigin(player);
Vec3 velocity = shitVelocity(player);
if (remaining < Action.SHIT.durationTicks() && remaining % 20 == 0) spawnShitDrop(level, player);
for (int step = 0; step < STREAM_SEGMENTS; step++) {
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
Vec3 midpoint = position.lerp(next, 0.5D);
float progress = step / (float) (STREAM_SEGMENTS - 1);
float spread = 0.002F + progress * 0.018F;
level.sendParticles(new DustParticleOptions(0x5B3417,
Mth.lerp(progress * progress, 0.05F, 1.55F)), midpoint.x, midpoint.y, midpoint.z,
1, spread, spread, spread, 0.0D);
if ((step & 1) == 0) level.sendParticles(new DustParticleOptions(0x75461F,
Mth.lerp(progress * progress, 0.04F, 0.85F)), midpoint.x, midpoint.y, midpoint.z,
1, spread, spread, spread, 0.0D);
HitResult hit = clip(level, player, position, next);
if (hit.getType() != HitResult.Type.MISS) {
level.sendParticles(SHIT_SPLAT, hit.getLocation().x, hit.getLocation().y,
hit.getLocation().z, 2, 0.03D, 0.02D, 0.03D, 0.0D);
level.sendParticles(SHIT_MUD, hit.getLocation().x, hit.getLocation().y,
hit.getLocation().z, 4, 0.04D, 0.025D, 0.04D, 0.0D);
return;
}
position = next;
velocity = velocity.add(0.0D, -0.032D * STREAM_STEP_SCALE, 0.0D)
.scale(Math.pow(0.986D, STREAM_STEP_SCALE));
if (position.y < level.getMinY()) return;
}
}
private static void spawnSmoke(ServerPlayer player) {
ServerLevel level = player.level();
Vec3 mouth = mouthOrigin(player);
level.sendParticles(ParticleTypes.SMOKE, mouth.x, mouth.y, mouth.z,
2, 0.015D, 0.01D, 0.015D, 0.003D);
if (level.getRandom().nextInt(3) == 0) level.sendParticles(ParticleTypes.WHITE_SMOKE,
mouth.x, mouth.y, mouth.z, 1, 0.01D, 0.008D, 0.01D, 0.002D);
}
private static HitResult clip(ServerLevel level, ServerPlayer player, Vec3 from, Vec3 to) {
return level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.ANY, player));
}
private static Vec3 pissOrigin(ServerPlayer player) {
Vec3 forward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize();
Vec3 right = new Vec3(-forward.z, 0.0D, forward.x);
double side = player.getMainArm() == HumanoidArm.RIGHT ? -0.12D : 0.12D;
return player.position().add(0.0D, player.getBbHeight() * 0.55D, 0.0D)
.add(forward.scale(0.24D)).add(right.scale(side));
}
private static Vec3 pissVelocity(ServerPlayer player) {
float pitch = Mth.clamp(player.getXRot() + 22.0F, 6.0F, 55.0F);
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(1.05D)
.add(player.getDeltaMovement().multiply(0.18D, 0.0D, 0.18D));
}
private static Vec3 vomitOrigin(ServerPlayer player) {
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
.add(forward.scale(0.18D));
}
private static Vec3 vomitVelocity(ServerPlayer player) {
float pitch = Mth.clamp(player.getXRot() + 8.0F, -18.0F, 42.0F);
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(0.8D)
.add(player.getDeltaMovement().multiply(0.15D, 0.0D, 0.15D));
}
private static Vec3 shitOrigin(ServerPlayer player) {
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
return player.position().add(0.0D, player.getBbHeight() * 0.44D, 0.0D)
.add(backward.scale(0.16D));
}
private static Vec3 shitVelocity(ServerPlayer player) {
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
return backward.scale(0.68D).add(0.0D, 0.08D, 0.0D)
.add(player.getDeltaMovement().multiply(0.1D, 0.0D, 0.1D));
}
private static Vec3 mouthOrigin(ServerPlayer player) {
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
.add(forward.scale(0.12D));
}
private static void spawnShitDrop(ServerLevel level, ServerPlayer player) {
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
Vec3 position = shitOrigin(player).add(backward.scale(0.08D)).add(0.0D, -0.08D, 0.0D);
Vec3 velocity = backward.scale(0.05D).add(0.0D, 0.02D, 0.0D);
ItemEntity item = new ItemEntity(level, position.x, position.y, position.z,
new ItemStack(AbjectionRegistries.SHIT_BALL), velocity.x, velocity.y, velocity.z);
item.setPickUpDelay(10);
item.setThrower(player);
level.addFreshEntity(item);
}
private static DustParticleOptions pissCore(int step) {
return switch (step % 3) {
case 0 -> PISS_BRIGHT;
case 1 -> PISS_GOLD;
default -> PISS_AMBER;
};
}
private static ColorParticleOption pissMist(int step) {
return switch (step % 3) {
case 0 -> PISS_BRIGHT_MIST;
case 1 -> PISS_GOLD_MIST;
default -> PISS_AMBER_MIST;
};
}
private static DustParticleOptions vomitCore(int step) {
return switch (step % 3) {
case 0 -> VOMIT_GREEN;
case 1 -> VOMIT_YELLOW;
default -> VOMIT_ORANGE;
};
}
private static ItemParticleOption vomitChunk(ServerLevel level) {
return switch (level.getRandom().nextInt(3)) {
case 0 -> VOMIT_FLESH;
case 1 -> VOMIT_EYE;
default -> VOMIT_POTATO;
};
}
private static void activateButton(ServerLevel level, ServerPlayer player, HitResult hit) {
if (!(hit instanceof BlockHitResult blockHit)) return;
BlockPos pos = blockHit.getBlockPos();
BlockState state = level.getBlockState(pos);
if (state.getBlock() instanceof ButtonBlock button && !state.getValue(ButtonBlock.POWERED)) {
button.press(state, level, pos, player);
}
}
private static void pissImpact(ServerLevel level, Vec3 position) {
for (DustParticleOptions particle : new DustParticleOptions[] {PISS_BRIGHT, PISS_GOLD, PISS_AMBER}) {
level.sendParticles(particle, position.x, position.y, position.z, 3,
0.02D, 0.02D, 0.02D, 0.0D);
}
for (ColorParticleOption particle : new ColorParticleOption[] {
PISS_BRIGHT_MIST, PISS_GOLD_MIST, PISS_AMBER_MIST}) {
level.sendParticles(particle, position.x, position.y, position.z, 2,
0.03D, 0.022D, 0.03D, 0.0D);
}
level.sendParticles(ParticleTypes.BUBBLE_POP, position.x, position.y, position.z,
8, 0.05D, 0.03D, 0.05D, 0.01D);
}
private static void vomitImpact(ServerLevel level, Vec3 position) {
for (DustParticleOptions particle : new DustParticleOptions[] {
VOMIT_GREEN, VOMIT_YELLOW, VOMIT_ORANGE, VOMIT_BROWN}) {
level.sendParticles(particle, position.x, position.y, position.z, 3,
0.02D, 0.02D, 0.02D, 0.0D);
}
for (ColorParticleOption particle : new ColorParticleOption[] {
VOMIT_GREEN_MIST, VOMIT_YELLOW_MIST, VOMIT_ORANGE_MIST}) {
level.sendParticles(particle, position.x, position.y, position.z, 2,
0.03D, 0.022D, 0.03D, 0.0D);
}
level.sendParticles(ParticleTypes.SPLASH, position.x, position.y, position.z,
4, 0.035D, 0.015D, 0.035D, 0.01D);
level.sendParticles(ParticleTypes.SNEEZE, position.x, position.y, position.z,
2, 0.03D, 0.02D, 0.03D, 0.0D);
level.sendParticles(ParticleTypes.ITEM_SLIME, position.x, position.y, position.z,
3, 0.03D, 0.02D, 0.03D, 0.0D);
for (int index = 0; index < 2; index++) level.sendParticles(vomitChunk(level),
position.x, position.y, position.z, 1, 0.03D, 0.02D, 0.03D, 0.0D);
}
}
@@ -0,0 +1,36 @@
package fr.koka99cab.sanctuary26.onlyfun.command;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
/** Historical root commands, routed through the shared server action API. */
public final class AbjectionCommands {
private AbjectionCommands() {
}
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
for (AbjectionActions.Action action : AbjectionActions.Action.values()) {
dispatcher.register(Commands.literal(action.id())
.executes(context -> start(context.getSource(), action)));
}
});
}
private static int start(CommandSourceStack source, AbjectionActions.Action action)
throws CommandSyntaxException {
ServerPlayer player = source.getPlayerOrException();
AbjectionActions.StartResult result = AbjectionActions.start(player, action);
if (result == AbjectionActions.StartResult.REJECTED) return 0;
String suffix = result == AbjectionActions.StartResult.RESTARTED ? "restarted" : "started";
source.sendSuccess(() -> Component.translatable(
"command.abjection." + action.id() + "." + suffix), false);
return Command.SINGLE_SUCCESS;
}
}
@@ -1,6 +1,7 @@
package fr.koka99cab.sanctuary26.onlyfun.registry;
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
@@ -27,6 +28,8 @@ public final class OnlyFunCreativeTab {
output.accept(OnlyFunRegistries.CAPE_LOOTBOX);
output.accept(OnlyFunRegistries.SPAWN_EGG_LOOTBOX);
output.accept(OnlyFunRegistries.ANOMALY_LOOTBOX);
output.accept(AbjectionRegistries.SHIT_BALL);
output.accept(AbjectionRegistries.SHIT_CUBE_ITEM);
})
.build()
);
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "abjection:block/shit_cube"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "abjection:item/shit_ball"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "abjection:item/shit_cube"
}
}
@@ -0,0 +1,12 @@
{
"item.abjection.shit_ball": "Shit Ball",
"block.abjection.shit_cube": "Shit Cube",
"command.abjection.piss.started": "Here we go for 15 seconds.",
"command.abjection.piss.restarted": "The timer restarts for 15 seconds.",
"command.abjection.vomit.started": "Yuck. You vomit for 8 seconds.",
"command.abjection.vomit.restarted": "You vomit again for 8 seconds.",
"command.abjection.shit.started": "You shit in a stream for 6 seconds.",
"command.abjection.shit.restarted": "The shit stream restarts for 6 seconds.",
"command.abjection.smoke.started": "You blow smoke for 10 seconds.",
"command.abjection.smoke.restarted": "You keep smoking for 10 seconds."
}
@@ -0,0 +1,12 @@
{
"item.abjection.shit_ball": "Boule de merde",
"block.abjection.shit_cube": "Cube de merde",
"command.abjection.piss.started": "C'est parti pour 15 secondes.",
"command.abjection.piss.restarted": "Le timer repart pour 15 secondes.",
"command.abjection.vomit.started": "Beurk. Tu vomis pendant 8 secondes.",
"command.abjection.vomit.restarted": "Tu revomis pour 8 secondes.",
"command.abjection.shit.started": "Tu chies en jet pendant 6 secondes.",
"command.abjection.shit.restarted": "Le jet de merde repart pour 6 secondes.",
"command.abjection.smoke.started": "Tu souffles de la fumée pendant 10 secondes.",
"command.abjection.smoke.restarted": "Tu continues de fumer pendant 10 secondes."
}
@@ -0,0 +1,12 @@
{
"item.abjection.shit_ball": "Шарик дерьма",
"block.abjection.shit_cube": "Куб дерьма",
"command.abjection.piss.started": "Струя запущена на 15 секунд.",
"command.abjection.piss.restarted": "Таймер струи перезапущен на 15 секунд.",
"command.abjection.vomit.started": "Фу. Тебя рвёт 8 секунд.",
"command.abjection.vomit.restarted": "Тебя снова рвёт 8 секунд.",
"command.abjection.shit.started": "Ты испражняешься струёй 6 секунд.",
"command.abjection.shit.restarted": "Струя дерьма перезапущена на 6 секунд.",
"command.abjection.smoke.started": "Ты выпускаешь дым 10 секунд.",
"command.abjection.smoke.restarted": "Ты продолжаешь курить ещё 10 секунд."
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "abjection:block/shit_cube"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "abjection:item/shit_ball"
}
}
@@ -0,0 +1,3 @@
{
"parent": "abjection:block/shit_cube"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "abjection:shit_cube"
}
],
"rolls": 1.0
}
],
"random_sequence": "abjection:blocks/shit_cube"
}
@@ -0,0 +1,16 @@
{
"type": "minecraft:crafting_shaped",
"category": "building",
"key": {
"#": "abjection:shit_ball"
},
"pattern": [
"###",
"###",
"###"
],
"result": {
"count": 1,
"id": "abjection:shit_cube"
}
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"abjection:shit_cube"
]
}
@@ -8,6 +8,9 @@
"KOKA99CAB"
],
"license": "GPL-3.0-or-later",
"provides": [
"abjection"
],
"icon": "assets/onlyfun/icon.png",
"environment": "*",
"entrypoints": {
@@ -0,0 +1,42 @@
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
import java.util.Set;
import java.util.UUID;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
/** Executable regression check for the bounded historical action contract. */
public final class AbjectionActionContractSmoke {
private AbjectionActionContractSmoke() {
}
public static void main(String[] args) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
require(AbjectionActions.Action.PISS.durationTicks() == 300, "piss duration changed");
require(AbjectionActions.Action.VOMIT.durationTicks() == 160, "vomit duration changed");
require(AbjectionActions.Action.SHIT.durationTicks() == 120, "shit duration changed");
require(AbjectionActions.Action.SMOKE.durationTicks() == 200, "smoke duration changed");
require(Set.of("piss", "vomit", "shit", "smoke").equals(Set.of(
AbjectionActions.Action.PISS.id(), AbjectionActions.Action.VOMIT.id(),
AbjectionActions.Action.SHIT.id(), AbjectionActions.Action.SMOKE.id())),
"historical command surface changed");
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000057");
require(!AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
"first action incorrectly marked as a restart");
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
"initial duration was not installed");
require(AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
"second action was not marked as a restart");
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
"restart did not reset the full duration");
AbjectionActions.clear(playerId);
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 0,
"cleanup left a temporary action behind");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}