Compare commits

...
Author SHA1 Message Date
Plastered_Crab 621178e892 Initial changes for testing Photonics compat on moves 2026-06-22 10:47:58 -07:00
Plastered_Crab 024d9ededf minor refector and edits 2026-06-20 09:28:40 -07:00
Plastered_Crab 0a7deedeea Tweak particle lighting values 2026-06-19 08:03:02 -07:00
Plastered_Crab 9d86fd4530 Push slightly better golurk animation lighting 2026-06-19 07:40:41 -07:00
plastered_crab 66b9a2a0b8 fix json reference 2026-06-18 21:45:01 -07:00
plastered_crab 8e1b420b3a Tweak per animation and per particle dynamic lighting to be more subtle 2026-06-18 20:56:54 -07:00
plastered_crab 3f969b7003 Tweak per animation and per particle dynamic lighting to be more subtle 2026-06-18 20:36:59 -07:00
plastered_crab 0009c83331 Tweak per animation and per particle dynamic lighting to be more subtle 2026-06-18 20:32:55 -07:00
plastered_crab f4c80cf77d Move and rename particle lighting json 2026-06-18 18:48:56 -07:00
Plastered_Crab 562ed04c68 Fix how animation lighting is read 2026-06-18 18:42:04 -07:00
Plastered_Crab 2175dd5b92 Add animation based LambDynamicLight support with some test values 2026-06-18 14:49:01 -07:00
Plastered_Crab b8ec624ba1 Manual tweaking of particle lighting list 2026-06-17 22:41:50 -07:00
Plastered_Crab 11d6fd0637 Initial comprehensive list of particles that might emit light (wip) 2026-06-17 21:46:03 -07:00
Plastered_Crab 68ece4cd96 Adjust how light levels are assigned to particles and give some example values 2026-06-17 16:11:25 -07:00
Plastered_Crab 0eebba6604 LambDynamicLight compat for moves testing 2026-06-17 11:48:32 -07:00
plastered_crab d0484c31ff Gate particle Iris bypass behind config 2026-06-17 06:30:23 -07:00
plastered_crab 658564f8c1 Get our particles to bypass Iris 2026-06-16 22:23:23 -07:00
Plastered_Crab c95a44039e Initial testing for our shaders to bypass Iris 2026-06-15 22:01:05 -07:00
Plastered_Crab b0f0c20404 Initial testing for Shader support for our particles 2026-06-15 20:49:08 -07:00
29 changed files with 2017 additions and 21 deletions
+9
View File
@@ -51,6 +51,15 @@ dependencies {
requireCapability("dev.lambdaurora.lambdynamiclights:api-mojmap")
}
}
// PhotonRT (our local fork of the Photonic Iris raytracing mod) exposes a dynamic-light injection API
// we call from the photonic compat. Consume its published Mojang-mapped API variant from mavenLocal —
// it's engine-free (no Iris/Sodium needed on this classpath). Plain compileOnly (NOT modCompileOnly):
// the variant is already Mojang-mapped, so Loom would wrongly re-remap a modCompileOnly. The running
// mod is the loose remapped jar in runClient/mods. Refresh via PhotonRT's `gradlew publishToMavenLocal`.
compileOnly("dev.plasteredcrab:photonrt:0.1.0+1.21.1") {
capabilities { requireCapability("dev.plasteredcrab.photonrt:api-mojmap") }
isTransitive = false
}
// Flywheel has no common dep so just pick one and don't use any platform specific code in common
// modCompileOnly(libs.flywheelFabric)
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.particle;
import com.cobblemon.mod.common.client.render.SnowstormParticle;
import org.joml.Vector3f;
/**
* Adapts a single glowing {@link SnowstormParticle} to the {@link PhotonicDynamicLight} contract so the
* Photonic raytracing mod can inject it as a moving light. Position and luminance track the live
* particle; the color is the particle's own tint, captured at spawn.
*
* <p>Color note: most Cobblemon move particles use white sprite textures tinted at runtime (e.g. the
* ember effect tints a white "cloudyfire" sprite orange), so the particle's resolved tint is the true
* emitted color — sampling the sprite would just return white. We therefore use
* {@link SnowstormParticle#currentLightColor()}.</p>
*
* <p>Written in Java (like the LambDynamicLights adapter) so {@code particle.getX()} resolves to the
* particle's accessor rather than the protected {@code Particle.x} field that Kotlin's property
* synthesis would otherwise bind to. Only instantiated when Photonic is present (see
* {@link PhotonicDynamicLights#getEnabled()}), so it never couples Cobblemon to the mod.</p>
*/
public final class SnowstormPhotonicLight implements PhotonicDynamicLight {
private static final int MAX_LUMINANCE = 15;
private final SnowstormParticle particle;
// Reused holder for the live tint. NOT captured at construction: the particle's tint isn't
// resolvable until it has aged a tick (a gradient at age 0 has no node), so reading it in the
// constructor would throw inside the particle engine. Refreshed lazily in getRed().
private final Vector3f color = new Vector3f(1.0f, 1.0f, 1.0f);
public SnowstormPhotonicLight(SnowstormParticle particle) {
this.particle = particle;
}
@Override
public double getX() {
return particle.getX();
}
@Override
public double getY() {
return particle.getY();
}
@Override
public double getZ() {
return particle.getZ();
}
// The consumer (PhotonicLightRegistryMixin) reads red, then green, then blue in that order, so we
// resolve the live tint once on getRed() and let green/blue read the cached value — one gradient
// evaluation per light per frame instead of three.
@Override
public float getRed() {
color.set(particle.currentLightColor());
return color.x;
}
@Override
public float getGreen() {
return color.y;
}
@Override
public float getBlue() {
return color.z;
}
@Override
public int getLuminance() {
return Math.min(MAX_LUMINANCE, particle.currentLightLevel());
}
@Override
public boolean isAlive() {
return particle.isAlive();
}
}
@@ -9,8 +9,10 @@
package com.cobblemon.mod.common.compat.lambdynamiclights;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.client.particle.MoveDynamicLights;
import dev.lambdaurora.lambdynlights.api.DynamicLightsContext;
import dev.lambdaurora.lambdynlights.api.DynamicLightsInitializer;
import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehaviorManager;
import dev.lambdaurora.lambdynlights.api.entity.luminance.EntityLuminance;
import dev.lambdaurora.lambdynlights.api.item.ItemLightSourceManager;
import net.minecraft.resources.ResourceLocation;
@@ -33,6 +35,12 @@ public class LambDynamicLightsInitializer implements DynamicLightsInitializer {
context.entityLightSourceManager().onRegisterEvent().register(ctx -> {
ctx.register(EntityType.PLAYER, PlayerLuminance.INSTANCE);
});
// Attach a monochrome dynamic light to each glowing particle. The light follows its particle and
// reports isRemoved() when the particle dies, so the manager cleans it up. The cap on how many
// particles per emitter may be lit at once is enforced at particle spawn, not here.
final DynamicLightBehaviorManager behaviorManager = context.dynamicLightBehaviorManager();
MoveDynamicLights.setSink(particle -> behaviorManager.add(new SnowstormParticleLight(particle)));
}
@SuppressWarnings({"removal", "UnstableApiUsage"})
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.compat.lambdynamiclights;
import com.cobblemon.mod.common.client.render.PokemonAnimationLight;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import dev.lambdaurora.lambdynlights.api.entity.luminance.EntityLuminance;
import dev.lambdaurora.lambdynlights.api.item.ItemLightSourceManager;
@@ -36,8 +37,9 @@ public final class PokemonLuminance extends CustomLuminance implements EntityLum
ItemStack item = pokemon.getShownItem();
int itemLightLevel = item.isEmpty() ? 0 : itemLightSourceManager.getLuminance(item, underwater);
int formLightLevel = extractFormLightLevel(pokemon.getForm(), underwater).orElse(0);
int animLightLevel = PokemonAnimationLight.luminanceFor(pokemon);
return Math.max(itemLightLevel, formLightLevel);
return Math.max(Math.max(itemLightLevel, formLightLevel), animLightLevel);
} else {
return 0;
}
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.compat.lambdynamiclights;
import com.cobblemon.mod.common.client.render.SnowstormParticle;
import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehavior;
import net.minecraft.core.BlockPos;
/**
* A real, monochrome dynamic light attached to a single Cobblemon particle, so each glowing particle
* lights the world around it like its own light source. The level is whatever the particle currently
* emits ({@link SnowstormParticle#currentLightLevel()}); a particle emitting 0 contributes nothing.
*
* <p>How many of these exist per emitter is capped (by the maxLitParticlesPerEmitter config, enforced
* at particle spawn, not here). This follows the particle as it moves and removes itself when it dies.</p>
*
* <p>Only instantiated by {@link LambDynamicLightsInitializer} (the LambDynamicLights entrypoint), so
* it is never loaded unless LambDynamicLights is installed.</p>
*/
public final class SnowstormParticleLight implements DynamicLightBehavior {
private static final int MAX_LUMINANCE = 15;
private final SnowstormParticle particle;
private int prevX = Integer.MIN_VALUE;
private int prevY = Integer.MIN_VALUE;
private int prevZ = Integer.MIN_VALUE;
private int prevLuminance = -1;
/** Level cached each tick in hasChanged() so the per block lightAtPos call stays cheap and stable. */
private double cachedLuminance;
public SnowstormParticleLight(SnowstormParticle particle) {
this.particle = particle;
this.cachedLuminance = Math.min(MAX_LUMINANCE, particle.currentLightLevel());
}
@Override
public double lightAtPos(BlockPos pos, double falloffRatio) {
double lum = cachedLuminance;
if (lum <= 0.0) return 0.0;
double dx = pos.getX() + 0.5 - particle.getX();
double dy = pos.getY() + 0.5 - particle.getY();
double dz = pos.getZ() + 0.5 - particle.getZ();
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Lose exactly 1 level per block so an authored level of N lights about N blocks, like vanilla
// block light. We intentionally ignore the falloffRatio LambDynamicLights passes (15 / MAX_RADIUS,
// about 1.94): applying it would compress the falloff into the mod's 7.75 block cap and make every
// light reach only about half its level in blocks, looking dimmer than the number implies. The mod
// still limits reach to MAX_RADIUS (about 7.75 blocks), so levels above 8 are clipped there.
return Math.max(0.0, lum - dist);
}
@Override
public DynamicLightBehavior.BoundingBox getBoundingBox() {
// Report only the emitting volume (the particle's own block); the API expands it by falloff.
int x = (int) Math.floor(particle.getX());
int y = (int) Math.floor(particle.getY());
int z = (int) Math.floor(particle.getZ());
return new DynamicLightBehavior.BoundingBox(x, y, z, x + 1, y + 1, z + 1);
}
@Override
public boolean hasChanged() {
int x = (int) Math.floor(particle.getX());
int y = (int) Math.floor(particle.getY());
int z = (int) Math.floor(particle.getZ());
int lum = Math.min(MAX_LUMINANCE, particle.currentLightLevel());
boolean changed = x != prevX || y != prevY || z != prevZ || lum != prevLuminance;
if (changed) {
prevX = x;
prevY = y;
prevZ = z;
prevLuminance = lum;
cachedLuminance = lum;
}
return changed;
}
@Override
public boolean isRemoved() {
return !particle.isAlive();
}
}
@@ -0,0 +1,146 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.compat.photonic;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.client.particle.PhotonicDynamicLight;
import com.cobblemon.mod.common.client.particle.PhotonicDynamicLights;
import dev.plasteredcrab.photonrt.api.LightEmitter;
import dev.plasteredcrab.photonrt.api.PhotonRT;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
/**
* Integrates Cobblemon with the PhotonRT raytracing mod (our fork of Photonic; mod id {@code photonrt}).
* Two things:
* <ul>
* <li><b>Block lights</b> — registers every Cobblemon light-emitting block as a colored, path-traced
* light via {@link PhotonRT#registerBlockLight}.</li>
* <li><b>Moving move-particle lights</b> — registers a single emitter hook that, each gather, pushes a
* traced point light for every glowing snowstorm particle currently active (see
* {@link PhotonicDynamicLights}). PhotonRT handles the per-frame light-grid rebuild so the lights
* follow the particles — the thing stock Photonic could not do.</li>
* </ul>
*
* <p>This class references PhotonRT API types directly (Cobblemon compiles against PhotonRT's jar as a
* {@code compileOnly} dependency, like it does for the LambDynamicLights API). It must therefore only be
* loaded when {@code photonrt} is actually present — the caller
* ({@link com.cobblemon.mod.fabric.client.CobblemonFabricClient}) gates the call on
* {@code isModLoaded("photonrt")} so this class (and its PhotonRT references) never load otherwise.</p>
*/
public final class PhotonRTIntegration {
private PhotonRTIntegration() {}
/** Photonic's default block-light attenuation. */
private static final float ATTEN_CONSTANT = 0.9f;
private static final float ATTEN_QUADRATIC = 0.9f;
private static boolean hookRegistered = false;
/** One Cobblemon block id -> the light color it should cast under raytracing (0..1 linear RGB). */
private record Entry(String id, float r, float g, float b) {}
private static final Entry[] ENTRIES = {
// Evolution stone blocks / ores — fire = orange
new Entry("fire_stone_block", 1.00f, 0.42f, 0.12f),
new Entry("fire_stone_ore", 1.00f, 0.42f, 0.12f),
new Entry("deepslate_fire_stone_ore", 1.00f, 0.42f, 0.12f),
new Entry("nether_fire_stone_ore", 1.00f, 0.36f, 0.10f),
// thunder = electric yellow
new Entry("thunder_stone_block", 1.00f, 0.88f, 0.25f),
new Entry("thunder_stone_ore", 1.00f, 0.88f, 0.25f),
new Entry("deepslate_thunder_stone_ore", 1.00f, 0.88f, 0.25f),
// sun = warm gold
new Entry("sun_stone_block", 1.00f, 0.78f, 0.30f),
new Entry("sun_stone_ore", 1.00f, 0.78f, 0.30f),
new Entry("deepslate_sun_stone_ore", 1.00f, 0.78f, 0.30f),
new Entry("terracotta_sun_stone_ore", 1.00f, 0.74f, 0.34f),
// shiny = bright near-white
new Entry("shiny_stone_block", 1.00f, 1.00f, 0.96f),
new Entry("shiny_stone_ore", 1.00f, 1.00f, 0.96f),
new Entry("deepslate_shiny_stone_ore", 1.00f, 1.00f, 0.96f),
// dawn = soft magenta/pink
new Entry("dawn_stone_block", 0.92f, 0.55f, 0.88f),
new Entry("dawn_stone_ore", 0.92f, 0.55f, 0.88f),
new Entry("deepslate_dawn_stone_ore", 0.92f, 0.55f, 0.88f),
// Machines / utility
new Entry("healing_machine", 1.00f, 0.32f, 0.48f), // pink-red
new Entry("pc", 0.30f, 0.80f, 1.00f), // cyan screen
new Entry("monitor", 0.35f, 0.72f, 1.00f),
new Entry("damaged_monitor", 0.45f, 0.85f, 0.60f), // sickly green glitch
new Entry("pasture", 1.00f, 0.90f, 0.70f), // warm
new Entry("lectern", 1.00f, 0.90f, 0.60f),
// Cooking / misc
new Entry("campfire", 1.00f, 0.50f, 0.16f),
new Entry("soul_campfire", 0.30f, 0.86f, 0.92f), // soul cyan
new Entry("poke_cake", 1.00f, 0.80f, 0.40f),
new Entry("poke_snack", 1.00f, 0.80f, 0.40f),
};
/**
* Registers Cobblemon's block lights and the move-particle emitter hook with PhotonRT. Call only when
* {@code photonrt} is loaded (the caller checks). Safe to call more than once: block lights are
* replaced rather than duplicated, and the emitter hook is registered at most once.
*/
public static void register() {
try {
int added = 0;
for (Entry entry : ENTRIES) {
final ResourceLocation id = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, entry.id());
final Block block = BuiltInRegistries.BLOCK.get(id);
if (block == null || block == Blocks.AIR) {
continue;
}
PhotonRT.registerBlockLight(block, entry.r(), entry.g(), entry.b(), ATTEN_CONSTANT, ATTEN_QUADRATIC, true);
added++;
}
if (!hookRegistered) {
PhotonRT.addEmitterHook(PhotonRTIntegration::emitDynamicLights);
hookRegistered = true;
}
// Let glowing move particles start registering themselves (consumed by the hook above).
PhotonicDynamicLights.setEnabled(true);
Cobblemon.LOGGER.info("PhotonRT raytracing compatibility enabled: {} Cobblemon light blocks + moving move-particle lights", added);
} catch (Throwable t) {
Cobblemon.LOGGER.warn("PhotonRT is present but its API could not be reached (version mismatch?); skipping Cobblemon raytracing integration.", t);
}
}
/**
* Emitter hook: push one traced point light per active glowing particle. Runs on the client thread
* during PhotonRT's per-gather pass; {@link PhotonicDynamicLights#activeSnapshot()} is thread-safe and
* prunes dead particles. Color is the particle's live tint (see {@code SnowstormPhotonicLight}).
*/
private static long lastDiagLogMs = 0L;
private static void emitDynamicLights(LightEmitter emitter) {
final java.util.List<PhotonicDynamicLight> active = PhotonicDynamicLights.activeSnapshot();
int pushed = 0;
for (PhotonicDynamicLight light : active) {
if (light.getLuminance() <= 0) {
continue;
}
emitter.light(light.getX(), light.getY(), light.getZ(), light.getRed(), light.getGreen(), light.getBlue());
pushed++;
}
// Diagnostic (throttled): proves the hook is firing and how many particle lights we feed PhotonRT.
// If this never logs, PhotonRT isn't calling the hook (raytracer inactive); if active>0 but you see
// no light in-game, the gap is on PhotonRT's render side.
final long now = System.currentTimeMillis();
if (now - lastDiagLogMs > 2000L) {
lastDiagLogMs = now;
Cobblemon.LOGGER.info("[PhotonRT integration] emitter hook fired: {} active particle light(s), {} pushed", active.size(), pushed);
}
}
}
@@ -25,6 +25,7 @@ object CobblemonCommands {
ChangeWalkSpeed.register(dispatcher)
TransformModelPartCommand.register(dispatcher)
TestCommand.register(dispatcher)
SpawnPikachuBattleCommand.register(dispatcher)
ReloadShowdownCommand.register(dispatcher)
ClickTextCommand.register(dispatcher)
PokemonEditCommand.register(dispatcher)
@@ -43,10 +43,26 @@ class BedrockParticle(
var tinting: ParticleTinting = ExpressionParticleTinting(),
var collision: ParticleCollision = ParticleCollision(),
var environmentLighting: Boolean = false,
/**
* Block light level (0..15) each particle of this type emits into the world via LambDynamicLights.
* A particle existing does not imply light: only particles whose resolved level is above 0 emit.
* This is the flat level, used when [lightEmissionFrames] is empty.
*/
var lightEmission: Int = 0,
/**
* Optional light levels (0..15) indexed by the particle's current flipbook frame. When non empty it
* overrides [lightEmission] so a particle can light differently per frame; frames beyond the array
* clamp to its ends. Empty means use the flat [lightEmission].
*/
var lightEmissionFrames: List<Int> = emptyList(),
var creationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
var expirationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
var timeline: EventTriggerTimeline = EventTriggerTimeline(mutableMapOf())
) {
/** True when this particle type emits block light at all, via the flat level or any per frame level. */
val emitsLight: Boolean
get() = lightEmission > 0 || lightEmissionFrames.any { it > 0 }
class ExpressionSet(
val sizeX: Expression,
val sizeY: Expression,
@@ -113,6 +129,8 @@ class BedrockParticle(
ParticleTinting.codec.fieldOf("tinting").forGetter { it.tinting },
ParticleCollision.CODEC.fieldOf("collision").forGetter { it.collision },
PrimitiveCodec.BOOL.fieldOf("environmentLighting").forGetter { it.environmentLighting },
Codec.INT.optionalFieldOf("lightEmission", 0).forGetter { it.lightEmission },
Codec.INT.listOf().optionalFieldOf("lightEmissionFrames", emptyList()).forGetter { it.lightEmissionFrames },
EVENT_SET_CODEC.fieldOf("eventSet").forGetter {
EventSet(
it.creationEvents,
@@ -121,7 +139,7 @@ class BedrockParticle(
)
}
).apply(instance) {
texture, materialStr, uvMode, expressionSet, motion, rotation, viewDirection, cameraMode, tinting, collision, environmentLighting, eventSet ->
texture, materialStr, uvMode, expressionSet, motion, rotation, viewDirection, cameraMode, tinting, collision, environmentLighting, lightEmission, lightEmissionFrames, eventSet ->
BedrockParticle(
texture = texture,
material = ParticleMaterial.valueOf(materialStr),
@@ -139,6 +157,8 @@ class BedrockParticle(
tinting = tinting,
collision = collision,
environmentLighting = environmentLighting,
lightEmission = lightEmission,
lightEmissionFrames = lightEmissionFrames,
creationEvents = eventSet.creationEvents,
expirationEvents = eventSet.expirationEvents,
timeline = eventSet.timeline
@@ -164,6 +184,8 @@ class BedrockParticle(
ParticleTinting.writeToBuffer(buffer, tinting)
collision.writeToBuffer(buffer)
buffer.writeBoolean(environmentLighting)
buffer.writeVarInt(lightEmission)
buffer.writeCollection(lightEmissionFrames) { pb, level -> pb.writeVarInt(level) }
buffer.writeCollection(creationEvents) { _, event -> event.encode(buffer) }
buffer.writeCollection(expirationEvents) { _, event -> event.encode(buffer) }
timeline.encode(buffer)
@@ -186,6 +208,8 @@ class BedrockParticle(
tinting = ParticleTinting.readFromBuffer(buffer)
collision.readFromBuffer(buffer)
environmentLighting = buffer.readBoolean()
lightEmission = buffer.readVarInt()
lightEmissionFrames = buffer.readList { it.readVarInt() }
creationEvents = buffer.readList { SimpleEventTrigger("").also { it.decode(buffer) } }
expirationEvents = buffer.readList { SimpleEventTrigger("").also { it.decode(buffer) } }
timeline.decode(buffer)
@@ -75,4 +75,4 @@ object ParticleMaterials {
override fun toString() = "OPAQUE"
}
}
}
@@ -44,6 +44,10 @@ abstract class ParticleUVMode : CodecMapped {
open var vSize: Expression = NumberExpression(8.0)
abstract fun get(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double, uvDetails: UVDetails): UVDetails
/** The current flipbook frame index for a particle at this age (0 for static modes). Used to look
* up per frame light levels (see [BedrockParticle.lightEmissionFrames]). */
open fun frameAt(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double): Int = 0
}
enum class ParticleUVModeType {
@@ -123,6 +127,18 @@ class AnimatedParticleUVMode(
buffer.writeBoolean(loop)
}
override fun frameAt(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double): Int {
val maxFrame = moLangRuntime.resolveInt(maxFrame) - 1
if (maxFrame <= 0) return 0
val frame = if (stretchToLifetime) {
if (maxAge <= 0.0) 0 else ((age / maxAge) * maxFrame).toInt()
} else {
val fps = moLangRuntime.resolveDouble(fps)
if (!loop && age * fps >= maxFrame) maxFrame else ((age * fps) % maxFrame).toInt()
}
return frame.coerceIn(0, maxFrame)
}
override fun get(runtime: MoLangRuntime, age: Double, maxAge: Double, uvDetails: UVDetails): UVDetails {
val maxFrame = runtime.resolveInt(maxFrame) - 1
val stepU = runtime.resolveDouble(stepU)
@@ -33,6 +33,8 @@ import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.PokemonAnimationLight
import com.cobblemon.mod.common.client.render.SnowstormShaderBypassRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
import com.cobblemon.mod.common.client.render.color.AprijuiceItemColorProvider
@@ -140,6 +142,9 @@ object CobblemonClient {
PlatformEvents.CLIENT_PLAYER_LOGIN.subscribe { onLogin() }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { onLogout() }
// Redraws snowstorm particles so they match vanilla when a shaderpack is active.
SnowstormShaderBypassRenderer.register()
this.registerBlockEntityRenderers()
registerBlockRenderTypes()
registerColors()
@@ -470,6 +475,8 @@ object CobblemonClient {
LOGGER.info("Loading assets...")
// Particles come first because animations need them.
BedrockParticleOptionsRepository.loadEffects(resourceManager)
// Emissive light windows keyed by animation; does nothing unless LambDynamicLights is installed.
PokemonAnimationLight.load(resourceManager)
// Animations come next because models need them.
BedrockAnimationRepository.loadAnimations(
resourceManager = resourceManager,
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.client.particle
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.snowstorm.BedrockParticleOptions
import com.cobblemon.mod.common.particle.SnowstormParticleReader
@@ -45,7 +46,60 @@ object BedrockParticleOptionsRepository {
}
LOGGER.info("Loaded ${effects.size} particle effects")
loadLightConfig(resourceManager)
}
/**
* Loads particle light levels from a separate Cobblemon data location and merges them onto the
* already loaded effects. These are kept out of the `.particle.json` on purpose: Snowstorm (the
* Bedrock particle editor) rebuilds the components block on export and silently drops unknown
* components, so a light field inside the particle file would be lost the next time it is saved.
* A sidecar file is never touched by Snowstorm.
*
* Files live under `assets/<namespace>/dynamiclights/particles/` (any `.json` file), each a map of
* effect id to its light config, e.g.:
* ```json
* {
* "cobblemon:thunderbolt_actor": { "light_level": 14 },
* "cobblemon:flamethrower_actor": { "light_levels": [0, 8, 14, 14, 8] }
* }
* ```
* `light_level` is a flat level (0..15) every particle emits; the optional `light_levels` array gives
* a level per flipbook frame and overrides the flat level. Entries for unknown effect ids are ignored.
*/
private fun loadLightConfig(resourceManager: ResourceManager) {
// LambDynamicLights is the only thing that consumes these levels. If it isn't installed, don't
// even read the sidecar files; the light config is never parsed, applied, or considered.
val lambDynamicLightsInstalled = Cobblemon.implementation.isModInstalled("lambdynlights") ||
Cobblemon.implementation.isModInstalled("lambdynamiclights")
if (!lambDynamicLightsInstalled) return
var applied = 0
val configPaths = listOf("dynamiclights/particles", "particle_light")
for (configPath in configPaths) {
resourceManager.listResources(configPath) { path -> path.endsWith(".json") }.forEach { identifier, resource ->
try {
resource.open().use { stream ->
val json = String(stream.readAllBytes(), StandardCharsets.UTF_8)
val root = GSON.fromJson(json, JsonObject::class.java)
for ((key, value) in root.entrySet()) {
val effect = effects[ResourceLocation.parse(key)] ?: continue
val config = value.asJsonObject
config.get("light_level")?.asInt?.let { effect.particle.lightEmission = it.coerceIn(0, 15) }
config.getAsJsonArray("light_levels")?.let { arr ->
effect.particle.lightEmissionFrames = arr.map { it.asInt.coerceIn(0, 15) }
}
applied++
}
}
} catch (e: Exception) {
LOGGER.error("Failed to load particle light config $identifier", e)
}
}
}
if (applied > 0) LOGGER.info("Applied dynamic-light config to $applied particle effect(s)")
}
fun getEffect(identifier: ResourceLocation): BedrockParticleOptions? = effects[identifier]
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.particle
import com.cobblemon.mod.common.client.render.SnowstormParticle
/**
* Bridge that lets the optional LambDynamicLights integration attach a dynamic light to individual
* glowing particles without the common particle code referencing the LambDynamicLights API directly.
*
* Each particle whose resolved light level is above 0 (and that claims one of its emitter's limited
* light slots) registers its own light through [sink]. When LambDynamicLights is installed its
* entrypoint ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets
* [sink]; when it is absent [sink] stays null and the registration call does nothing, so there is no
* hard dependency.
*/
object MoveDynamicLights {
@JvmStatic
var sink: GlowingParticleLightSink? = null
}
/** Receives an individual glowing [SnowstormParticle] so a real dynamic light can be attached to it. */
fun interface GlowingParticleLightSink {
fun register(particle: SnowstormParticle)
}
@@ -11,6 +11,7 @@ package com.cobblemon.mod.common.client.particle
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.VariableStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.function.LivingEntityMoLangFunctions
@@ -374,4 +375,23 @@ class ParticleStorm(
fun setInvisible() {
particles.forEach { particle -> particle.invisible = true }
}
/** How many of this emitter's live particles currently hold a dynamic light, capped by the
* maxLitParticlesPerEmitter config. A particle existing does not imply a light. */
var litParticleCount = 0
private set
/** Claims one of this emitter's limited light slots for a particle. Returns true if a slot was
* free (and reserved), false if the emitter is at the configured cap. A cap of 0 disables the
* particle lights entirely. */
fun reserveLightSlot(): Boolean {
if (litParticleCount >= Cobblemon.config.maxLitParticlesPerEmitter) return false
litParticleCount++
return true
}
/** Releases a previously reserved light slot (called when a lit particle is removed). */
fun releaseLightSlot() {
if (litParticleCount > 0) litParticleCount--
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.particle
import java.util.concurrent.ConcurrentHashMap
/**
* Cobblemon-side registry of moving dynamic lights (glowing move particles, and later other emitters)
* exposed to the optional Photonic raytracing mod.
*
* Unlike LambDynamicLights (which only edits the vanilla lightmap), Photonic raytraces real light
* sources and has NO public API to push per-frame point lights. So instead of pushing into Photonic,
* Cobblemon keeps the set of currently-active dynamic lights here, and a Photonic-targeting mixin
* (loaded only when "photonics" is present) READS this set each light-registry build and injects the
* entries into Photonic's light grid. When Photonic is absent nothing reads this set; [enabled] stays
* false and [register] is a cheap no-op, so there is no hard dependency and no overhead.
*
* The set is concurrent because Photonic compiles its light registry on a background thread while the
* client thread spawns/expires particles.
*
* @see com.cobblemon.mod.common.client.render.SnowstormParticle for the registration site
* @see PhotonicDynamicLight for the per-light contract the mixin reads
*/
object PhotonicDynamicLights {
/** Set true by the Photonic mixin during its init, once it confirms it can read this registry. */
@JvmStatic
@Volatile
var enabled: Boolean = false
private val lights = ConcurrentHashMap.newKeySet<PhotonicDynamicLight>()
/** Registers a moving light. No-op (and unreferenced) unless Photonic is present and active. */
@JvmStatic
fun register(light: PhotonicDynamicLight) {
if (enabled) lights.add(light)
}
/**
* Snapshot of the currently-alive lights, pruning any that have expired. Called by the Photonic
* mixin when it rebuilds its light grid. Pruning here (rather than on a tick) keeps the dead-light
* cleanup tied to actual consumption.
*/
@JvmStatic
fun activeSnapshot(): List<PhotonicDynamicLight> {
if (lights.isEmpty()) return emptyList()
lights.removeIf { !it.isAlive }
return lights.toList()
}
/** Drops every tracked light (e.g. on world unload). */
@JvmStatic
fun clear() {
lights.clear()
}
}
/**
* One moving light the Photonic mixin can read each frame: a world position, an RGB color (0..1,
* sampled from the source's sprite where applicable), and a 0..15 luminance. [isAlive] returning false
* marks the light for removal on the next [PhotonicDynamicLights.activeSnapshot].
*/
interface PhotonicDynamicLight {
val x: Double
val y: Double
val z: Double
/** Linear RGB, 0..1. */
val red: Float
val green: Float
val blue: Float
/** Vanilla-style light level, 0..15. */
val luminance: Int
val isAlive: Boolean
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockPoseAnimation
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.endsWith
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import java.nio.charset.StandardCharsets
import net.minecraft.server.packs.resources.ResourceManager
/**
* Light levels emitted by a Pokemon's emissive parts during specific animations, for the optional
* LambDynamicLights integration. Some Pokemon only show their emissive geometry at certain moments
* (Pikachu's cheeks, the Cyndaquil line's flames flaring in battle, Bellibolt's belly), so the level
* applies only while a matching animation's elapsed time falls inside one of its windows.
*
* Windows are matched against the current primary animation (moves such as special, physical, status),
* active stateful animations (cry, battle_cry), and the current pose's idle animations (battle_idle,
* air_idle, air_fly) which loop continuously. Data lives in JSON under
* `assets/<namespace>/dynamiclights/entity/animations/`, mapping species to animation name to a list
* of `{ start, end, level }` in seconds.
*/
object PokemonAnimationLight {
private val GSON = GsonBuilder().create()
private class Window(val start: Float, val end: Float, val level: Int)
// species path -> animation short name -> windows
private var data: Map<String, Map<String, List<Window>>> = emptyMap()
fun load(resourceManager: ResourceManager) {
// Only LambDynamicLights consumes these; skip entirely if it isn't installed.
if (!Cobblemon.implementation.isModInstalled("lambdynlights")) {
data = emptyMap()
Cobblemon.LOGGER.info("[AnimLight] LambDynamicLights not installed; animation lights disabled")
return
}
val loaded = mutableMapOf<String, MutableMap<String, List<Window>>>()
resourceManager.listResources("dynamiclights/entity/animations") { path -> path.endsWith(".json") }
.forEach { identifier, resource ->
try {
resource.open().use { stream ->
val root = GSON.fromJson(String(stream.readAllBytes(), StandardCharsets.UTF_8), JsonObject::class.java)
for ((species, animsElement) in root.entrySet()) {
val animMap = loaded.getOrPut(species) { mutableMapOf() }
for ((animName, windowsElement) in animsElement.asJsonObject.entrySet()) {
animMap[animName] = windowsElement.asJsonArray.map { w ->
val o = w.asJsonObject
Window(o.get("start").asFloat, o.get("end").asFloat, o.get("level").asInt.coerceIn(0, 15))
}
}
}
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[AnimLight] Failed to load animation light config $identifier", e)
}
}
data = loaded
Cobblemon.LOGGER.info("[AnimLight] loaded animation dynamic-light windows for ${loaded.size} species: ${loaded.keys}")
}
/** Block light (0..15) this Pokemon's emissive parts emit right now based on its active animation, or 0. */
@JvmStatic
fun luminanceFor(entity: PokemonEntity): Int {
if (data.isEmpty()) return 0
val perAnim = data[entity.exposedSpecies.resourceIdentifier.path] ?: return 0
val state = entity.delegate as? PosableState ?: return 0
val seconds = state.animationSeconds
var best = 0
// startedSeconds != null -> timed (primary/stateful); null -> continuous pose idle (use the loop clock)
fun match(animation: BedrockAnimation?, startedSeconds: Float?) {
animation ?: return
val windows = perAnim[animation.name.substringAfterLast('.')] ?: return
val length = animation.animationLength.toFloat()
val elapsed: Float = if (startedSeconds != null && startedSeconds >= 0f) {
val e = seconds - startedSeconds
if (animation.shouldLoop && length > 0f) (e % length + length) % length else e
} else if (length > 0f) {
(seconds % length + length) % length
} else {
Float.NaN // continuous, no length info -> treat as always within its windows
}
for (w in windows) {
val inWindow = elapsed.isNaN() || (elapsed >= w.start && elapsed <= w.end)
if (inWindow && w.level > best) best = w.level
}
}
// Move animations: primary (special/physical/status/volttackle) and stateful (cry/battle_cry).
(state.primaryAnimation?.animation as? BedrockActiveAnimation)?.let { match(it.animation, it.startedSeconds) }
state.allActiveAnimations.forEach { (it as? BedrockActiveAnimation)?.let { b -> match(b.animation, b.startedSeconds) } }
// Pose idle animations (battle_idle / air_idle / air_fly etc.) run continuously off the pose, not as active animations.
state.currentModel?.poses?.get(state.currentPose)?.animations?.forEach { poseAnim ->
(poseAnim as? BedrockPoseAnimation)?.let { match(it.animation, null) }
}
return best
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import java.lang.reflect.Method
/**
* Detects whether a shaderpack is currently active, via Iris (Fabric/NeoForge) or OptiFine.
*
* Every reference here is reflective and fully guarded: Iris/OptiFine are optional and may be
* absent at runtime, so we never reference their classes directly. If neither is present (or their
* APIs ever change shape) the lookups fail silently during class init and [shadersActive] simply
* returns `false`. The rest of the particle code relies on this contract staying safe under any loader — it
* must work identically on Fabric and NeoForge and when no shader mod is installed at all.
*
* The reflective handles are resolved once; only the cheap boolean accessor is invoked per call.
* Particle render types call this at most a handful of times per frame (once per [begin]), so the
* cost is negligible.
*/
object ShaderCompat {
// Iris: net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse()
// getInstance() returns a stable singleton, so it is safe to cache.
private val irisInstance: Any?
private val irisIsShaderPackInUse: Method?
// OptiFine: net.optifine.Config.isShaders() (static)
private val optifineIsShaders: Method?
init {
var irisInst: Any? = null
var irisMethod: Method? = null
try {
val irisApi = Class.forName("net.irisshaders.iris.api.v0.IrisApi")
irisInst = irisApi.getMethod("getInstance").invoke(null)
irisMethod = irisApi.getMethod("isShaderPackInUse")
} catch (ignored: Throwable) {
irisInst = null
irisMethod = null
}
irisInstance = irisInst
irisIsShaderPackInUse = irisMethod
var optifineMethod: Method? = null
try {
optifineMethod = Class.forName("net.optifine.Config").getMethod("isShaders")
} catch (ignored: Throwable) {
optifineMethod = null
}
optifineIsShaders = optifineMethod
}
/**
* `true` when Iris or OptiFine reports an active (compiled and enabled) shaderpack. Never throws;
* any reflective failure is treated as "no shaders".
*/
fun shadersActive(): Boolean {
val instance = irisInstance
val irisMethod = irisIsShaderPackInUse
if (instance != null && irisMethod != null) {
try {
if (irisMethod.invoke(instance) == true) return true
} catch (ignored: Throwable) {}
}
val optifineMethod = optifineIsShaders
if (optifineMethod != null) {
try {
if (optifineMethod.invoke(null) == true) return true
} catch (ignored: Throwable) {}
}
return false
}
}
@@ -15,6 +15,9 @@ import com.cobblemon.mod.common.ModAPI
import com.cobblemon.mod.common.api.snowstorm.ParticleMaterial
import com.cobblemon.mod.common.api.snowstorm.ParticleMaterials
import com.cobblemon.mod.common.api.snowstorm.UVDetails
import com.cobblemon.mod.common.client.particle.MoveDynamicLights
import com.cobblemon.mod.common.client.particle.PhotonicDynamicLights
import com.cobblemon.mod.common.client.particle.SnowstormPhotonicLight
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.util.math.geometry.transformDirection
import com.cobblemon.mod.common.util.resolveBoolean
@@ -54,6 +57,15 @@ class SnowstormParticle(
) : Particle(world, x, y, z) {
companion object {
const val MAXIMUM_DISTANCE_CHANGE_PER_TICK_FOR_FRICTION = 0.005
/**
* Every live snowstorm particle, in spawn order. When a shaderpack is active we pull these out
* of the vanilla [net.minecraft.client.particle.ParticleEngine] (see [getRenderType] returning
* [NO_RENDER]) and redraw them after composite via [SnowstormShaderBypassRenderer] so the pack's
* particle program can't alter their look. Ordered by insertion; particles use identity equality
* so a LinkedHashSet is safe.
*/
val activeParticles: MutableSet<SnowstormParticle> = LinkedHashSet()
}
val sprite = getSpriteFromAtlas()
@@ -107,6 +119,10 @@ class SnowstormParticle(
runtime.environment.setSimpleVariable("emitter_random_4", DoubleValue(Random.Default.nextDouble()))
}
/** True once this particle has claimed one of its emitter's limited light slots. */
var litSlot = false
private set
init {
setParticleSpeed(initialVelocity.x, initialVelocity.y, initialVelocity.z)
roll = -storm.effect.particle.rotation.getInitialRotation(runtime).toFloat()
@@ -127,6 +143,52 @@ class SnowstormParticle(
}
}
storm.effect.particle.creationEvents.forEach { it.trigger(storm, this) }
activeParticles.add(this)
// Give this particle its own dynamic light only when LambDynamicLights is present, the type
// emits light, and the emitter still has a free light slot. The level comes from
// currentLightLevel(); when the mod is absent the sink is null and this does nothing.
// Attach a dynamic light when either dynamic-light integration is active (LambDynamicLights for
// the vanilla lightmap, Photonic for raytraced lights), the type emits light, and the emitter
// still has a free light slot. When neither mod is present both gates are false and nothing runs.
val lambSink = MoveDynamicLights.sink
val photonicActive = PhotonicDynamicLights.enabled
if ((lambSink != null || photonicActive) && storm.effect.particle.emitsLight && storm.reserveLightSlot()) {
litSlot = true
lambSink?.register(this)
if (photonicActive) PhotonicDynamicLights.register(SnowstormPhotonicLight(this))
}
}
/**
* The block light level (0..15) this particle emits right now. Uses the per frame array when the
* type defines one (indexed by the current flipbook frame), otherwise the flat level. Invisible
* particles emit nothing.
*/
fun currentLightLevel(): Int {
if (invisible) return 0
val particle = storm.effect.particle
val frames = particle.lightEmissionFrames
if (frames.isEmpty()) return particle.lightEmission
val frame = particle.uvMode.frameAt(runtime, age / 20.0, lifetime / 20.0)
return frames[frame.coerceIn(0, frames.size - 1)]
}
/**
* The particle's current RGB tint (0..1) used as its dynamic light color for the Photonic
* integration. Move particles are usually white sprites tinted at runtime, so the resolved tint —
* not the sprite — is the true emitted color.
*/
fun currentLightColor(): org.joml.Vector3f {
// getTint evaluates the tinting curve/gradient against the runtime; that can throw before the
// particle is fully initialized (e.g. at spawn, when age/lifetime aren't set yet and a gradient
// has no resolvable node). Fall back to white rather than crashing the particle engine.
val colour = try {
storm.getParticleColor() ?: storm.effect.particle.tinting.getTint(runtime)
} catch (e: Exception) {
return org.joml.Vector3f(1f, 1f, 1f)
}
return org.joml.Vector3f(colour.x, colour.y, colour.z)
}
override fun render(vertexConsumer: VertexConsumer, camera: Camera, tickDelta: Float) {
@@ -496,10 +558,21 @@ class SnowstormParticle(
runtime.environment.variable.setDirectly("particle_lifetime", DoubleValue(lifetime / 20.0))
}
override fun getRenderType() = particleTextureSheet
// Under an active shaderpack we suppress the vanilla ParticleEngine draw (it would be routed through
// the pack's translucent particle program and mangled differently per pack) and instead redraw after
// composite in SnowstormShaderBypassRenderer so the look matches vanilla under any pack. With no
// shaders this is the normal material sheet, so the majority path is unchanged.
override fun getRenderType() = if (Cobblemon.config.shaderParticleBypass && ShaderCompat.shadersActive()) NO_RENDER else particleTextureSheet
override fun remove() {
super.remove()
storm.particles.remove(this)
activeParticles.remove(this)
// Free our light slot so another particle of this emitter can light up. The registered behavior
// reports isRemoved() via this particle's isAlive(), so the manager drops the light on its own.
if (litSlot) {
storm.releaseLightSlot()
litSlot = false
}
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.RenderEvent
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferUploader
import com.mojang.blaze3d.vertex.Tesselator
import net.minecraft.client.Minecraft
import org.joml.Matrix4f
/**
* Renders Cobblemon's snowstorm particles so they look like vanilla under any pack while a
* shaderpack (Iris/OptiFine) is active.
*
* ## Why this exists
* Iris/OptiFine replace the vanilla particle shader with the shaderpack's translucent particle
* program for the duration of the world pass, and every pack composites that program differently —
* which made our particles render too solid (Complementary), invisible (Photon), or still darkening
* (Bliss) no matter how we tuned blend/alpha inside the vanilla [net.minecraft.client.particle.ParticleEngine].
* The pack has final authority there; we cannot win.
*
* The one place a shaderpack does NOT intercept a draw is **after** `LevelRenderer.renderLevel`
* returns: Iris only overrides shaders while `isRenderingWorld` is true, and it flips that off during
* its final composite (injected at the RETURN of world rendering). So we:
* 1. suppress the vanilla draw of these particles while shaders are active
* ([SnowstormParticle.getRenderType] returns NO_RENDER), and
* 2. redraw them here at [RenderEvent.Stage.POST_LEVEL] (after composite) with the exact vanilla GL
* state from [com.cobblemon.mod.common.api.snowstorm.ParticleMaterials] and the unchanged
* [SnowstormParticle.render] quad math.
*
* The main framebuffer's depth attachment still holds the full (opaque + translucent) world depth at
* this point — Iris reuses Minecraft's main depth texture and the composite only copies colour — so
* particles still occlude correctly behind terrain AND water/glass. They simply don't receive the
* pack's lighting/fog/bloom, which is exactly the intended "looks like vanilla" result.
*
* When no shaderpack is active this renderer does nothing and particles draw normally through the
* ParticleEngine, so the majority path (no shaders) is completely unchanged.
*
* @author Cobblemon
*/
object SnowstormShaderBypassRenderer {
// The world's camera view matrix for the current frame, captured at the late stage within the
// level pass where Fabric/NeoForge still have the camera transform applied. POST_LEVEL fires after
// that state is unwound, but the bypass draw still needs it because SnowstormParticle.render emits
// vertices relative to the camera just like vanilla particles do.
private val modelViewMatrix = Matrix4f()
private var captured = false
fun register() {
PlatformEvents.RENDER.subscribe { event ->
when (event.stage) {
RenderEvent.Stage.LAST -> {
modelViewMatrix.set(RenderSystem.getModelViewMatrix())
captured = true
}
RenderEvent.Stage.POST_LEVEL -> render(event)
else -> {}
}
}
}
private fun render(event: RenderEvent) {
if (!Cobblemon.config.shaderParticleBypass || !ShaderCompat.shadersActive() || !captured) {
captured = false
return
}
val visible = SnowstormParticle.activeParticles.filter { !it.invisible }
if (visible.isEmpty()) {
captured = false
return
}
val minecraft = Minecraft.getInstance()
val camera = event.camera
val partialTicks = event.tickCounter.getGameTimeDeltaPartialTick(false)
val lightTexture = minecraft.gameRenderer.lightTexture()
val tesselator = Tesselator.getInstance()
// Restore the world's camera view so the quads built relative to the camera by render()
// land in the right place; projection is still the world's 3D projection at this point in the frame.
val modelViewStack = RenderSystem.getModelViewStack()
modelViewStack.pushMatrix()
modelViewStack.set(modelViewMatrix)
RenderSystem.applyModelViewMatrix()
try {
lightTexture.turnOnLightLayer()
// Group by the particle's own render type (ParticleMaterials.ALPHA/ADD/BLEND/OPAQUE). Calling
// its begin() sets the IDENTICAL GL state vanilla would use, so the look matches the no shader path.
for ((renderType, particles) in visible.groupBy { it.particleTextureSheet }) {
val buffer = renderType.begin(tesselator, minecraft.textureManager) ?: continue
for (particle in particles) {
particle.render(buffer, camera, partialTicks)
}
val mesh = buffer.build() ?: continue
BufferUploader.drawWithShader(mesh)
}
} finally {
lightTexture.turnOffLightLayer()
modelViewStack.popMatrix()
RenderSystem.applyModelViewMatrix()
captured = false
}
// Restore neutral GL state so nothing downstream (hand already drawn, GUI next) is affected.
RenderSystem.depthMask(true)
RenderSystem.disableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.enableCull()
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.command
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.party
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.MobSpawnType
/**
* Test/dev command: spawns a wild level 20 Pikachu in front of the player and immediately starts a
* singles battle against it. Handy for exercising move animations/particles in battle (e.g. for the
* PhotonRT raytraced move-light integration). Op-only ({@code /spawnpikachubattle}).
*/
object SpawnPikachuBattleCommand {
private const val NAME = "spawnpikachubattle"
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
dispatcher.register(
Commands.literal(NAME)
.requires { it.hasPermission(2) }
.executes(::execute)
)
}
private fun execute(context: CommandContext<CommandSourceStack>): Int {
val player = context.source.entity as? ServerPlayer ?: run {
context.source.sendFailure(Component.literal("This command must be run by a player.").red())
return 0
}
val world = player.level() as ServerLevel
// The player needs at least one Pokémon to battle with.
val party = player.party()
party.heal()
val playerTeam = party.toBattleTeam()
if (playerTeam.isEmpty()) {
context.source.sendFailure(Component.literal("You need at least one Pokémon in your party to battle.").red())
return 0
}
// Spawn a wild level 20 Pikachu ~2 blocks in front of the player, facing them.
val look = player.lookAngle
val spawnPos = player.position().add(look.x * 2.0, 0.0, look.z * 2.0)
val pikachu = PokemonProperties.parse("pikachu level=20").createEntity(world)
pikachu.moveTo(spawnPos.x, spawnPos.y, spawnPos.z, player.yRot + 180f, 0f)
pikachu.finalizeSpawn(world, world.getCurrentDifficultyAt(pikachu.blockPosition()), MobSpawnType.COMMAND, null)
if (!world.addFreshEntity(pikachu)) {
context.source.sendFailure(Component.literal("Failed to spawn the Pikachu at the target position.").red())
return 0
}
// Start a single battle: the player (their party) vs the wild Pikachu.
BattleRegistry.startBattle(
BattleFormat.GEN_9_SINGLES,
BattleSide(PlayerBattleActor(player.uuid, playerTeam)),
BattleSide(PokemonBattleActor(pikachu.pokemon.uuid, BattlePokemon(pikachu.pokemon), Cobblemon.config.defaultFleeDistance))
)
context.source.sendSuccess({ Component.literal("Spawned a level 20 Pikachu and started a battle.") }, false)
return Command.SINGLE_SUCCESS
}
}
@@ -331,6 +331,13 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Interface, lang = "animate_battle_tiles", CLIENT)
var animateBattleTiles = false
@CobblemonConfigField(Category.Interface, lang = "shader_particle_bypass", CLIENT)
var shaderParticleBypass = true
@CobblemonConfigField(Category.Interface, lang = "max_lit_particles_per_emitter", CLIENT)
@IntConstraint(min = 0, max = 256)
var maxLitParticlesPerEmitter = 20
// Disabled as non-tenable for 1.7 until all the posers catch up with this property.
// @CobblemonConfigField(Category.Riding, lang = "third_person_view_bobbing", CLIENT)
var thirdPersonViewBobbing = true
@@ -363,6 +363,11 @@ object SnowstormParticleReader {
ExpressionParticleTinting()
}
val environmentLighting = componentsJson.has("minecraft:particle_appearance_lighting")
// Light levels are intentionally not read from the .particle.json. Snowstorm (the Bedrock
// particle editor) rebuilds the components block on export and silently drops unknown components,
// so a custom light field here would be lost the next time a file is saved. Light levels instead
// live in a separate Cobblemon data file, merged onto the effect after load (see
// BedrockParticleOptionsRepository.loadLightConfig).
val collision = collisionJson?.let {
var collides = true
val radius = it.get("collision_radius")?.asString?.asExpression() ?: run {
@@ -32,6 +32,22 @@ data class RenderEvent(
/** Represents when a layer is rendered by [LevelRenderer.renderLevel]. Ordinal corresponds with its render order. */
enum class Stage {
TRANSLUCENT
TRANSLUCENT,
/**
* Fired after the world framebuffer writes are complete but before the level renderer tears
* down its camera view transform. Direct draws at [POST_LEVEL] can reuse the matrix captured
* here when they need to render after shaderpack composition.
*/
LAST,
/**
* Fired after [LevelRenderer.renderLevel] has fully finished — on a shaderpack, this is after
* Iris/OptiFine has composited the world to the main framebuffer and restored vanilla shaders.
* Draws here are NOT intercepted by the shaderpack, so they render with vanilla shaders while
* still depth testing against the full (opaque + translucent) world depth. Used to render
* Cobblemon's snowstorm particles so they look like vanilla under shaders.
*/
POST_LEVEL
}
}
}
@@ -0,0 +1,65 @@
{
"pikachu": {
"battle_cry": [ { "start": 0.4583, "end": 0.5417, "level": 4 }, { "start": 0.75, "end": 0.7917, "level": 4 }, { "start": 0.875, "end": 0.9167, "level": 4 }, { "start": 1.25, "end": 1.3333, "level": 4 }, { "start": 1.375, "end": 1.4167, "level": 4 }, { "start": 1.5, "end": 1.5833, "level": 4 }, { "start": 1.625, "end": 1.6667, "level": 4 }, { "start": 1.7083, "end": 1.7917, "level": 4 }, { "start": 1.875, "end": 1.9583, "level": 4 } ],
"physical": [ { "start": 1.125, "end": 1.2083, "level": 4 }, { "start": 1.25, "end": 1.2917, "level": 4 }, { "start": 1.3333, "end": 1.4583, "level": 4 }, { "start": 1.5417, "end": 1.6667, "level": 4 } ],
"special": [ { "start": 0.0, "end": 0.625, "level": 4 }, { "start": 0.6667, "end": 1.3333, "level": 4 }, { "start": 1.375, "end": 1.4167, "level": 4 }, { "start": 1.5, "end": 1.5833, "level": 4 }, { "start": 1.625, "end": 1.6667, "level": 4 }, { "start": 1.7083, "end": 1.7917, "level": 4 }, { "start": 1.875, "end": 1.9583, "level": 4 } ],
"status": [ { "start": 0.4583, "end": 0.5417, "level": 4 }, { "start": 0.75, "end": 0.7917, "level": 4 }, { "start": 0.875, "end": 0.9167, "level": 4 }, { "start": 1.25, "end": 1.3333, "level": 4 }, { "start": 1.375, "end": 1.4167, "level": 4 }, { "start": 1.5, "end": 1.5833, "level": 4 }, { "start": 1.625, "end": 1.6667, "level": 4 }, { "start": 1.7083, "end": 1.7917, "level": 4 }, { "start": 1.875, "end": 1.9583, "level": 4 } ],
"volttackle": [ { "start": 0.375, "end": 0.4583, "level": 4 }, { "start": 0.6667, "end": 0.7083, "level": 4 }, { "start": 0.9167, "end": 1.0, "level": 4 }, { "start": 1.125, "end": 1.2083, "level": 4 }, { "start": 1.25, "end": 1.2917, "level": 4 }, { "start": 1.3333, "end": 1.4167, "level": 4 }, { "start": 1.5417, "end": 1.5833, "level": 4 } ]
},
"cyndaquil": {
"battle_cry": [ { "start": 0.0, "end": 0.3333, "level": 5 }, { "start": 0.3333, "end": 0.6667, "level": 6 }, { "start": 0.6667, "end": 1.0, "level": 5 }, { "start": 1.0, "end": 1.3333, "level": 4 }, { "start": 1.3333, "end": 1.6667, "level": 5 }, { "start": 1.6667, "end": 2.0, "level": 6 } ],
"battle_entrance": [ { "start": 1.0833, "end": 1.375, "level": 5 }, { "start": 1.375, "end": 1.6667, "level": 6 }, { "start": 1.6667, "end": 1.9583, "level": 5 }, { "start": 1.9583, "end": 2.2083, "level": 4 }, { "start": 2.2083, "end": 2.5, "level": 5 } ],
"battle_idle": [ { "start": 0.0, "end": 0.3333, "level": 5 }, { "start": 0.3333, "end": 0.6667, "level": 6 }, { "start": 0.6667, "end": 1.0, "level": 5 }, { "start": 1.0, "end": 1.3333, "level": 4 }, { "start": 1.3333, "end": 1.6667, "level": 5 }, { "start": 1.6667, "end": 2.0, "level": 6 } ]
},
"quilava": {
"battle_cry": [ { "start": 0.0, "end": 0.2917, "level": 6 }, { "start": 0.2917, "end": 0.5833, "level": 7 }, { "start": 0.5833, "end": 0.875, "level": 6 }, { "start": 0.875, "end": 1.1667, "level": 5 } ],
"battle_idle": [ { "start": 0.0, "end": 0.3333, "level": 6 }, { "start": 0.3333, "end": 0.6667, "level": 7 }, { "start": 0.6667, "end": 1.0, "level": 6 }, { "start": 1.0, "end": 1.3333, "level": 5 }, { "start": 1.3333, "end": 1.6667, "level": 6 }, { "start": 1.6667, "end": 2.0, "level": 7 } ],
"cry": [ { "start": 0.125, "end": 0.3333, "level": 6 }, { "start": 0.3333, "end": 0.5417, "level": 7 }, { "start": 0.5417, "end": 0.75, "level": 6 }, { "start": 0.75, "end": 0.9583, "level": 5 } ]
},
"typhlosion": {
"battle_cry": [ { "start": 0.0, "end": 0.3681, "level": 8 }, { "start": 0.3681, "end": 0.7361, "level": 9 }, { "start": 0.7361, "end": 1.1042, "level": 8 }, { "start": 1.1042, "end": 1.4722, "level": 7 }, { "start": 1.4722, "end": 1.8403, "level": 8 }, { "start": 1.8403, "end": 2.2083, "level": 9 } ],
"battle_idle": [ { "start": 0.0, "end": 0.3333, "level": 8 }, { "start": 0.3333, "end": 0.6667, "level": 9 }, { "start": 0.6667, "end": 1.0, "level": 8 }, { "start": 1.0, "end": 1.3333, "level": 7 }, { "start": 1.3333, "end": 1.6667, "level": 8 }, { "start": 1.6667, "end": 2.0, "level": 9 } ],
"cry": [ { "start": 0.0, "end": 0.3681, "level": 8 }, { "start": 0.3681, "end": 0.7361, "level": 9 }, { "start": 0.7361, "end": 1.1042, "level": 8 }, { "start": 1.1042, "end": 1.4722, "level": 7 }, { "start": 1.4722, "end": 1.8403, "level": 8 }, { "start": 1.8403, "end": 2.2083, "level": 9 } ]
},
"ampharos": {
"quirk_glow": [ { "start": 0.0833, "end": 2.0, "level": 5 } ]
},
"spiritomb": {
"cry": [ { "start": 0.0, "end": 1.5, "level": 3 } ]
},
"dusknoir": {
"battle_cry": [ { "start": 0.9583, "end": 1.125, "level": 3 }, { "start": 1.2917, "end": 1.4583, "level": 3 }, { "start": 1.625, "end": 1.7917, "level": 3 }, { "start": 1.9583, "end": 2.125, "level": 3 }, { "start": 2.2917, "end": 2.7083, "level": 3 } ],
"battle_idle": [ { "start": 0.0, "end": 2.0, "level": 3 } ],
"physical": [ { "start": 0.4167, "end": 0.5833, "level": 3 }, { "start": 0.75, "end": 0.9167, "level": 3 }, { "start": 1.0417, "end": 1.125, "level": 3 }, { "start": 1.2083, "end": 1.8333, "level": 3 } ],
"special": [ { "start": 0.3333, "end": 0.7083, "level": 3 }, { "start": 1.25, "end": 2.2083, "level": 3 } ],
"status": [ { "start": 0.9583, "end": 1.125, "level": 3 }, { "start": 1.2917, "end": 1.4583, "level": 3 }, { "start": 1.625, "end": 1.7917, "level": 3 }, { "start": 1.9583, "end": 2.125, "level": 3 }, { "start": 2.2917, "end": 2.7083, "level": 3 } ]
},
"golurk": {
"air_idle": [ { "start": 0.0, "end": 2.0, "level": 15 } ],
"air_fly": [ { "start": 0.0, "end": 2.0, "level": 15 } ],
"ride_air_idle": [ { "start": 0.0, "end": 2.0, "level": 15 } ],
"ride_air_fly": [ { "start": 0.0, "end": 2.0, "level": 15 } ],
"ride_jump": [ { "start": 0.0, "end": 2.0, "level": 15 } ],
"physical": [ { "start": 0.0, "end": 3.0, "level": 15 } ],
"physical2": [ { "start": 0.083, "end": 3.0, "level": 15 } ],
"special": [ { "start": 0.0, "end": 0.083, "level": 15 }, { "start": 1.208, "end": 1.333, "level": 15 }, { "start": 1.375, "end": 3.0, "level": 15 } ],
"battle_cry": [ { "start": 1.25, "end": 3.0, "level": 15 } ],
"status": [ { "start": 1.25, "end": 3.0, "level": 15 } ],
"recoil": [ { "start": 0.0, "end": 0.083, "level": 15 }, { "start": 0.125, "end": 0.208, "level": 15 }, { "start": 0.25, "end": 0.333, "level": 15 }, { "start": 0.375, "end": 0.458, "level": 15 }, { "start": 0.542, "end": 0.833, "level": 15 } ],
"faint": [ { "start": 0.0, "end": 3.0, "level": 15 } ],
"red": [ { "start": 0.0, "end": 2.0, "level": 15 } ]
},
"bellibolt": {
"battle_cry": [ { "start": 0.0, "end": 0.1667, "level": 3 }, { "start": 0.1667, "end": 0.25, "level": 6 }, { "start": 0.25, "end": 0.5, "level": 4 }, { "start": 0.5, "end": 0.5833, "level": 7 }, { "start": 0.5833, "end": 0.9167, "level": 3 }, { "start": 0.9167, "end": 1.0, "level": 6 }, { "start": 1.0, "end": 1.4167, "level": 4 }, { "start": 1.4167, "end": 1.5, "level": 7 }, { "start": 1.5, "end": 1.8333, "level": 3 }, { "start": 1.8333, "end": 1.9167, "level": 6 }, { "start": 1.9167, "end": 2.3333, "level": 4 }, { "start": 2.3333, "end": 2.4167, "level": 7 }, { "start": 2.4167, "end": 3.0, "level": 3 } ],
"battle_idle": [ { "start": 0.0, "end": 0.125, "level": 3 }, { "start": 0.125, "end": 0.2083, "level": 6 }, { "start": 0.2083, "end": 0.5, "level": 4 }, { "start": 0.5, "end": 0.5833, "level": 7 }, { "start": 0.5833, "end": 0.875, "level": 3 }, { "start": 0.875, "end": 0.9583, "level": 6 }, { "start": 0.9583, "end": 1.25, "level": 4 }, { "start": 1.25, "end": 1.3333, "level": 7 }, { "start": 1.3333, "end": 1.625, "level": 3 }, { "start": 1.625, "end": 1.7083, "level": 6 }, { "start": 1.7083, "end": 2.0, "level": 4 } ],
"cry": [ { "start": 0.0, "end": 0.125, "level": 3 }, { "start": 0.125, "end": 0.2083, "level": 6 }, { "start": 0.2083, "end": 0.4167, "level": 4 }, { "start": 0.4167, "end": 0.5, "level": 7 }, { "start": 0.5, "end": 0.75, "level": 3 }, { "start": 0.75, "end": 0.8333, "level": 6 }, { "start": 0.8333, "end": 1.0833, "level": 4 }, { "start": 1.0833, "end": 1.1667, "level": 7 }, { "start": 1.1667, "end": 1.2917, "level": 3 } ],
"physical": [ { "start": 0.0, "end": 0.1667, "level": 3 }, { "start": 0.1667, "end": 0.25, "level": 6 }, { "start": 0.25, "end": 0.5, "level": 4 }, { "start": 0.5, "end": 0.5833, "level": 7 }, { "start": 0.5833, "end": 0.9167, "level": 3 }, { "start": 0.9167, "end": 1.0, "level": 6 }, { "start": 1.0, "end": 1.4167, "level": 4 }, { "start": 1.4167, "end": 1.5, "level": 7 }, { "start": 1.5, "end": 1.8333, "level": 3 }, { "start": 1.8333, "end": 1.9167, "level": 6 }, { "start": 1.9167, "end": 2.3333, "level": 4 }, { "start": 2.3333, "end": 2.4167, "level": 7 }, { "start": 2.4167, "end": 3.0, "level": 3 } ],
"special": [ { "start": 0.0, "end": 0.0833, "level": 5 }, { "start": 0.0833, "end": 0.1667, "level": 8 }, { "start": 0.1667, "end": 0.3333, "level": 4 }, { "start": 0.3333, "end": 0.4167, "level": 7 }, { "start": 0.4167, "end": 0.6667, "level": 3 }, { "start": 0.6667, "end": 0.75, "level": 8 }, { "start": 0.75, "end": 1.0833, "level": 4 }, { "start": 1.0833, "end": 1.1667, "level": 7 }, { "start": 1.1667, "end": 1.5, "level": 3 }, { "start": 1.5, "end": 1.5833, "level": 8 }, { "start": 1.5833, "end": 2.0, "level": 4 }, { "start": 2.0, "end": 2.0833, "level": 7 }, { "start": 2.0833, "end": 2.5, "level": 3 }, { "start": 2.5, "end": 2.5833, "level": 8 }, { "start": 2.5833, "end": 3.0, "level": 4 } ],
"status": [ { "start": 0.0, "end": 0.1667, "level": 3 }, { "start": 0.1667, "end": 0.25, "level": 6 }, { "start": 0.25, "end": 0.5, "level": 4 }, { "start": 0.5, "end": 0.5833, "level": 7 }, { "start": 0.5833, "end": 0.9167, "level": 3 }, { "start": 0.9167, "end": 1.0, "level": 6 }, { "start": 1.0, "end": 1.4167, "level": 4 }, { "start": 1.4167, "end": 1.5, "level": 7 }, { "start": 1.5, "end": 1.8333, "level": 3 }, { "start": 1.8333, "end": 1.9167, "level": 6 }, { "start": 1.9167, "end": 2.3333, "level": 4 }, { "start": 2.3333, "end": 2.4167, "level": 7 }, { "start": 2.4167, "end": 3.0, "level": 3 } ]
},
"kilowattrel": {
"battle_idle": [ { "start": 0.0, "end": 2.0, "level": 4 } ],
"cry": [ { "start": 0.0, "end": 1.8333, "level": 4 } ],
"special": [ { "start": 1.125, "end": 2.25, "level": 4 } ]
}
}
@@ -0,0 +1,742 @@
{
"cobblemon:absorb_actor": { "light_levels": [1, 2, 2, 3, 4, 2, 1, 1, 1] },
"cobblemon:absorb_actorhit": { "light_levels": [1, 1, 1, 2, 4, 2, 1, 1, 1] },
"cobblemon:absorb_actortrail": { "light_levels": [2, 1] },
"cobblemon:afterspark": { "light_levels": [2, 2, 1, 1] },
"cobblemon:ancientazureball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientazureball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientazureball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientazureball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientazureball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientazureball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientazureball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientcitrineball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientcitrineball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientcitrineball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientcitrineball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientcitrineball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientcitrineball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientcitrineball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientfeatherball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientfeatherball/battle/ancientfeatherballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientfeatherball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientfeatherball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 2, 2, 3, 3, 2, 2, 1, 0] },
"cobblemon:ancientfeatherball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientfeatherball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientfeatherball/casual/sendflash": { "light_levels": [0, 1, 2, 3, 2, 2, 3, 3, 2, 2, 1, 0] },
"cobblemon:ancientgigatonball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientgigatonball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientgigatonball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientgigatonball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 3, 3, 3, 2, 1, 0] },
"cobblemon:ancientgigatonball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientgigatonball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientgigatonball/casual/sendflash": { "light_levels": [0, 1, 2, 3, 3, 2, 2, 3, 3, 2, 1, 0] },
"cobblemon:ancientgreatball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientgreatball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientgreatball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientgreatball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 0] },
"cobblemon:ancientgreatball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientgreatball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientgreatball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientheavyball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientheavyball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientheavyball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientheavyball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 0] },
"cobblemon:ancientheavyball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientheavyball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientheavyball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:ancientivoryball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientivoryball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientivoryball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientivoryball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientivoryball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientivoryball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientivoryball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientjetball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientjetball/battle/ancientjetballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientjetball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientjetball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 4, 4, 5, 4, 4, 3, 3, 3, 2, 1, 0] },
"cobblemon:ancientjetball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientjetball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientjetball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 4, 5, 4, 3, 2, 1, 0] },
"cobblemon:ancientleadenball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientleadenball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientleadenball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientleadenball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:ancientleadenball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientleadenball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientleadenball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:ancientoriginball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientoriginball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientoriginball/battle/ballsparks": { "light_levels": [3] },
"cobblemon:ancientoriginball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0] },
"cobblemon:ancientoriginball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientoriginball/casual/ballsparks": { "light_levels": [3] },
"cobblemon:ancientoriginball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 4, 2, 2, 1, 1, 1, 1, 0, 0] },
"cobblemon:ancientpokeball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientpokeball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientpokeball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientpokeball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientpokeball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientpokeball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientpokeball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientroseateball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientroseateball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientroseateball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientroseateball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientroseateball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientroseateball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientroseateball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientslateball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientslateball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientslateball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientslateball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientslateball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientslateball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientslateball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientultraball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientultraball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientultraball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientultraball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientultraball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientultraball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientultraball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 1, 1, 1, 0] },
"cobblemon:ancientverdantball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientverdantball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientverdantball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientverdantball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ancientverdantball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientverdantball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ancientverdantball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ancientwingball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ancientwingball/battle/ancientwingballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientwingball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientwingball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 3, 3, 3, 4, 3, 3, 1, 0] },
"cobblemon:ancientwingball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ancientwingball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:ancientwingball/casual/sendflash": { "light_levels": [0, 1, 2, 3, 3, 3, 4, 4, 3, 3, 1, 0] },
"cobblemon:aurorabeam_actor": { "light_levels": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] },
"cobblemon:aurorabeam_actoremit": { "light_levels": [2, 3, 5, 6] },
"cobblemon:aurorabeam_actorfrost": { "light_levels": [2, 1, 1] },
"cobblemon:aurorabeam_actorspray": { "light_levels": [3, 5, 6, 5, 3, 3, 1] },
"cobblemon:aurorabeam_charge": { "light_levels": [1, 3, 3, 5, 6] },
"cobblemon:aurorabeam_target": { "light_levels": [2, 2, 2, 3, 3, 2] },
"cobblemon:aurorabeam_targetburst": { "light_levels": [3, 5, 6, 5, 3, 3, 1] },
"cobblemon:aurorabeam_targetsparkle": { "light_levels": [4, 3, 2, 1] },
"cobblemon:azureball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:azureball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:azureball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:azureball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:azureball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:azureball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:azureball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:beastball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:beastball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:beastball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:beastball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 0] },
"cobblemon:beastball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:beastball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:beastball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 1, 0] },
"cobblemon:burn_actor": { "light_levels": [0, 2, 4, 3, 4, 4, 3] },
"cobblemon:burn_actorresidual": { "light_levels": [0, 2, 1, 0, 0] },
"cobblemon:capturesparks": { "light_levels": [0, 1, 3, 1, 0, 0] },
"cobblemon:capturestar": { "light_levels": [0, 1, 1, 3, 3, 3, 3, 3, 3, 1, 1, 0, 0] },
"cobblemon:cherishball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:cherishball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:cherishball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:cherishball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0] },
"cobblemon:cherishball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:cherishball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:cherishball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:citrineball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:citrineball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:citrineball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:citrineball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:citrineball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:citrineball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:citrineball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:daimonji1": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:daimonji2": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:daimonji3": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:daimonji4": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:diveball/ballsparkle": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:diveball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:diveball/battle/ballsparks": { "light_levels": [1, 2, 3, 3, 1, 0] },
"cobblemon:diveball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 0] },
"cobblemon:diveball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:diveball/casual/ballsparks": { "light_levels": [1, 2, 3, 3, 1, 0] },
"cobblemon:diveball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 3, 3, 2, 2, 0] },
"cobblemon:dracometeor_accent": { "light_levels": [11] },
"cobblemon:dracometeor_accenttrail": { "light_levels": [10, 8, 7, 7, 6, 6, 5, 5] },
"cobblemon:dracometeor_actoraurasparkle": { "light_levels": [5, 6, 7, 8, 7, 6] },
"cobblemon:dracometeor_actorpulse": { "light_levels": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] },
"cobblemon:dracometeor_actorpulseaura": { "light_levels": [10, 8, 7, 7, 6, 6, 5, 5] },
"cobblemon:dracometeor_bigburst": { "light_levels": [6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_bigring": { "light_levels": [5, 5, 5, 5, 5, 5, 5] },
"cobblemon:dracometeor_burst": { "light_levels": [6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_meteortrail1": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_meteortrail2": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_meteortrail3": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_meteortrail4": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:dracometeor_ring": { "light_levels": [5, 5, 5, 5, 5, 5, 5] },
"cobblemon:dragonbreath_actor": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:dragonbreath_target": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:dragonclaw_aura": { "light_levels": [3, 3, 3, 4, 3, 3] },
"cobblemon:dragonclaw_target": { "light_levels": [3, 4, 4, 4, 3, 3, 3] },
"cobblemon:dragonrage_actor": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:dragonrage_actorburst": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:dragonrage_actorring": { "light_levels": [3, 3, 3, 3, 3, 3, 3, 2] },
"cobblemon:dragonrage_target": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:dragonrage_targetcover": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:drainpunch_actor": { "light_levels": [0, 1, 1, 3, 7, 3, 1, 1, 0] },
"cobblemon:drainpunch_target": { "light_levels": [3, 2, 2, 1, 0, 0, 0, 0, 0] },
"cobblemon:drainpunch_targetburst": { "light_levels": [0, 2, 5, 2, 0, 0, 0] },
"cobblemon:drainpunch_targetring": { "light_levels": [1, 1, 1, 1, 1, 1] },
"cobblemon:dreamball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:dreamball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:dreamball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:dreamball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 4, 4, 5, 4, 4, 2, 1, 1, 1, 0, 0, 0] },
"cobblemon:dreamball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:dreamball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:dreamball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 4, 4, 4, 1, 1, 0, 0] },
"cobblemon:duskball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:duskball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:duskball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:duskball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 4, 4, 5, 4, 4, 4, 3, 3, 2, 2, 1, 0] },
"cobblemon:duskball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:duskball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:duskball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 5, 5, 4, 3, 2, 2, 1, 0] },
"cobblemon:eggbomb_targetfire": { "light_levels": [2, 2, 4, 6, 7, 7, 6, 4, 2, 2] },
"cobblemon:ember_actor": { "light_levels": [2, 2, 3, 4, 4, 3, 2, 2, 2] },
"cobblemon:ember_fire": { "light_levels": [3, 6, 5, 3, 2] },
"cobblemon:ember_firesparks": { "light_levels": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] },
"cobblemon:ember_froth": { "light_levels": [3, 6, 5, 3, 2] },
"cobblemon:ember_impact": { "light_levels": [3, 6, 5, 3, 2] },
"cobblemon:ember_impactlinger": { "light_levels": [3, 6, 5, 3, 2] },
"cobblemon:ember_target": { "light_levels": [3, 4, 4, 3, 2, 2, 2] },
"cobblemon:ember_targetlinger": { "light_levels": [3, 6, 5, 3, 2] },
"cobblemon:eruption_actor": { "light_levels": [6, 6, 7, 8, 9, 9, 8, 7, 6, 6] },
"cobblemon:eruption_actorburst": { "light_levels": [6, 7, 8, 9, 9, 8, 7, 6, 6] },
"cobblemon:eruption_actorcinder": { "light_levels": [8, 11, 10, 8, 6] },
"cobblemon:eruption_actorlava": { "light_levels": [9] },
"cobblemon:eruption_actorspume": { "light_levels": [6, 7, 8, 9, 9, 8, 7, 6, 6] },
"cobblemon:eruption_target": { "light_levels": [11] },
"cobblemon:eruption_targetburst": { "light_levels": [6, 7, 8, 9, 9, 8, 7, 6, 6] },
"cobblemon:eruption_targetflame": { "light_levels": [6, 7, 8, 9, 9, 8, 7, 6, 6] },
"cobblemon:evo_bigsparkle": { "light_levels": [3, 4, 5, 6, 5, 4] },
"cobblemon:evo_buildup": { "light_levels": [3, 4, 6] },
"cobblemon:evo_flyingsparkles": { "light_levels": [6, 4, 4, 3] },
"cobblemon:evo_godrays": { "light_levels": [8] },
"cobblemon:evo_godrays_small": { "light_levels": [8] },
"cobblemon:evo_implode": { "light_levels": [8, 6, 4, 3] },
"cobblemon:evo_obscuring": { "light_levels": [3, 5, 7, 8, 9, 9] },
"cobblemon:evo_particles": { "light_levels": [3] },
"cobblemon:evo_smokeburst": { "light_levels": [11, 10, 9, 7, 5, 3] },
"cobblemon:evo_sparkle": { "light_levels": [6, 4, 4, 3] },
"cobblemon:evo_sparkleburst": { "light_levels": [6, 4, 4, 3] },
"cobblemon:evo_sparklefetti": { "light_levels": [6, 6] },
"cobblemon:explosion_actorbigring": { "light_levels": [13] },
"cobblemon:explosion_actorboom": { "light_levels": [15] },
"cobblemon:explosion_actorfire": { "light_levels": [6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:explosion_actororbs": { "light_levels": [3] },
"cobblemon:explosion_actorrelease": { "light_levels": [9] },
"cobblemon:explosion_actorrings": { "light_levels": [5, 6, 6, 7, 8, 8, 8, 6, 5] },
"cobblemon:explosion_burst_fire": { "light_levels": [13, 13, 13, 13, 13, 13, 13] },
"cobblemon:explosion_burst_flash": { "light_levels": [15] },
"cobblemon:explosion_charge_accretion": { "light_levels": [8] },
"cobblemon:explosion_charge_light": { "light_levels": [8] },
"cobblemon:explosion_target": { "light_levels": [15, 15, 15, 15, 15, 15, 15] },
"cobblemon:fastball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:fastball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 0] },
"cobblemon:fastball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:fastball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0] },
"cobblemon:fastball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 0] },
"cobblemon:fastball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:fastball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0] },
"cobblemon:fireblast_actor1": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:fireblast_actor2": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:fireblast_actorshoot": { "light_levels": [5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:fireblast_target": { "light_levels": [5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:fireblast_targetcinder": { "light_levels": [7, 10, 9, 7, 5] },
"cobblemon:firefang_target": { "light_levels": [5, 5, 5, 5, 6, 4, 4, 4, 4, 4] },
"cobblemon:firefang_targetburst": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firefang_targetspark": { "light_levels": [6] },
"cobblemon:firepunch": { "light_levels": [5, 7, 7, 7, 7] },
"cobblemon:firepunch_hand": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firepunch_target": { "light_levels": [6, 5, 5, 5, 4, 4, 3, 3, 3] },
"cobblemon:firepunch_targetburst": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firepunch_targetlinger": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firespin_actor": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firespin_actorburst": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firespin_actorcinder": { "light_levels": [4, 7, 6, 4, 2] },
"cobblemon:firespin_actormiss": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firespin_target": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:firespin_targetburst": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flameburst_actor": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flameburst_actorfling": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flameburst_target": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flameburst_targetcinders": { "light_levels": [5] },
"cobblemon:flamecharge_actor": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flamecharge_actorcharge": { "light_levels": [2, 4, 4, 6, 8, 6, 4, 4, 2] },
"cobblemon:flamecharge_actorcinder": { "light_levels": [4, 7, 6, 4, 2] },
"cobblemon:flamecharge_actorspiral": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flamecharge_target": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flamecharge_targetcover": { "light_levels": [2, 2, 3, 4, 5, 5, 4, 3, 2, 2] },
"cobblemon:flamethrower_actor": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:flamethrower_target": { "light_levels": [5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:flamethrower_target_linger": { "light_levels": [6, 6, 6, 5, 5, 4, 2] },
"cobblemon:flamethrower_targetburst": { "light_levels": [5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:flamethrower_targetcinder": { "light_levels": [7, 10, 9, 7, 5] },
"cobblemon:flamewheel_actor": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:flamewheel_actoraura": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:flamewheel_actorboost": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:flamewheel_actorcinder": { "light_levels": [5, 8, 7, 5, 3] },
"cobblemon:flamewheel_target": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:flamewheel_targetcover": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:friendball/ballsparkle": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:friendball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:friendball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:friendball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0] },
"cobblemon:friendball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:friendball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:friendball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 1, 2, 1, 1, 0] },
"cobblemon:gigadrain_actor": { "light_levels": [1, 3, 4, 6, 7, 6, 4, 3, 1] },
"cobblemon:gigadrain_actoraccent": { "light_levels": [1] },
"cobblemon:gigadrain_actordrainaccent": { "light_levels": [1] },
"cobblemon:gigadrain_actorhit": { "light_levels": [0, 1, 1, 3, 7, 3, 1, 1, 0] },
"cobblemon:gigadrain_actorouter": { "light_levels": [1, 3, 4, 6, 7, 6, 4, 3, 1] },
"cobblemon:gigadrain_actortrail": { "light_levels": [5, 0] },
"cobblemon:greatball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:greatball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:greatball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:greatball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 0] },
"cobblemon:greatball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:greatball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:greatball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:healball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:healball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:healball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:healball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 0] },
"cobblemon:healball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:healball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:healball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:heavyball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:heavyball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:heavyball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:heavyball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0] },
"cobblemon:heavyball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:heavyball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:heavyball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 1, 1, 1, 0, 0, 0] },
"cobblemon:hisuiafterspark": { "light_levels": [7, 5, 3, 3] },
"cobblemon:hisuicapturestar": { "light_levels": [4, 5, 5, 7, 7, 7, 7, 7, 7, 5, 5, 4, 3] },
"cobblemon:hisuisendspark": { "light_levels": [3, 4, 4, 4, 4, 4, 4, 3, 3] },
"cobblemon:hisuispark": { "light_levels": [5, 4, 4, 4, 3, 4, 4, 3, 3] },
"cobblemon:hisuitrail": { "light_levels": [3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3] },
"cobblemon:hooh_tailtrail": { "light_levels": [5, 2, 5, 2, 5, 2, 2, 5, 2, 2, 1] },
"cobblemon:hypnosis_actor": { "light_levels": [3, 3, 3, 3, 3, 4, 4, 4, 4] },
"cobblemon:hypnosis_target": { "light_levels": [4, 5, 6, 5, 5, 4, 3, 2] },
"cobblemon:hypnosis_targetimpact": { "light_levels": [4, 4, 4, 4, 4, 4, 4, 4] },
"cobblemon:icebeam_actor": { "light_levels": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] },
"cobblemon:icebeam_actoremit": { "light_levels": [2, 3, 4, 6] },
"cobblemon:icebeam_actorfrost": { "light_levels": [3, 2, 2] },
"cobblemon:icebeam_actorpilot": { "light_levels": [2] },
"cobblemon:icebeam_actorsparkle": { "light_levels": [5, 4, 3, 2] },
"cobblemon:icebeam_actorspray": { "light_levels": [5, 4, 3, 2] },
"cobblemon:icebeam_target": { "light_levels": [5, 4, 3, 2] },
"cobblemon:icebeam_targetbuild": { "light_levels": [2, 3, 5] },
"cobblemon:icebeam_targetburst": { "light_levels": [4] },
"cobblemon:icebeam_targetfrost": { "light_levels": [4] },
"cobblemon:impact_electric": { "light_levels": [6, 3, 0, 0, 4, 0, 0] },
"cobblemon:impact_fire": { "light_levels": [2, 4, 4, 5, 6, 7, 4] },
"cobblemon:impact_psychic": { "light_levels": [2, 2, 3, 4, 3, 4, 2] },
"cobblemon:kinesis_actorspiral": { "light_levels": [3, 4, 5, 4, 4, 3, 2, 1] },
"cobblemon:lavaplume_actor": { "light_levels": [5, 5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:lavaplume_actorcinders": { "light_levels": [8] },
"cobblemon:lavaplume_target": { "light_levels": [8] },
"cobblemon:lavaplume_targetflame": { "light_levels": [5, 6, 7, 8, 8, 7, 6, 5, 5] },
"cobblemon:leechlife_actor": { "light_levels": [2, 3, 3, 4, 5, 5, 3, 2, 2] },
"cobblemon:leechlife_actortrail": { "light_levels": [2, 3] },
"cobblemon:leechlife_target": { "light_levels": [3, 3, 2, 3, 3, 3] },
"cobblemon:leechlife_targetspark": { "light_levels": [4] },
"cobblemon:leechseed_target": { "light_levels": [2, 3, 3, 4, 5, 4, 3, 3, 1] },
"cobblemon:levelball/ballsparkle": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:levelball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:levelball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:levelball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0] },
"cobblemon:levelball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:levelball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:levelball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 3, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:lightscreen_actorborder1": { "light_levels": [2, 2, 2, 3, 3, 4, 4, 3, 2, 2, 2, 2] },
"cobblemon:lightscreen_actorborder2": { "light_levels": [2, 2, 2, 3, 3, 4, 4, 3, 2, 2, 2, 2] },
"cobblemon:loveball/ballsparkle": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:loveball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:loveball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:loveball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:loveball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:loveball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:loveball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:lucario_aura": { "light_levels": [6, 5, 4, 3, 3, 1] },
"cobblemon:lucario_aura_sparks": { "light_levels": [2, 1, 1, 1, 1, 1] },
"cobblemon:lureball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:lureball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:lureball/battle/ballsparks": { "light_levels": [1, 2, 3, 3, 1, 0] },
"cobblemon:lureball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 2, 3, 3, 2, 2, 2, 1, 0] },
"cobblemon:lureball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:lureball/casual/ballsparks": { "light_levels": [1, 2, 3, 3, 1, 0] },
"cobblemon:lureball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 2, 2, 1, 0] },
"cobblemon:luxuryball/ballsparkle": { "light_levels": [1, 2, 3, 2, 0] },
"cobblemon:luxuryball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:luxuryball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:luxuryball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1, 0] },
"cobblemon:luxuryball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:luxuryball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:luxuryball/casual/sendflash": { "light_levels": [0, 1, 2, 3, 3, 3, 3, 2, 1, 1, 1, 0] },
"cobblemon:magcargo_lava_bubble": { "light_levels": [6, 6, 6, 6, 6, 5, 4, 3, 2] },
"cobblemon:masterball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:masterball/battle/ballsendsparkle": { "light_levels": [2] },
"cobblemon:masterball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:masterball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:masterball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:masterball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0] },
"cobblemon:masterball/casual/sendflash": { "light_levels": [0, 1, 2, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:megadrain_actor": { "light_levels": [2, 4, 4, 4, 5, 4, 4, 4, 2] },
"cobblemon:megadrain_actoraccent": { "light_levels": [4] },
"cobblemon:megadrain_actordrainaccent": { "light_levels": [4] },
"cobblemon:megadrain_actorheal": { "light_levels": [5, 3] },
"cobblemon:megadrain_actorhealbig": { "light_levels": [5, 3] },
"cobblemon:megadrain_actorhealtimer": { "light_levels": [5, 3] },
"cobblemon:megadrain_actorhit": { "light_levels": [2, 4, 4, 4, 5, 6, 4, 4, 2] },
"cobblemon:megadrain_actortrail": { "light_levels": [8, 3] },
"cobblemon:metal_sparks": { "light_levels": [3] },
"cobblemon:moonball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:moonball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:moonball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:moonball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:moonball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:moonball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:moonball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:mysticalfire_actor": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:mysticalfire_actorbig": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:mysticalfire_actorburst": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:mysticalfire_actormotilesparkle": { "light_levels": [6, 5, 4, 3] },
"cobblemon:mysticalfire_actorshot": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:mysticalfire_actorsparkle": { "light_levels": [6, 5, 4, 3] },
"cobblemon:mysticalfire_target": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:mysticalfire_targetsparkle": { "light_levels": [6, 5, 4, 3] },
"cobblemon:nestball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:nestball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:nestball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:nestball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:nestball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:nestball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:nestball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0] },
"cobblemon:netball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:netball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:netball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:netball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0] },
"cobblemon:netball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:netball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:netball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 1, 1, 1, 0] },
"cobblemon:paralysis_actor": { "light_levels": [1, 4, 1, 3, 1, 2, 4, 1, 1, 3] },
"cobblemon:paralysis_actorresidual": { "light_levels": [3, 1, 4, 1] },
"cobblemon:paralysis_passive": { "light_levels": [4, 1, 3, 1, 1, 4, 2, 1, 3, 1] },
"cobblemon:parkball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:parkball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:parkball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:parkball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 0] },
"cobblemon:parkball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:parkball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:parkball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 2, 0] },
"cobblemon:pokeball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:pokeball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:pokeball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:pokeball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:pokeball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:pokeball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:pokeball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:premierball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:premierball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:premierball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:premierball/battle/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:premierball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:premierball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:premierball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:protect_actor1": { "light_levels": [2, 3, 4, 6, 5, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 2, 1, 2, 1, 2] },
"cobblemon:protect_actor2": { "light_levels": [2, 3, 4, 6, 5, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 2, 1, 2, 1, 2] },
"cobblemon:protect_actor3": { "light_levels": [2, 3, 4, 6, 5, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 2, 1, 2, 1, 2] },
"cobblemon:protect_actor4": { "light_levels": [2, 3, 4, 6, 5, 4, 3, 3, 2, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 2, 1, 2, 1, 2] },
"cobblemon:protect_actoreffect": { "light_levels": [4, 2] },
"cobblemon:protect_actorresidual": { "light_levels": [4, 2] },
"cobblemon:protect_block": { "light_levels": [2, 3, 4, 5, 5, 4, 3, 3, 2, 2, 2] },
"cobblemon:protect_blockback": { "light_levels": [3] },
"cobblemon:protect_blockchip": { "light_levels": [3, 4, 6, 4, 3, 3, 1] },
"cobblemon:protect_blockend": { "light_levels": [2, 3, 4, 5, 4, 4, 3, 3, 2, 2, 2] },
"cobblemon:protect-blockchip": { "light_levels": [3, 4, 6, 4, 3, 3, 1] },
"cobblemon:psybeam_actor": { "light_levels": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] },
"cobblemon:psybeam_actoremit": { "light_levels": [3, 4, 6, 8] },
"cobblemon:psybeam_actorenergy": { "light_levels": [3, 3, 5, 7, 4, 6, 3, 2] },
"cobblemon:psybeam_actorpilot": { "light_levels": [2] },
"cobblemon:psybeam_actorspray": { "light_levels": [4, 6, 8, 6, 4, 4, 2] },
"cobblemon:psybeam_charge": { "light_levels": [2, 4, 4, 6, 8] },
"cobblemon:psybeam_target": { "light_levels": [3, 3, 3, 4, 4, 3] },
"cobblemon:psybeam_targetburst": { "light_levels": [4, 6, 8, 6, 4, 4, 2] },
"cobblemon:psybeam_targetsparkle": { "light_levels": [4, 5, 6, 5, 5, 4, 3, 2] },
"cobblemon:psychic_actor": { "light_levels": [1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 5, 4, 4, 4, 4, 5, 3] },
"cobblemon:psychic_actordots": { "light_levels": [1, 3, 3, 5, 7, 5, 3, 3, 1] },
"cobblemon:psychic_impact": { "light_levels": [2, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3] },
"cobblemon:psychic_impactdots": { "light_levels": [2, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6, 3] },
"cobblemon:psychic_target": { "light_levels": [3, 4, 5, 4, 4, 3, 2, 1] },
"cobblemon:quickball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:quickball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:quickball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:quickball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 3, 3, 3, 3, 2, 0] },
"cobblemon:quickball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:quickball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0, 0] },
"cobblemon:quickball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 4, 3, 3, 2, 0] },
"cobblemon:reflect_actorborder1": { "light_levels": [2, 2, 2, 3, 3, 4, 4, 3, 2, 2, 2, 2] },
"cobblemon:reflect_actorborder2": { "light_levels": [2, 2, 2, 3, 3, 4, 4, 3, 2, 2, 2, 2] },
"cobblemon:repeatball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:repeatball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 0] },
"cobblemon:repeatball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:repeatball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0] },
"cobblemon:repeatball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 0] },
"cobblemon:repeatball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:repeatball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 0] },
"cobblemon:roseateball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:roseateball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:roseateball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:roseateball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:roseateball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:roseateball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:roseateball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:safariball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:safariball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:safariball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:safariball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:safariball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:safariball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:safariball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 3, 2, 3, 2, 1, 0] },
"cobblemon:seedbomb_target": { "light_levels": [1, 1, 2, 3, 4, 4, 3, 2, 1, 1] },
"cobblemon:seedbomb_targetorbs": { "light_levels": [1, 3, 3, 5, 6, 5, 3, 3, 1] },
"cobblemon:shine_sparkle1": { "light_levels": [2] },
"cobblemon:shine_sparkle2": { "light_levels": [2] },
"cobblemon:shiny_glimmer": { "light_levels": [1, 1] },
"cobblemon:shiny_magcargo_lava_bubble": { "light_levels": [6, 6, 6, 6, 6, 5, 4, 3, 2] },
"cobblemon:shiny_ring": { "light_levels": [2] },
"cobblemon:shiny_ring2": { "light_levels": [2] },
"cobblemon:shiny_ring3": { "light_levels": [2] },
"cobblemon:shiny_ring4": { "light_levels": [2] },
"cobblemon:shiny_ring5": { "light_levels": [2] },
"cobblemon:shiny_ring6": { "light_levels": [2] },
"cobblemon:shiny_ring7": { "light_levels": [2] },
"cobblemon:shiny_ring8": { "light_levels": [2] },
"cobblemon:shiny_slugma_lava_bubble": { "light_levels": [5, 5, 5, 5, 5, 4, 3, 2, 1] },
"cobblemon:shiny_sparkle_ambient": { "light_levels": [1, 1, 1, 1, 1, 1, 1] },
"cobblemon:shiny_sparkle_ambient_wild": { "light_levels": [2] },
"cobblemon:slateball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:slateball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:slateball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:slateball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:slateball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:slateball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:slateball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:slugma_lava_bubble": { "light_levels": [5, 5, 5, 5, 5, 4, 3, 2, 1] },
"cobblemon:sportball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:sportball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:sportball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:sportball/battle/sendflash": { "light_levels": [0, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0] },
"cobblemon:sportball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:sportball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:sportball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 2, 1, 2, 1, 0] },
"cobblemon:swordsdance_actor": { "light_levels": [4, 4, 4, 4, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4] },
"cobblemon:swordsdance_sword2": { "light_levels": [4, 4, 4, 4, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4] },
"cobblemon:swordsdance_sword3": { "light_levels": [4, 4, 4, 4, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4] },
"cobblemon:swordsdance_sword4": { "light_levels": [4, 4, 4, 4, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4] },
"cobblemon:synthesis_accretion": { "light_levels": [3] },
"cobblemon:synthesis_sun": { "light_levels": [6] },
"cobblemon:synthesis_sunlight": { "light_levels": [5] },
"cobblemon:tailflame_large": { "light_levels": [6, 6, 6, 6] },
"cobblemon:tailflame_medium": { "light_levels": [5, 5, 5, 5] },
"cobblemon:tailflame_small": { "light_levels": [4, 4, 4, 4] },
"cobblemon:thunder_actor": { "light_levels": [1, 15, 1, 12, 15] },
"cobblemon:thunder_actorring": { "light_levels": [2, 13, 1, 15, 12, 15, 1, 14, 2, 13] },
"cobblemon:thunder_actorsparks": { "light_levels": [4] },
"cobblemon:thunder_target": { "light_levels": [0, 12, 0, 1, 0, 13, 13] },
"cobblemon:thunder_targetboom": { "light_levels": [0, 2, 2, 2, 13, 12, 14, 1, 15] },
"cobblemon:thunder_targetcloudspark": { "light_levels": [10] },
"cobblemon:thunder_targetfire": { "light_levels": [15, 0, 0, 13, 12, 12, 13, 15, 1, 13] },
"cobblemon:thunder_targetfloorsparks": { "light_levels": [4] },
"cobblemon:thunder_targetglowcloud": { "light_levels": [14, 1, 14, 13, 0, 2, 0, 12] },
"cobblemon:thunder_targetsparks": { "light_levels": [4] },
"cobblemon:thunderbolt_actor": { "light_levels": [0, 0, 1, 12, 2, 2, 0, 14, 1, 2, 11] },
"cobblemon:thunderbolt_actorbolts": { "light_levels": [2, 0, 1, 15, 1, 14, 2, 0, 11, 2, 14] },
"cobblemon:thunderbolt_actorsparks": { "light_levels": [2] },
"cobblemon:thunderbolt_target": { "light_levels": [0, 13, 4, 2, 13, 11] },
"cobblemon:thunderbolt_targetimpact": { "light_levels": [14, 1, 2, 5, 0, 13, 10] },
"cobblemon:thunderbolt_targetsparks": { "light_levels": [2] },
"cobblemon:thunderfang_target": { "light_levels": [7, 0, 0, 0, 3, 7, 7, 0, 1, 7] },
"cobblemon:thunderfang_targetimpact": { "light_levels": [7, 0, 0, 8, 0, 8, 7] },
"cobblemon:thunderfang_targetspark": { "light_levels": [0] },
"cobblemon:thunderpunch": { "light_levels": [7, 0, 0, 7, 0] },
"cobblemon:thunderpunch_hand": { "light_levels": [0] },
"cobblemon:thunderpunch_target": { "light_levels": [0, 7, 8, 5, 0, 0, 7, 0, 7] },
"cobblemon:thunderpunch_targetimpact": { "light_levels": [8, 0, 0, 7, 0, 7, 7] },
"cobblemon:thunderpunch_targetlinger": { "light_levels": [0, 7, 0, 7, 6, 0] },
"cobblemon:thunderpunch_targetsparks": { "light_levels": [0] },
"cobblemon:thundershock_actor": { "light_levels": [1, 1, 5, 3, 6, 1, 1, 6, 7, 1, 1] },
"cobblemon:thundershock_actorbolts": { "light_levels": [1, 7, 5, 1, 1, 1, 5, 6, 1, 5, 1] },
"cobblemon:thundershock_actorsparks": { "light_levels": [2] },
"cobblemon:thundershock_target": { "light_levels": [2] },
"cobblemon:thundershock_targetbolts": { "light_levels": [5, 1, 1, 5, 5, 1] },
"cobblemon:thundershock_targetimpact": { "light_levels": [1, 1, 5, 1, 6, 1, 5] },
"cobblemon:thunderwave_actor": { "light_levels": [5, 6, 3, 0, 0, 0] },
"cobblemon:thunderwave_impact1": { "light_levels": [2, 3, 0, 0, 5, 2, 4, 3] },
"cobblemon:thunderwave_impact2": { "light_levels": [5, 0, 0, 5, 0, 0, 2, 0] },
"cobblemon:thunderwave_impact3": { "light_levels": [2, 5, 0, 4, 0, 3] },
"cobblemon:thunderwave_launch": { "light_levels": [0, 2, 0, 0, 4, 5] },
"cobblemon:thunderwave_launch1": { "light_levels": [0, 0, 3, 2, 0, 3, 3, 0] },
"cobblemon:thunderwave_launch2": { "light_levels": [0, 2, 2, 0, 0, 2, 0, 0] },
"cobblemon:thunderwave_sparking": { "light_levels": [0, 3, 0, 4] },
"cobblemon:timerball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:timerball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:timerball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:timerball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0] },
"cobblemon:timerball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:timerball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:timerball/casual/sendflash": { "light_levels": [0, 1, 2, 2, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:ultraball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:ultraball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ultraball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ultraball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:ultraball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:ultraball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:ultraball/casual/sendflash": { "light_levels": [0, 1, 3, 3, 4, 2, 2, 2, 2, 1, 1, 1, 0] },
"cobblemon:verdantball/ballsparkle": { "light_levels": [4, 1, 1, 0] },
"cobblemon:verdantball/battle/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:verdantball/battle/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:verdantball/battle/sendflash": { "light_levels": [0, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 1, 0] },
"cobblemon:verdantball/casual/ballsendsparkle": { "light_levels": [4, 1, 4, 1, 4, 1, 1, 4, 1, 1, 0] },
"cobblemon:verdantball/casual/ballsparks": { "light_levels": [1, 2, 3, 4, 4, 4, 3, 2, 1, 0] },
"cobblemon:verdantball/casual/sendflash": { "light_levels": [0, 1, 3, 4, 4, 2, 2, 2, 2, 1, 1, 0] },
"cobblemon:wild_shiny_ring": { "light_levels": [1] },
"cobblemon:willowisp_actor": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:willowisp_actorappear": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:willowisp_actorburst": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:willowisp_actorcinder": { "light_levels": [4] },
"cobblemon:willowisp_actorproj": { "light_levels": [3, 3, 4, 5, 6, 6, 5, 4, 3, 3] },
"cobblemon:willowisp_actorwisp": { "light_levels": [5, 5, 5, 3, 3, 5, 6, 6, 5, 5, 5] },
"cobblemon:willowisp_target": { "light_levels": [3, 4, 5, 6, 6, 5, 4, 3, 3] }
}
@@ -2539,6 +2539,10 @@
"cobblemon.config.ui.summary_pokemon_follow_cursor.tooltip": "Whether or not Pokémon should follow the mouse cursor with their heads in the Summary screen.",
"cobblemon.config.ui.animate_battle_tiles": "Animate Battle Tiles",
"cobblemon.config.ui.animate_battle_tiles.tooltip": "Whether or not the battle portraits of Pokemon should be animated.",
"cobblemon.config.ui.shader_particle_bypass": "Shader Particle Bypass",
"cobblemon.config.ui.shader_particle_bypass.tooltip": "When enabled, Cobblemon particles are redrawn after shaderpack composition so Iris and OptiFine shaderpacks do not alter their appearance.",
"cobblemon.config.ui.max_lit_particles_per_emitter": "Max Lit Particles Per Emitter",
"cobblemon.config.ui.max_lit_particles_per_emitter.tooltip": "With LambDynamicLights installed, the most particles from a single effect that may emit real dynamic light at once. Higher looks better but costs more performance; 0 disables particle dynamic lights.",
"cobblemon.config.ui.party_portrait_animations": "Party Portrait Animations",
"cobblemon.config.ui.party_portrait_animations.never_animate": "Never Animate",
"cobblemon.config.ui.party_portrait_animations.never_animate.tooltip": "No portraits animate",
+94
View File
@@ -8,6 +8,8 @@
*
*/
import java.net.URI
configurations.all {
resolutionStrategy {
force(libs.fabric.loader)
@@ -24,6 +26,17 @@ architectury {
fabric()
}
// DEV ONLY: turn on PhotonRT's debug logging in the runClient so we can see its per-tick dynamic-light
// count ("dynamic lights this tick: N | raytracer active: T/F | max lights: 1000") and confirm Cobblemon's
// move-particle lights are reaching PhotonRT each gather (vs. being dropped). Remove this block to silence it.
loom {
runs {
named("client") {
vmArg("-Dphotonrt.debug=true")
}
}
}
val generatedResources = file("src/generated/resources")
sourceSets.main {
@@ -38,6 +51,7 @@ repositories {
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven(url = "https://api.modrinth.com/maven")
maven(url = "https://maven.terraformersmc.com/")
maven(url = "https://maven.gegy.dev") // LambDynamicLights + spruceui/pridelib (dev runtime)
}
dependencies {
@@ -48,6 +62,35 @@ dependencies {
isTransitive = false
}
modLocalRuntime(libs.fabric.debugutils)
// Shader-compat testing (DEV RUNTIME ONLY — never shipped, never a real dependency).
// Lets `runClient` load Iris + Sodium so shaderpacks actually run, which is required to
// exercise ParticleMaterials' depthMask branch (see ShaderCompat). isTransitive = false so
// these don't drag in a second copy of Fabric API / FLK (already provided above). If you bump
// these, keep the Iris<->Sodium pairing valid (Iris 1.8.x needs Sodium 0.6.x for MC 1.21.1).
modLocalRuntime("maven.modrinth:iris:1.8.8+1.21.1-fabric") { isTransitive = false }
modLocalRuntime("maven.modrinth:sodium:mc1.21.1-0.6.13-fabric") { isTransitive = false }
// Iris bundles these as jar-in-jar in production, but loom doesn't add a mod's nested *libraries*
// to the dev classpath, so the dev client crashes with NoClassDefFoundError (e.g. org.anarres.cpp
// / glsl-transformer) when Iris loads a shaderpack. Supply them as plain dev-runtime libs.
// Versions are taken verbatim from iris-1.8.8's META-INF/jars — keep them in lockstep if Iris is
// bumped. localRuntime = dev runtime only, not remapped, not exported to the published jar.
localRuntime("org.anarres:jcpp:1.4.14")
localRuntime("io.github.douira:glsl-transformer:2.0.1")
localRuntime("org.antlr:antlr4-runtime:4.13.1")
// Dynamic lighting (DEV RUNTIME ONLY). Cobblemon compiles against the LambDynamicLights API
// (modCompileOnly, mojmap; see common/build.gradle.kts); this provides the running mod in dev so
// runClient actually exercises the dynamic lights. Pinned to 4.1.6 to match that API and fabric
// loader 0.17.2 (4.8.9 needs loader 0.18.4 or newer). Loom does not surface a mod's bundled
// dependency mods to the dev run, so its four are listed explicitly (from gegy + Maven Central).
// Bump all five together if LambDynamicLights is updated.
modLocalRuntime("maven.modrinth:lambdynamiclights:4.1.6+1.21.1")
modLocalRuntime("dev.lambdaurora.lambdynamiclights:lambdynamiclights-api:4.1.6+1.21.1")
modLocalRuntime("dev.lambdaurora:spruceui:6.2.1+1.21")
modLocalRuntime("dev.yumi.mc.core:yumi-mc-foundation:1.0.0-alpha.7+1.21.1")
modLocalRuntime("io.github.queerbric:pridelib:1.2.1+1.21")
modImplementation(libs.fabric.loader)
modApi(libs.fabric.api)
modApi(libs.bundles.fabric)
@@ -110,6 +153,57 @@ publishing {
}
tasks {
// Downloads the Complementary Unbound shaderpack into the dev client's shaderpacks/ folder and
// enables Iris by default, so `runClient` comes up shader-ready with zero manual setup. Both
// writes are idempotent: existing files are left untouched, so your in-game shader choice and
// any pack you swap in are preserved across runs. Run dir is "runClient" (see platform-conventions).
val setupShaderTesting by registering {
group = "cobblemon"
description = "Provisions the test shaderpacks + enables Iris for the dev runClient."
// The four Iris packs we want particles to look correct under. Swap between them in-game
// (Video Settings -> Shader Packs) to A/B the premultiplied-alpha fix in ParticleMaterials.
// <filename> to <direct Modrinth CDN url>.
//
// VERSIONS ARE PINNED FOR MC 1.21.1. The newest releases of these packs reference the
// BIOME_PALE_GARDEN uniform (Pale Garden is a 1.21.4 biome), which Iris cannot resolve on
// 1.21.1 -> hard crash building the render pipeline ("Unknown variable: BIOME_PALE_GARDEN").
// Each pin below was confirmed Pale-Garden-free by grepping the archive. If this project ever
// targets >= 1.21.4, bump these to the latest versions.
val packs = mapOf(
"ComplementaryUnbound_r5.3.zip" to "https://cdn.modrinth.com/data/R6NEzAwj/versions/CVu3AGAd/ComplementaryUnbound_r5.3.zip",
"ComplementaryReimagined_r5.3.zip" to "https://cdn.modrinth.com/data/HVnmMxH1/versions/71Hn3myZ/ComplementaryReimagined_r5.3.zip",
"Bliss_v2.1.2_(Chocapic13_Shaders_edit).zip" to "https://cdn.modrinth.com/data/ZvMtQlho/versions/kC2Y8q1P/Bliss_v2.1.2_%28Chocapic13_Shaders_edit%29.zip",
"photon_v1.1.zip" to "https://cdn.modrinth.com/data/lLqFfGNs/versions/6FdrBtKp/photon_v1.1.zip"
)
val shaderpacksDir = layout.projectDirectory.dir("runClient/shaderpacks")
val irisProps = layout.projectDirectory.file("runClient/config/iris.properties")
// Default the in-game selection to Complementary Unbound (the originally-reported pack); idempotent.
val defaultPack = "ComplementaryUnbound_r5.3.zip"
outputs.dir(shaderpacksDir)
doLast {
packs.forEach { (filename, url) ->
val zip = shaderpacksDir.file(filename).asFile
if (!zip.exists()) {
zip.parentFile.mkdirs()
logger.lifecycle("Downloading shaderpack -> ${zip.relativeTo(rootProject.projectDir)}")
URI(url).toURL().openStream().use { input ->
zip.outputStream().use { output -> input.copyTo(output) }
}
}
}
val props = irisProps.asFile
if (!props.exists()) {
props.parentFile.mkdirs()
props.writeText("enableShaders=true\nshaderPack=$defaultPack\n")
logger.lifecycle("Wrote ${props.relativeTo(rootProject.projectDir)} (Iris auto-enabled with $defaultPack).")
}
}
}
named("runClient") {
dependsOn(setupShaderTesting)
}
// The AW file is needed in :fabric project resources when the game is run.
val copyAccessWidener by registering(Copy::class) {
from(loom.accessWidenerPath)
@@ -18,6 +18,7 @@ import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinds
import com.cobblemon.mod.common.client.pokedex.PokedexType
import com.cobblemon.mod.common.client.render.atlas.CobblemonAtlases
import com.cobblemon.mod.common.client.render.item.CobblemonModelPredicateRegistry
import com.cobblemon.mod.common.compat.photonic.PhotonRTIntegration
import com.cobblemon.mod.common.item.PokedexItem
import com.cobblemon.mod.common.particle.CobblemonParticles
import com.cobblemon.mod.common.particle.SnowstormParticleType
@@ -35,8 +36,10 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.function.Supplier
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
@@ -47,6 +50,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry
import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener
import net.fabricmc.fabric.api.resource.ResourceManagerHelper
@@ -79,6 +83,15 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
override fun onInitializeClient() {
registerParticleFactory(CobblemonParticles.SNOWSTORM_PARTICLE_TYPE, SnowstormParticleType::Factory)
CobblemonClient.initialize(this)
// When the PhotonRT raytracing mod is present, register Cobblemon's block lights + moving
// move-particle lights with it after client startup. Gated here so PhotonRTIntegration (which
// references PhotonRT API types) is only classloaded when photonrt is actually installed.
ClientLifecycleEvents.CLIENT_STARTED.register {
if (FabricLoader.getInstance().isModLoaded("photonrt")) {
PhotonRTIntegration.register()
}
}
ModelLoadingPlugin.register {
PokeBalls.all().forEach { pokeBall -> it.addModels(pokeBall.model3d) }
PokedexType.entries.toList().forEach { pokedex ->
@@ -158,22 +171,29 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
ClientPlayConnectionEvents.DISCONNECT.register { _, client -> client.player?.let { PlatformEvents.CLIENT_PLAYER_LOGOUT.post(ClientPlayerEvent.Logout(it)) } }
ItemTooltipCallback.EVENT.register { stack, context, type, lines -> PlatformEvents.CLIENT_ITEM_TOOLTIP.post(ItemTooltipEvent(stack, context, type, lines)) }
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> PlatformEvents.RENDER.post(
RenderEvent(
stage = RenderEvent.Stage.TRANSLUCENT,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> postRenderStage(RenderEvent.Stage.TRANSLUCENT, context) }
// LAST captures the camera transform while it is still applied; POST_LEVEL fires after the level
// (and any shaderpack composite) finishes, the only point a draw is NOT intercepted by Iris/OptiFine.
WorldRenderEvents.LAST.register { context -> postRenderStage(RenderEvent.Stage.LAST, context) }
WorldRenderEvents.END.register { context -> postRenderStage(RenderEvent.Stage.POST_LEVEL, context) }
CobblemonModelPredicateRegistry.registerPredicates()
}
private fun postRenderStage(stage: RenderEvent.Stage, context: WorldRenderContext) {
PlatformEvents.RENDER.post(
RenderEvent(
stage = stage,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
override fun registerLayer(modelLayer: ModelLayerLocation, supplier: Supplier<LayerDefinition>) {
EntityModelLayerRegistry.registerModelLayer(modelLayer) { supplier.get() }
}
@@ -201,4 +221,4 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
override fun <T : Entity> registerEntityRenderer(type: EntityType<out T>, factory: EntityRendererProvider<T>) {
EntityRendererRegistry.register(type, factory)
}
}
}
@@ -74,6 +74,8 @@ object NeoForgeClientPlatformEventHandler {
fun onRenderLevelStageEvent(event: RenderLevelStageEvent) {
val stage = when (event.stage) {
RenderLevelStageEvent.Stage.AFTER_PARTICLES -> RenderEvent.Stage.TRANSLUCENT
RenderLevelStageEvent.Stage.AFTER_WEATHER -> RenderEvent.Stage.LAST
RenderLevelStageEvent.Stage.AFTER_LEVEL -> RenderEvent.Stage.POST_LEVEL
else -> return
}
@@ -89,4 +91,4 @@ object NeoForgeClientPlatformEventHandler {
)
)
}
}
}