render cat/wolf collars, charged creeper and wither armor energy swirl (additive), fox/dolphin mouth items

This commit is contained in:
MineTec committed 2026-11-04 14:45:57 +01:00
1 parent fbea9dfe08
commit c66ebaa942
11 files changed
+231 -20

No files matched your search

@@ -13,10 +13,16 @@ public final class EntityCube {
public final Face[] faces; // by Direction.ordinal()
public final Affine toWorld;
public final Affine toLocal; // inverse
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld) {
this(from, to, faces, toWorld, false);
}
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive) {
this.additive = additive;
this.from = from;
this.to = to;
this.faces = faces;
@@ -103,7 +103,7 @@ public final class EntityIntersector {
Vector normal = new Vector(n[0], n[1], n[2]).normalize();
// A far/back face is seen from inside: shade it as facing the viewer.
if (normal.getX() * dx + normal.getY() * dy + normal.getZ() * dz > 0) normal.multiply(-1);
return new FaceHit(t, world, normal, color, -1);
return new FaceHit(t, world, normal, color, -1, cube.additive);
}
private static double frac(double v, double lo, double hi) {
@@ -35,7 +35,10 @@ public record EntityState(
boolean passenger, // riding something (vanilla sitting pose)
ItemSpec rightHand, // item in the RIGHT hand (main or off hand, resolved by handedness); null = empty
ItemSpec leftHand, // item in the LEFT hand; null = empty
ItemSpec headItem // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none
ItemSpec headItem, // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none
int collar, // ARGB dye of a tamed cat's/wolf's collar; 0 = no collar (untamed)
boolean powered, // energy-swirl layer: charged creeper, wither below half health
ItemSpec mouthItem // item carried in the mouth (fox/dolphin main hand); null = none
) {
/** Worn armor (4 slots) of a humanoid wearer; any field may be null. */
@@ -110,6 +110,10 @@ public final class CemBaker implements EntityBaker<EntityState> {
String m = "entity/horse/horse_markings_" + s.markings();
overlayIfPresent(tex, ch != null && ch.babyModel ? m + "_baby" : m);
}
if (s.collar() != 0 && tex != null && layerModels(base).stream().noneMatch(n -> n.contains("CollarLayer."))) {
// Collar drawn on the body model itself (WolfCollarLayer: parent model, same UV) -> composite it.
tex = withCollar(tex, base, ch != null && ch.babyModel, s.collar());
}
if (s.typeKey().equals("tropical_fish") && tex != null && s.tint() != 0) {
tex = TextureOps.deepCopy(tex);
TextureOps.tint(tex, s.tint()); // white base body tinted with the body colour
@@ -160,15 +164,16 @@ public final class CemBaker implements EntityBaker<EntityState> {
: List.of();
List<CemGeometry.Baked> baked = new ArrayList<>();
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
if (!invisible) {
baked.addAll(body);
if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked);
if (vanilla) addVanillaLayers(s, ch, pose, show, tex, baked, glow);
}
// Humanoid armor / elytra: drawn even when invisible (vanilla "floating armor").
if (vanilla && s.equipment() != null) addArmorLayers(s, ch, pose, baked);
List<EntityCube> itemCubes = new ArrayList<>();
if (vanilla && items != null) addItems(s, frames, isIllager(info), itemCubes);
if (baked.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex);
if (baked.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex);
// Legacy CEM models: snap the lowest point to the ground (their rest geometry has no reliable origin).
if (place == null) {
@@ -182,6 +187,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
List<EntityCube> cubes = new ArrayList<>(baked.size());
for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world())));
for (CemGeometry.Baked b : glow) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), true));
}
for (EntityCube c : itemCubes) cubes.add(new EntityCube(c.from, c.to, c.faces, place.mul(c.toWorld)));
return RenderedEntity.of(cubes);
}
@@ -260,6 +268,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
items.bake(item, "ground", false, x, out);
}
}
addMouthItem(s, frames, out);
ItemSpec head = s.headItem();
Affine h = frames.get("head");
if (head != null && h != null) {
@@ -280,6 +289,27 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
}
/**
* Item carried in the mouth, rendered in the "ground" context like vanilla (HoldingEntityRenderState).
* Fox (FoxHeldItemLayer, awake): head pivot · [baby: S(0.75)] · head rotation · T(0.06, 0.27 [baby 0.26],
* -0.5) · rotX(90) — the head frame carries exactly that pivot + rotation. Dolphin (DolphinCarryingItemLayer)
* at the model root · T(0, 1, -1) (its xRot-dependent shift is 0: PixelPics dolphins aren't pitched).
*/
private void addMouthItem(EntityState s, Map<String, Affine> frames, List<EntityCube> out) {
ItemSpec item = s.mouthItem();
if (item == null) return;
boolean baby = s.baby();
Affine x = null;
if (s.typeKey().equals("fox") && frames.containsKey("head")) {
x = frames.get("head").mul(CemGeometry.JAVA_LOCAL);
if (baby) x = x.mul(Affine.scale(0.75));
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
} else if (s.typeKey().equals("dolphin") && frames.containsKey("root")) {
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(Affine.translation(0, 1, -1));
}
if (x != null) items.bake(item, "ground", false, x, out);
}
private static boolean isIllager(CemModelLoader.EntityInfo info) {
return info != null && info.poses().keySet().stream().anyMatch(k -> k.startsWith("arms_"));
}
@@ -373,7 +403,13 @@ public final class CemBaker implements EntityBaker<EntityState> {
// --- vanilla render layers -----------------------------------------------------------------------
private void addVanillaLayers(
EntityState s, Choice ch, Pose pose, Set<String> show, int[][] bodyTex, List<CemGeometry.Baked> baked) {
EntityState s,
Choice ch,
Pose pose,
Set<String> show,
int[][] bodyTex,
List<CemGeometry.Baked> baked,
List<CemGeometry.Baked> glow) {
String base = ch.base;
boolean baby = ch.babyModel;
for (String n : layerModels(base)) {
@@ -392,6 +428,21 @@ public final class CemBaker implements EntityBaker<EntityState> {
baked.addAll(pose.bake(lm, t, show));
}
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
case "CatCollarLayer" -> {
// Own collar model (inflated by 0.01), collar texture tinted with the collar's dye colour.
if (s.collar() == 0 || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t == null) continue;
t = TextureOps.deepCopy(t);
TextureOps.tint(t, s.collar());
baked.addAll(pose.bake(lm, t, show));
}
case "CreeperPowerLayer", "WitherArmorLayer" -> {
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
if (!s.powered() || lm.texture() == null) continue;
int[][] t = energySwirl(lm.texture(), layerClass.equals("WitherArmorLayer"));
if (t != null) glow.addAll(pose.bake(lm, t, show));
}
case "SlimeOuterLayer" -> {
// Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer).
int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex;
@@ -475,6 +526,49 @@ public final class CemBaker implements EntityBaker<EntityState> {
return out;
}
// --- energy swirl & collars ----------------------------------------------------------------------
// A photo has no time: the swirl's scrolling texture is frozen at this age (ticks). Chosen so the sparse
// wither_armor pattern covers every wither part (at e.g. 42 the side heads land on empty rows).
private static final float SWIRL_AGE = 59f;
/**
* Energy-swirl texture (vanilla EnergySwirlLayer / RenderTypes.energySwirl): the texture matrix shifts the UV
* by ({@code xOffset(age) % 1}, {@code age·0.01 % 1}) on a repeating texture — baked here as a wrapped copy
* of the texture rolled by that offset — and the vertex colour 0xFF808080 halves the RGB. The result is
* drawn additively (BlendFunction.ADDITIVE), so alpha only cuts out (shader ALPHA_CUTOUT 0.1).
* xOffset: creeper {@code age·0.01}, wither {@code cos(age·0.02)·3}.
*/
private int[][] energySwirl(String path, boolean wither) {
int[][] src = texture(path);
if (src == null) return null;
double u = wither ? Math.cos(SWIRL_AGE * 0.02) * 3 : SWIRL_AGE * 0.01;
double v = SWIRL_AGE * 0.01;
int h = src.length, w = src[0].length;
int du = Math.floorMod((int) Math.round((u % 1) * w), w);
int dv = Math.floorMod((int) Math.round((v % 1) * h), h);
int[][] out = new int[h][w];
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int p = src[(y + dv) % h][(x + du) % w];
out[y][x] = (p & 0xFF000000) | ((p >>> 1) & 0x7F7F7F);
}
}
return out;
}
/** Body texture with the dye-tinted {@code entity/<base>/<base>_collar[_baby]} composited over it. */
private int[][] withCollar(int[][] tex, String base, boolean baby, int dye) {
String path = "entity/" + base + "/" + base + "_collar" + (baby ? "_baby" : "");
int[][] collar = texture(path);
if (collar == null || collar.length != tex.length || collar[0].length != tex[0].length) return tex;
int[][] tinted = TextureOps.deepCopy(collar);
TextureOps.tint(tinted, dye);
int[][] out = TextureOps.deepCopy(tex);
TextureOps.overlay(out, tinted);
return out;
}
// --- texture resolution --------------------------------------------------------------------------
private int[][] texture(String path) {
@@ -501,6 +595,12 @@ public final class CemBaker implements EntityBaker<EntityState> {
Math.min(adult.size(), EntityModels.specificCandidateCount(s.typeKey(), s.variant())),
info.texture());
}
if (s.collar() != 0) {
// Tamed: vanilla switches to the "_tame" texture where the variant has one (wolves).
List<String> tame = new ArrayList<>();
for (String a : adult) tame.add(a + "_tame");
adult.addAll(0, tame);
}
List<String> order = new ArrayList<>();
if (ch != null && ch.babyModel) {
for (String a : adult) order.add(a + "_baby");
@@ -81,7 +81,7 @@ public final class ElementIntersector {
Vector world = new Vector(bx + ox + dx * c.t, by + oy + dy * c.t, bz + oz + dz * c.t);
Vector normal = new Vector(c.normal[0], c.normal[1], c.normal[2]);
return new FaceHit(c.t, world, normal, color, face.tintIndex);
return new FaceHit(c.t, world, normal, color, face.tintIndex, false);
}
return null;
}
@@ -4,6 +4,7 @@ import org.bukkit.util.Vector;
/**
* The result of intersecting a ray with a block's geometry: the world-space hit point and normal,
* the sampled ARGB color (before shading/tinting) and the face's tint index ({@code -1} = none).
* the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none) and whether the
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind).
*/
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex) {}
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex, boolean additive) {}
@@ -42,6 +42,8 @@ public final class SnapshotRaytracer {
private static final int MAX_TRANSLUCENT_LAYERS = 6;
private static final double MIN_TRANSMITTANCE = 0.03;
private static final double LAYER_EPS = 1e-5;
// layerAlpha marker of an additive (emissive) layer: its colour is added to what lies behind.
private static final double ADDITIVE = -1;
private final BlockModelRegistry registry;
private final BiomeTintProvider tintProvider;
@@ -78,7 +80,8 @@ public final class SnapshotRaytracer {
BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth);
boolean entities = scene != null && !scene.isEmpty();
// Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1, ray t).
// Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1 or ADDITIVE,
// ray t).
int[] layerColor = null;
double[] layerAlpha = null, layerT = null;
int layers = 0;
@@ -97,8 +100,11 @@ public final class SnapshotRaytracer {
FaceHit eh = entities ? scene.nearestHit(ox, oy, oz, dx, dy, dz, minT, limit) : null;
int color;
double t;
boolean additive = false;
if (eh != null) {
color = ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
// Emissive additive surfaces (energy swirl) are unshaded: vanilla NO_CARDINAL_LIGHTING + EMISSIVE.
additive = eh.additive();
color = additive ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
t = eh.t();
} else if (pending != null) {
color = pending.color;
@@ -109,7 +115,14 @@ public final class SnapshotRaytracer {
}
int alpha = ColorUtil.alpha(color);
double a = alpha / 255.0;
if (alpha >= OPAQUE_ALPHA
if (additive) {
// Additive light never hides what lies behind it: it neither ends the ray nor dims it. Beyond the
// layer budget it is dropped.
if (layers == MAX_TRANSLUCENT_LAYERS) {
minT = t + LAYER_EPS;
continue;
}
} else if (alpha >= OPAQUE_ALPHA
|| layers == MAX_TRANSLUCENT_LAYERS
|| transmittance * (1 - a) < MIN_TRANSMITTANCE) {
// Opaque, or what lies behind would contribute next to nothing: this surface ends the ray.
@@ -123,10 +136,10 @@ public final class SnapshotRaytracer {
layerT = new double[MAX_TRANSLUCENT_LAYERS];
}
layerColor[layers] = color;
layerAlpha[layers] = a;
layerAlpha[layers] = additive ? ADDITIVE : a;
layerT[layers] = t;
layers++;
transmittance *= 1 - a;
if (!additive) transmittance *= 1 - a;
minT = t + LAYER_EPS;
}
@@ -138,7 +151,12 @@ public final class SnapshotRaytracer {
color = blocks.applyLiquid(color, finalT, skyColor);
liquidPending = false;
}
color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
if (layerAlpha[i] == ADDITIVE) {
// Added light fades with distance like the surface it would otherwise be.
color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - fogFactor(layerT[i])));
} else {
color = ColorUtil.mix(fog(layerColor[i], layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
}
}
if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor);
return color & 0xFFFFFF;
@@ -19,6 +19,8 @@ import java.util.function.DoubleSupplier;
import org.bukkit.Keyed;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.Ageable;
@@ -28,6 +30,7 @@ import org.bukkit.entity.Cat;
import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.Chicken;
import org.bukkit.entity.Cow;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Fox;
import org.bukkit.entity.Frog;
@@ -51,6 +54,7 @@ import org.bukkit.entity.TraderLlama;
import org.bukkit.entity.Trident;
import org.bukkit.entity.TropicalFish;
import org.bukkit.entity.Villager;
import org.bukkit.entity.Wither;
import org.bukkit.entity.Wolf;
import org.bukkit.entity.ZombieVillager;
import org.bukkit.inventory.EntityEquipment;
@@ -114,6 +118,9 @@ public final class EntitySnapshotBuilder {
"piglin",
"piglin_brute");
// Mobs whose main-hand item is drawn in the mouth (FoxHeldItemLayer, DolphinCarryingItemLayer).
private static final Set<String> MOUTH_CARRIERS = Set.of("fox", "dolphin");
/** {@code nearby}: {@link FrustumBounds#nearbyEntities} of this photo. */
public static List<EntityState> build(Collection<Entity> nearby, UUID shooter) {
List<EntityState> states = new ArrayList<>();
@@ -169,6 +176,8 @@ public final class EntitySnapshotBuilder {
String bodyEquip = null;
int bodyDye = 0;
int tint2 = 0;
int collar = 0;
boolean powered = false;
try {
// Slime & magma cube (MagmaCube extends Slime) scale their model by size (1/2/4).
if (e instanceof Slime sl) sizeScale = sl.getSize();
@@ -187,8 +196,16 @@ public final class EntitySnapshotBuilder {
default -> "big";};
} else if (e instanceof Cat c) {
variant = keyOf(c.getCatType());
if (c.isTamed()) collar = ColorUtil.dyeArgb(c.getCollarColor(), 0);
} else if (e instanceof Wolf w) {
variant = keyOf(w.getVariant());
if (w.isTamed()) collar = ColorUtil.dyeArgb(w.getCollarColor(), 0);
} else if (e instanceof Creeper cr) {
powered = cr.isPowered();
} else if (e instanceof Wither wi) {
// WitherBoss.isPowered(): health <= max health / 2 (wither armor layer).
AttributeInstance max = wi.getAttribute(Attribute.MAX_HEALTH);
powered = max != null && wi.getHealth() <= max.getValue() / 2;
} else if (e instanceof Axolotl a) {
variant = keyOf(a.getVariant());
} else if (e instanceof Parrot p) {
@@ -245,7 +262,7 @@ public final class EntitySnapshotBuilder {
// Equipment slots: saddle + BODY equipment (horse armor, llama carpet, harness, wolf/nautilus armor, …)
// for every mount — including ones added in newer versions — plus held items (by hand side, like
// vanilla: a left-handed mob holds its main-hand item left) and a non-armor head item.
ItemSpec rightHand = null, leftHand = null, headItem = null;
ItemSpec rightHand = null, leftHand = null, headItem = null, mouthItem = null;
EntityEquipment eq = e instanceof LivingEntity le ? le.getEquipment() : null;
if (eq != null) {
try {
@@ -263,8 +280,12 @@ public final class EntitySnapshotBuilder {
|| (e instanceof HumanEntity he && he.getMainHand() == MainHand.LEFT);
ItemSpec main = ItemSpecs.of(eq.getItemInMainHand());
ItemSpec off = ItemSpecs.of(eq.getItemInOffHand());
rightHand = leftHanded ? off : main;
leftHand = leftHanded ? main : off;
if (MOUTH_CARRIERS.contains(type)) {
mouthItem = main; // fox/dolphin carry their main-hand item in the mouth, not in a hand
} else {
rightHand = leftHanded ? off : main;
leftHand = leftHanded ? main : off;
}
ItemStack helmet = eq.getHelmet();
if (helmet != null && !helmet.getType().isAir() && armorAsset(helmet.getType()) == null) {
headItem = ItemSpecs.of(helmet);
@@ -312,7 +333,10 @@ public final class EntitySnapshotBuilder {
e.isInsideVehicle(),
rightHand,
leftHand,
headItem);
headItem,
collar,
powered,
mouthItem);
}
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
@@ -92,7 +92,6 @@ public final class ColorUtil {
return argb(a, r, g, b);
}
/** Scales the RGB channels by {@code factor} (0..1), keeping alpha. Used for directional face shading. */
/** Texels with alpha at or below this are holes (alpha cutout) for every intersector and the extrusion. */
public static final int ALPHA_CUTOUT = 16;
@@ -101,6 +100,7 @@ public final class ColorUtil {
return alpha(argb) > ALPHA_CUTOUT;
}
/** Scales the RGB channels by {@code factor} (0..1), keeping alpha. Used for directional face shading. */
public static int shade(int argb, double factor) {
int a = alpha(argb);
int r = clamp((int) (red(argb) * factor));
@@ -109,6 +109,13 @@ public final class ColorUtil {
return argb(a, r, g, b);
}
/** Additive blend (vanilla BlendFunction.ADDITIVE, ONE/ONE): RGB of {@code src} added to {@code dst}, clamped. */
public static int add(int dst, int src) {
return (clamp(red(dst) + red(src)) << 16)
| (clamp(green(dst) + green(src)) << 8)
| clamp(blue(dst) + blue(src));
}
private static int clamp(int v) {
return v < 0 ? 0 : Math.min(v, 255);
}
+1 -1
View File
@@ -185,7 +185,7 @@ public class EntityTestRender {
EntityState s = new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
isPlayer, null, false, VAR.get(key), tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
MARK.get(key), SADDLE.contains(key), CHEST.contains(key), EQUIP.get(key), baby ? null : ARMOR.get(key),
!baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null);
!baby && INVISIBLE.contains(key), 0, 0, tint2, false, null, null, null, 0, false, null);
RenderedEntity re = baker.bake(s);
if (re == null || re.cubes.isEmpty()) { // invisible with no equipment -> nothing renders
return new BufferedImage(TW, TH, BufferedImage.TYPE_INT_RGB);
+52
View File
@@ -79,6 +79,18 @@ public class PPTest extends JavaPlugin {
scenes.put("proj_stuck_close_t", new Scene("proj_stuck_close_t", this::projStuck, 18.5, 2.6, -3, 13.5, 1.6, 1));
// Rendered 4 ticks after the shots: freshly stuck arrows still wobble (vanilla shakeTime -> pitch offset).
scenes.put("proj_shake", new Scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
// Conditional vanilla layers: collars (tamed only, dye-tinted), charged creeper / wither armor (additive
// energy swirl), fox/dolphin mouth items.
scenes.put("layers", new Scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5));
scenes.put("layers_side", new Scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5));
scenes.put("layers_close", new Scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5));
scenes.put("layers_pets", new Scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0));
scenes.put("layers_dolphin", new Scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0));
scenes.put("layers_cats", new Scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0));
scenes.put("layers_cats_side", new Scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0));
scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5));
scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1));
scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
}
@@ -613,6 +625,46 @@ public class PPTest extends JavaPlugin {
t.setBillboard(Display.Billboard.CENTER);
}
// --- conditional layers ----------------------------------------------------------------------
private void layers(Location o) {
tame(spawn(o, 0, 0, 0, 180, Cat.class), DyeColor.RED);
tame(spawn(o, 1.2, 0, 0, 180, Cat.class), DyeColor.BLUE);
spawn(o, 2.4, 0, 0, 180, Cat.class); // untamed: no collar
Cat kitten = spawn(o, 3.4, 0, 0, 180, Cat.class);
kitten.setBaby();
tame(kitten, DyeColor.YELLOW);
tame(spawn(o, 5, 0, 0, 180, Wolf.class), DyeColor.RED);
tame(spawn(o, 6.4, 0, 0, 180, Wolf.class), DyeColor.LIME);
spawn(o, 7.8, 0, 0, 180, Wolf.class); // untamed: wild texture, no collar
Wolf pup = spawn(o, 9.0, 0, 0, 180, Wolf.class);
pup.setBaby();
tame(pup, DyeColor.MAGENTA);
spawn(o, 0.5, 0, 2.5, 180, Creeper.class);
spawn(o, 2.2, 0, 2.5, 180, Creeper.class).setPowered(true);
hands(spawn(o, 4.2, 0, 2.5, 180, Fox.class), Material.EMERALD, null);
Fox kit = spawn(o, 5.5, 0, 2.5, 180, Fox.class);
kit.setBaby();
hands(kit, Material.SWEET_BERRIES, null);
hands(spawn(o, 7.5, 0, 2.5, 180, Dolphin.class), Material.COD, null);
// Spawned mobs get a random body yaw: face them all to the camera (north).
for (Entity e : w.getNearbyEntities(o.clone().add(5, 0, 1), 7, 3, 4))
if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180);
}
private void tame(Tameable t, DyeColor collar) {
t.setTamed(true);
if (t instanceof Cat c) c.setCollarColor(collar);
if (t instanceof Wolf wo) wo.setCollarColor(collar);
}
private void withers(Location o) {
spawn(o, 1, 0, 1, 180, Wither.class); // full health: no armor
Wither half = spawn(o, 6, 0, 1, 180, Wither.class);
half.setHealth(half.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH).getValue() / 2 - 1);
for (Entity e : w.getNearbyEntities(o, 12, 8, 8)) if (e instanceof Wither wi) wi.setBodyYaw(180);
}
// --- projectiles -----------------------------------------------------------------------------
/** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */