diff --git a/build.gradle b/build.gradle index 01ef370..ae4c8cf 100644 --- a/build.gradle +++ b/build.gradle @@ -3916,11 +3916,11 @@ project(":onlyfun") { "src/main/java/fr/koka99cab/sanctuary26/onlyfun/world/CobbleDungeonStructure.java").text def dungeonStructureSet = new JsonSlurper().parse(file( "src/main/resources/data/onlyfun/worldgen/structure_set/cobble_dungeons.json")) - def expectedItems = ["normal_lootbox", "cape_lootbox", "spawn_egg_lootbox", "anomaly_lootbox", "lucky_block"] + def expectedItems = ["normal_lootbox", "cape_lootbox", "spawn_egg_lootbox", "anomaly_lootbox", "lucky_block", "nourra_firework"] def missingModels = expectedItems.findAll { id -> !file("src/main/resources/assets/onlyfun/items/${id}.json").exists() } - def textureIds = ["normal_lootbox", "cape_lootbox", "spawn_egg_lootbox", "anomaly_lootbox"] + def textureIds = ["normal_lootbox", "cape_lootbox", "spawn_egg_lootbox", "anomaly_lootbox", "nourra_firework"] def invalidTextures = textureIds.findAll { id -> def texture = file("src/main/resources/assets/onlyfun/textures/item/${id}.png") if (!texture.isFile()) return true @@ -4138,6 +4138,36 @@ project(":onlyfun") { || !shopCsvText.contains("TRUE,ITEM,iliketomoveit:magic_cloud,1,3072,192,") || !javaText.contains("MAGIC_CLOUD, MAGIC_CARPET") || !file("src/main/resources/assets/onlyfun/models/block/lucky_block.json").text.contains("onlyfun:block/lucky_block") + || !javaText.contains("class NourraFireworkItem extends FireworkRocketItem") + || !javaText.contains("class SkinFaceFireworkExplosions") + || !javaText.contains("FACE = {") + || !javaText.contains("HOOD = {") + || !javaText.contains("HOOD_TOP = {") + || !javaText.contains("0x4A9DFC") + || !javaText.contains("TRANSPARENT") + || !javaText.contains("class SkinFaceFireworkClient") + || !javaText.contains("DustParticleOptions") + || !javaText.contains("PIXEL = 1.85D") + || !javaText.contains("LARGE_PIXEL = 2.7D") + || !javaText.contains("LARGE_SPARK_SIZE = 5.15F") + || !javaText.contains("SPARK_LIFETIME_TICKS = 38") + || !javaText.contains("new Fireworks(3, List.of())") + || !javaText.contains("LIFETIME_TRIM_TICKS = 5") + || !javaText.contains("onlyfun\$midFlightLifetime") + || !javaText.contains("onlyfun\$drawSkinFace") + || !javaText.contains("output.accept(OnlyFunRegistries.NOURRA_FIREWORK)") + || !file("src/main/resources/onlyfun.mixins.json").text.contains("FireworkRocketLifetimeMixin") + || !file("src/main/resources/onlyfun.client.mixins.json").text.contains("FireworkRocketEntityMixin") + || !file("src/main/resources/onlyfun.client.mixins.json").text.contains("ParticleLifetimeAccessor") + || !file("src/main/resources/fabric.mod.json").text.contains("onlyfun.mixins.json") + || !file("src/main/resources/fabric.mod.json").text.contains("onlyfun.client.mixins.json") + || !file("src/main/resources/data/onlyfun/recipe/nourra_firework.json").text.contains("onlyfun:nourra_firework") + || ["fr_fr", "en_us", "ru_ru"].any { locale -> + def translations = new JsonSlurper().parse(file( + "src/main/resources/assets/onlyfun/lang/${locale}.json")) + !translations.containsKey("item.onlyfun.nourra_firework") + || !translations.containsKey("tooltip.onlyfun.nourra_firework") + } || !missingModels.isEmpty() || !invalidTextureHashes.isEmpty() || !invalidTextures.isEmpty()) { diff --git a/onlyfun/art/skins/zayn97.png b/onlyfun/art/skins/zayn97.png new file mode 100644 index 0000000..c6c2c3b Binary files /dev/null and b/onlyfun/art/skins/zayn97.png differ diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/SkinFaceFireworkClient.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/SkinFaceFireworkClient.java new file mode 100644 index 0000000..de81930 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/SkinFaceFireworkClient.java @@ -0,0 +1,100 @@ +package fr.koka99cab.sanctuary26.onlyfun.client; + +import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions; +import fr.koka99cab.sanctuary26.onlyfun.mixin.client.ParticleLifetimeAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.Particle; +import net.minecraft.core.particles.ColorParticleOption; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.projectile.FireworkRocketEntity; +import net.minecraft.world.phys.Vec3; + +/** Paints the baked skin as a firework burst of coloured sparks, facing the player. */ +public final class SkinFaceFireworkClient { + public static final double PIXEL = 1.85D; + public static final double LARGE_PIXEL = 2.7D; + public static final float SPARK_SIZE = 3.6F; + public static final float LARGE_SPARK_SIZE = 5.15F; + public static final int SPARK_LIFETIME_TICKS = 38; + private static final int SUBPIXELS = 2; + + private SkinFaceFireworkClient() { + } + + public static void explode(FireworkRocketEntity rocket) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.particleEngine == null) return; + Vec3 origin = rocket.position(); + RandomSource random = rocket.level().getRandom(); + minecraft.level.playLocalSound(origin.x, origin.y, origin.z, + SoundEvents.FIREWORK_ROCKET_BLAST, SoundSource.AMBIENT, + 20.0F, 0.95F + random.nextFloat() * 0.1F, true); + minecraft.level.addParticle(ColorParticleOption.create(ParticleTypes.FLASH, 0xFFFFFFFF), + origin.x, origin.y, origin.z, 0.0D, 0.0D, 0.0D); + Vec3 look = facing(minecraft, origin); + Vec3 right = look.cross(new Vec3(0.0D, 1.0D, 0.0D)); + if (right.lengthSqr() < 1.0E-6D) right = new Vec3(1.0D, 0.0D, 0.0D); + right = right.normalize(); + Vec3 up = new Vec3(0.0D, 1.0D, 0.0D); + int[] pixels = SkinFaceFireworkExplosions.portrait(); + int width = SkinFaceFireworkExplosions.WIDTH; + int height = SkinFaceFireworkExplosions.portraitHeight(); + boolean large = largeBurst(rocket); + double pixel = burstPixel(large); + float sparkSize = burstSparkSize(large); + double midX = (width - 1) / 2.0D; + double midY = (height - 1) / 2.0D; + double step = 1.0D / SUBPIXELS; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + int color = visibleSpark(pixels[row * width + col]); + DustParticleOptions dust = new DustParticleOptions(color, sparkSize); + for (int subY = 0; subY < SUBPIXELS; subY++) { + for (int subX = 0; subX < SUBPIXELS; subX++) { + double px = (col - midX) + (subX + 0.5D) * step - 0.5D; + double py = (midY - row) - (subY + 0.5D) * step + 0.5D; + Vec3 offset = right.scale(px * pixel).add(up.scale(py * pixel)); + Vec3 pos = origin.add(offset); + Vec3 velocity = offset.scale(0.05D); + Particle spark = minecraft.particleEngine.createParticle( + dust, pos.x, pos.y, pos.z, velocity.x, velocity.y, velocity.z); + if (spark != null) { + ((ParticleLifetimeAccessor) spark).onlyfun$setLifetime(SPARK_LIFETIME_TICKS); + } + } + } + } + } + } + + public static boolean largeBurst(FireworkRocketEntity rocket) { + return (rocket.getUUID().getLeastSignificantBits() & 1L) != 0L; + } + + public static double burstPixel(boolean large) { + return large ? LARGE_PIXEL : PIXEL; + } + + public static float burstSparkSize(boolean large) { + return large ? LARGE_SPARK_SIZE : SPARK_SIZE; + } + + private static Vec3 facing(Minecraft minecraft, Vec3 origin) { + Vec3 camera = minecraft.gameRenderer.mainCamera().position(); + Vec3 horizontal = new Vec3(origin.x - camera.x, 0.0D, origin.z - camera.z); + if (horizontal.lengthSqr() < 1.0E-4D) return new Vec3(0.0D, 0.0D, 1.0D); + return horizontal.normalize(); + } + + private static int visibleSpark(int rgb) { + int red = rgb >> 16 & 0xFF; + int green = rgb >> 8 & 0xFF; + int blue = rgb & 0xFF; + if (red + green + blue < 24) return 0x2A2A2A; + return rgb; + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/NourraFireworkItem.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/NourraFireworkItem.java new file mode 100644 index 0000000..d07b657 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/NourraFireworkItem.java @@ -0,0 +1,23 @@ +package fr.koka99cab.sanctuary26.onlyfun.item; + +import java.util.function.Consumer; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.FireworkRocketItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipDisplay; + +/** Firework rocket that bursts into the baked Nourra skin face. */ +public final class NourraFireworkItem extends FireworkRocketItem { + public NourraFireworkItem(Properties properties) { + super(properties); + } + + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, + Consumer tooltip, TooltipFlag flag) { + tooltip.accept(Component.translatable("tooltip.onlyfun.nourra_firework").withStyle(ChatFormatting.AQUA)); + super.appendHoverText(stack, context, display, tooltip, flag); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkExplosions.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkExplosions.java new file mode 100644 index 0000000..d8d4a9c --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkExplosions.java @@ -0,0 +1,83 @@ +package fr.koka99cab.sanctuary26.onlyfun.item; + +import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries; +import java.util.List; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.Fireworks; + +/** + * Baked Nourra head (NameMC 6166a439e9354904): inner face, hood overlay, hood top. + * The portrait is drawn client-side with firework sparks. + */ +public final class SkinFaceFireworkExplosions { + public static final int WIDTH = 8; + public static final int HEIGHT = 8; + public static final int TRANSPARENT = -1; + /** Inner head front, top-left origin, row-major RGB. */ + public static final int[] FACE = { + 0x000000, 0x020202, 0x0A0A0A, 0x111111, 0x111111, 0x0A0A0A, 0x020202, 0x000000, + 0x020202, 0x0A0A0A, 0x111111, 0x181818, 0x181818, 0x111111, 0x0A0A0A, 0x020202, + 0x0A0A0A, 0x111111, 0x181818, 0x212121, 0x212121, 0x181818, 0x111111, 0x0A0A0A, + 0x111111, 0x181818, 0x212121, 0x2B2B2B, 0x2B2B2B, 0x212121, 0x181818, 0x111111, + 0x111111, 0x000000, 0x000000, 0xFFF0E1, 0xFFF0E1, 0x000000, 0x000000, 0x111111, + 0x0A0A0A, 0xFFFFFF, 0x4A9DFC, 0xFDE8D7, 0xFFF0E1, 0x4A9DFC, 0xFFFFFF, 0x0A0A0A, + 0x020202, 0xFFFFFF, 0x4A9DFC, 0xFDE8D7, 0xFDE8D7, 0x4A9DFC, 0xFFFFFF, 0x020202, + 0x000000, 0xF0C6A5, 0xF7D5B9, 0xFCE1CA, 0xFCE1CA, 0xF7D5B9, 0xF0C6A5, 0x000000 + }; + /** Hat/hood overlay on the front; {@link #TRANSPARENT} keeps the inner face visible. */ + public static final int[] HOOD = { + 0x000000, 0x0D0D0D, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0x0D0D0D, 0x000000, + 0x0D0D0D, 0x707070, 0xD8D8D8, 0xE5E5E5, 0xE5E5E5, 0xD8D8D8, 0x707070, 0x0D0D0D, + 0xCBCBCB, 0xD8D8D8, 0xFFFFFF, 0x212121, 0x212121, 0x181818, 0xD8D8D8, 0xCBCBCB, + 0xD8D8D8, 0x181818, 0x212121, 0x2B2B2B, 0x2B2B2B, 0x212121, 0x181818, 0xD8D8D8, + 0x111111, 0x181818, 0x212121, 0x2B2B2B, TRANSPARENT, TRANSPARENT, TRANSPARENT, 0x111111, + 0x0A0A0A, 0x111111, 0x181818, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, + 0x020202, 0x0A0A0A, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, + TRANSPARENT, 0x020202, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT + }; + /** Front edge of the hood top (skin overlay top rows closest to the face). */ + public static final int[] HOOD_TOP = { + 0xBEBEBE, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0xBEBEBE, + 0xB8B8B8, 0xB8B8B8, 0xCBCBCB, 0xD8D8D8, 0xD8D8D8, 0xCBCBCB, 0xB8B8B8, 0xB8B8B8 + }; + + private SkinFaceFireworkExplosions() { + } + + /** Vanilla flight 3 minus half a gunpowder step, so the burst sits between 2 and 3. */ + public static final int LIFETIME_TRIM_TICKS = 5; + + public static Fireworks defaultFireworks() { + return new Fireworks(3, List.of()); + } + + public static int midFlightLifetime(int vanillaLifetime) { + return Math.max(1, vanillaLifetime - LIFETIME_TRIM_TICKS); + } + + public static boolean isSkinFaceRocket(ItemStack stack) { + return !stack.isEmpty() && stack.is(OnlyFunRegistries.NOURRA_FIREWORK); + } + + public static int opaqueHoodPixels() { + int count = 0; + for (int color : HOOD) { + if (color != TRANSPARENT) count++; + } + return count; + } + + /** Hood top (2 rows) then the 8×8 face with the hood overlay composited. */ + public static int[] portrait() { + int[] pixels = new int[WIDTH * (HEIGHT + 2)]; + System.arraycopy(HOOD_TOP, 0, pixels, 0, HOOD_TOP.length); + for (int index = 0; index < FACE.length; index++) { + pixels[HOOD_TOP.length + index] = HOOD[index] == TRANSPARENT ? FACE[index] : HOOD[index]; + } + return pixels; + } + + public static int portraitHeight() { + return HEIGHT + HOOD_TOP.length / WIDTH; + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/FireworkRocketLifetimeMixin.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/FireworkRocketLifetimeMixin.java new file mode 100644 index 0000000..df5a10a --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/FireworkRocketLifetimeMixin.java @@ -0,0 +1,25 @@ +package fr.koka99cab.sanctuary26.onlyfun.mixin; + +import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions; +import net.minecraft.world.entity.projectile.FireworkRocketEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FireworkRocketEntity.class) +public abstract class FireworkRocketLifetimeMixin { + @Shadow + private int life; + @Shadow + private int lifetime; + + @Inject(method = "tick", at = @At("HEAD")) + private void onlyfun$midFlightLifetime(CallbackInfo callback) { + if (this.life != 0) return; + FireworkRocketEntity rocket = (FireworkRocketEntity) (Object) this; + if (!SkinFaceFireworkExplosions.isSkinFaceRocket(rocket.getItem())) return; + this.lifetime = SkinFaceFireworkExplosions.midFlightLifetime(this.lifetime); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/FireworkRocketEntityMixin.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/FireworkRocketEntityMixin.java new file mode 100644 index 0000000..a3f841b --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/FireworkRocketEntityMixin.java @@ -0,0 +1,22 @@ +package fr.koka99cab.sanctuary26.onlyfun.mixin.client; + +import fr.koka99cab.sanctuary26.onlyfun.client.SkinFaceFireworkClient; +import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions; +import net.minecraft.world.entity.projectile.FireworkRocketEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FireworkRocketEntity.class) +public abstract class FireworkRocketEntityMixin { + @Inject(method = "handleEntityEvent", at = @At("HEAD"), cancellable = true) + private void onlyfun$drawSkinFace(byte id, CallbackInfo callback) { + if (id != 17) return; + FireworkRocketEntity rocket = (FireworkRocketEntity) (Object) this; + if (!rocket.level().isClientSide()) return; + if (!SkinFaceFireworkExplosions.isSkinFaceRocket(rocket.getItem())) return; + SkinFaceFireworkClient.explode(rocket); + callback.cancel(); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/ParticleLifetimeAccessor.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/ParticleLifetimeAccessor.java new file mode 100644 index 0000000..72608b8 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/mixin/client/ParticleLifetimeAccessor.java @@ -0,0 +1,11 @@ +package fr.koka99cab.sanctuary26.onlyfun.mixin.client; + +import net.minecraft.client.particle.Particle; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Particle.class) +public interface ParticleLifetimeAccessor { + @Accessor("lifetime") + void onlyfun$setLifetime(int lifetime); +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunCreativeTab.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunCreativeTab.java index 3efe9c6..7116203 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunCreativeTab.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunCreativeTab.java @@ -28,6 +28,7 @@ public final class OnlyFunCreativeTab { output.accept(OnlyFunRegistries.CAPE_LOOTBOX); output.accept(OnlyFunRegistries.SPAWN_EGG_LOOTBOX); output.accept(OnlyFunRegistries.ANOMALY_LOOTBOX); + output.accept(OnlyFunRegistries.NOURRA_FIREWORK); output.accept(AbjectionRegistries.SHIT_BALL); output.accept(AbjectionRegistries.SHIT_CUBE_ITEM); }) diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunRegistries.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunRegistries.java index fbadc27..289f2b1 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunRegistries.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/registry/OnlyFunRegistries.java @@ -3,6 +3,8 @@ package fr.koka99cab.sanctuary26.onlyfun.registry; import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod; import fr.koka99cab.sanctuary26.onlyfun.block.LuckyBlockBlock; import fr.koka99cab.sanctuary26.onlyfun.item.LootboxItem; +import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkExplosions; +import fr.koka99cab.sanctuary26.onlyfun.item.NourraFireworkItem; import fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunGenerator; import fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunSource; import java.util.function.Function; @@ -32,6 +34,9 @@ public final class OnlyFunRegistries { public static final Item CAPE_LOOTBOX = registerLootbox("cape_lootbox", OnlyFunSource.CAPE_LOOTBOX); public static final Item SPAWN_EGG_LOOTBOX = registerLootbox("spawn_egg_lootbox", OnlyFunSource.SPAWN_EGG_LOOTBOX); public static final Item ANOMALY_LOOTBOX = registerLootbox("anomaly_lootbox", OnlyFunSource.ANOMALY_LOOTBOX); + public static final Item NOURRA_FIREWORK = registerItem("nourra_firework", + properties -> new NourraFireworkItem(properties.stacksTo(64) + .component(DataComponents.FIREWORKS, SkinFaceFireworkExplosions.defaultFireworks()))); private static boolean initialized; private OnlyFunRegistries() { @@ -61,6 +66,11 @@ public final class OnlyFunRegistries { OnlyFunMod.LOGGER.debug("[{}] Item and block registries initialized.", OnlyFunMod.DISPLAY_NAME); } + private static Item registerItem(String path, Function factory) { + ResourceKey key = ResourceKey.create(Registries.ITEM, OnlyFunMod.id(path)); + return Registry.register(BuiltInRegistries.ITEM, key, factory.apply(new Item.Properties().setId(key))); + } + private static Item registerLootbox(String path, OnlyFunSource source) { ResourceKey key = ResourceKey.create(Registries.ITEM, OnlyFunMod.id(path)); return Registry.register(BuiltInRegistries.ITEM, key, diff --git a/onlyfun/src/main/resources/assets/onlyfun/items/nourra_firework.json b/onlyfun/src/main/resources/assets/onlyfun/items/nourra_firework.json new file mode 100644 index 0000000..38c8acf --- /dev/null +++ b/onlyfun/src/main/resources/assets/onlyfun/items/nourra_firework.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "onlyfun:item/nourra_firework" + } +} diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json b/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json index c507c3c..498b7d2 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json @@ -6,6 +6,8 @@ "item.onlyfun.cape_lootbox": "Cape Lootbox", "item.onlyfun.spawn_egg_lootbox": "Spawn Egg Lootbox", "item.onlyfun.anomaly_lootbox": "Anomaly Lootbox", + "item.onlyfun.nourra_firework": "Nourra Firework", + "tooltip.onlyfun.nourra_firework": "The burst paints the skin face, at normal or larger size.", "item.onlyfun.quality_gear": "Quality Gear", "item.onlyfun.impossible_tool": "Impossible Tool", "item.onlyfun.impossible_book": "Impossible Book", diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json b/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json index e06e14b..98f2a21 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json @@ -6,6 +6,8 @@ "item.onlyfun.cape_lootbox": "Lootbox de capes", "item.onlyfun.spawn_egg_lootbox": "Lootbox d'œufs", "item.onlyfun.anomaly_lootbox": "Lootbox anomalie", + "item.onlyfun.nourra_firework": "Feu d'artifice Nourra", + "tooltip.onlyfun.nourra_firework": "L'explosion dessine le visage, en taille normale ou plus grande.", "item.onlyfun.quality_gear": "Équipement de qualité", "item.onlyfun.impossible_tool": "Outil impossible", "item.onlyfun.impossible_book": "Livre impossible", diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json b/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json index 812f033..6eb8818 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json @@ -6,6 +6,8 @@ "item.onlyfun.cape_lootbox": "Лутбокс плащей", "item.onlyfun.spawn_egg_lootbox": "Лутбокс яиц призыва", "item.onlyfun.anomaly_lootbox": "Лутбокс-аномалия", + "item.onlyfun.nourra_firework": "Фейерверк Нурры", + "tooltip.onlyfun.nourra_firework": "Взрыв рисует лицо скина обычного или увеличенного размера.", "item.onlyfun.quality_gear": "Качественное снаряжение", "item.onlyfun.impossible_tool": "Невозможный инструмент", "item.onlyfun.impossible_book": "Невозможная книга", diff --git a/onlyfun/src/main/resources/assets/onlyfun/models/item/nourra_firework.json b/onlyfun/src/main/resources/assets/onlyfun/models/item/nourra_firework.json new file mode 100644 index 0000000..9d6cdfc --- /dev/null +++ b/onlyfun/src/main/resources/assets/onlyfun/models/item/nourra_firework.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "onlyfun:item/nourra_firework" + } +} diff --git a/onlyfun/src/main/resources/assets/onlyfun/textures/item/nourra_firework.png b/onlyfun/src/main/resources/assets/onlyfun/textures/item/nourra_firework.png new file mode 100644 index 0000000..3ca0450 Binary files /dev/null and b/onlyfun/src/main/resources/assets/onlyfun/textures/item/nourra_firework.png differ diff --git a/onlyfun/src/main/resources/data/onlyfun/recipe/nourra_firework.json b/onlyfun/src/main/resources/data/onlyfun/recipe/nourra_firework.json new file mode 100644 index 0000000..879b06a --- /dev/null +++ b/onlyfun/src/main/resources/data/onlyfun/recipe/nourra_firework.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "minecraft:firework_rocket", + "minecraft:light_blue_dye", + "minecraft:paper" + ], + "result": { + "count": 1, + "id": "onlyfun:nourra_firework" + } +} diff --git a/onlyfun/src/main/resources/fabric.mod.json b/onlyfun/src/main/resources/fabric.mod.json index 49f24b8..79ba9a8 100644 --- a/onlyfun/src/main/resources/fabric.mod.json +++ b/onlyfun/src/main/resources/fabric.mod.json @@ -21,6 +21,13 @@ "fr.koka99cab.sanctuary26.onlyfun.client.OnlyFunClient" ] }, + "mixins": [ + "onlyfun.mixins.json", + { + "config": "onlyfun.client.mixins.json", + "environment": "client" + } + ], "depends": { "fabricloader": ">=0.19.3", "minecraft": "~26.2", diff --git a/onlyfun/src/main/resources/onlyfun.client.mixins.json b/onlyfun/src/main/resources/onlyfun.client.mixins.json new file mode 100644 index 0000000..b5913e1 --- /dev/null +++ b/onlyfun/src/main/resources/onlyfun.client.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "fr.koka99cab.sanctuary26.onlyfun.mixin.client", + "compatibilityLevel": "JAVA_25", + "client": [ + "FireworkRocketEntityMixin", + "ParticleLifetimeAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/onlyfun/src/main/resources/onlyfun.mixins.json b/onlyfun/src/main/resources/onlyfun.mixins.json new file mode 100644 index 0000000..a68228a --- /dev/null +++ b/onlyfun/src/main/resources/onlyfun.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "fr.koka99cab.sanctuary26.onlyfun.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": [ + "FireworkRocketLifetimeMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkSmoke.java b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkSmoke.java new file mode 100644 index 0000000..f6f0f4f --- /dev/null +++ b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/item/SkinFaceFireworkSmoke.java @@ -0,0 +1,66 @@ +package fr.koka99cab.sanctuary26.onlyfun.item; + +import fr.koka99cab.sanctuary26.onlyfun.client.SkinFaceFireworkClient; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.item.component.Fireworks; + +/** Executable check for the baked Nourra face and hood firework contract. */ +public final class SkinFaceFireworkSmoke { + private SkinFaceFireworkSmoke() { + } + + public static void main(String[] args) { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + verify(); + } + + public static void verify() { + require(SkinFaceFireworkExplosions.WIDTH == 8 && SkinFaceFireworkExplosions.HEIGHT == 8, + "Face grid is no longer 8×8"); + require(SkinFaceFireworkExplosions.FACE.length == 64, "Face palette length drifted"); + require(SkinFaceFireworkExplosions.HOOD.length == 64, "Hood overlay length drifted"); + require(SkinFaceFireworkExplosions.HOOD_TOP.length == 16, "Hood top strip length drifted"); + require(SkinFaceFireworkExplosions.FACE[5 * 8 + 2] == 0x4A9DFC + && SkinFaceFireworkExplosions.FACE[5 * 8 + 5] == 0x4A9DFC, + "Blue eyes are missing from the inner face"); + require(SkinFaceFireworkExplosions.FACE[4 * 8 + 3] == 0xFFF0E1, + "Skin tone is missing from the baked face"); + require(SkinFaceFireworkExplosions.HOOD[2] == 0xCBCBCB, + "Hood overlay is missing from the top of the face"); + require(SkinFaceFireworkExplosions.HOOD[5 * 8 + 2] == 0x181818, + "Hood no longer covers the left eye"); + require(SkinFaceFireworkExplosions.HOOD[5 * 8 + 5] == SkinFaceFireworkExplosions.TRANSPARENT, + "Hood opening no longer leaves the right eye visible"); + require(SkinFaceFireworkExplosions.opaqueHoodPixels() == 43, + "Opaque hood pixel count drifted"); + Fireworks fireworks = SkinFaceFireworkExplosions.defaultFireworks(); + require(fireworks.flightDuration() == 3 && fireworks.explosions().isEmpty(), + "Default firework burst contract changed"); + require(SkinFaceFireworkExplosions.LIFETIME_TRIM_TICKS == 5 + && SkinFaceFireworkExplosions.midFlightLifetime(45) == 40, + "Mid-flight lifetime trim drifted"); + int[] portrait = SkinFaceFireworkExplosions.portrait(); + require(portrait.length == 80 && SkinFaceFireworkExplosions.portraitHeight() == 10, + "Portrait grid drifted"); + require(portrait[16 + 5 * 8 + 5] == 0x4A9DFC, + "Right eye is missing from the composited portrait"); + require(portrait[2] == 0xD8D8D8, + "Hood top is missing from the portrait"); + require(SkinFaceFireworkClient.PIXEL == 1.85D, "Portrait scale drifted"); + require(SkinFaceFireworkClient.LARGE_PIXEL == 2.7D, "Large portrait scale drifted"); + require(SkinFaceFireworkClient.burstPixel(false) == SkinFaceFireworkClient.PIXEL + && SkinFaceFireworkClient.burstSparkSize(false) == SkinFaceFireworkClient.SPARK_SIZE, + "Normal burst scale drifted"); + require(SkinFaceFireworkClient.burstPixel(true) == SkinFaceFireworkClient.LARGE_PIXEL + && SkinFaceFireworkClient.burstSparkSize(true) == SkinFaceFireworkClient.LARGE_SPARK_SIZE, + "Large burst scale drifted"); + require(SkinFaceFireworkClient.SPARK_LIFETIME_TICKS == 38, "Spark lifetime drifted"); + System.out.println("Only Fun Nourra firework: 8×8 face with hood overlay ready."); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new IllegalStateException(message); + } +} diff --git a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/loot/OnlyFunGeneratorSmoke.java b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/loot/OnlyFunGeneratorSmoke.java index 0cda8f4..c64c7d5 100644 --- a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/loot/OnlyFunGeneratorSmoke.java +++ b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/loot/OnlyFunGeneratorSmoke.java @@ -1,5 +1,6 @@ package fr.koka99cab.sanctuary26.onlyfun.loot; +import fr.koka99cab.sanctuary26.onlyfun.item.SkinFaceFireworkSmoke; import fr.koka99cab.sanctuary26.onlyfun.shop.OnlyFunShopCatalogSmoke; import fr.koka99cab.sanctuary26.onlyfun.world.CobbleDungeonPlacementSmoke; import java.util.EnumMap; @@ -18,6 +19,7 @@ public final class OnlyFunGeneratorSmoke { Bootstrap.bootStrap(); OnlyFunShopCatalogSmoke.verify(); CobbleDungeonPlacementSmoke.verify(); + SkinFaceFireworkSmoke.verify(); OnlyFunGenerator.initialize(); require(OnlyFunGenerator.ANOMALY_DENOMINATOR == 256, "Anomaly odds changed from 1/256"); require(OnlyFunGenerator.PITY_START == 24, "Pity threshold changed");