mirror of
https://github.com/Sollace/Unicopia.git
synced 2025-02-01 11:36:43 +01:00
Added a hug ability
This commit is contained in:
parent
3e9a0df7a1
commit
a9923c71f4
15 changed files with 847 additions and 20 deletions
|
@ -36,6 +36,7 @@ public interface Abilities {
|
|||
Ability<?> KICK = register(new EarthPonyKickAbility(), "kick", AbilitySlot.PRIMARY);
|
||||
Ability<?> GROW = register(new EarthPonyGrowAbility(), "grow", AbilitySlot.SECONDARY);
|
||||
Ability<?> STOMP = register(new EarthPonyStompAbility(), "stomp", AbilitySlot.TERTIARY);
|
||||
Ability<?> HUG = register(new HugAbility(), "hug", AbilitySlot.TERTIARY);
|
||||
|
||||
// pegasus
|
||||
Ability<?> RAINBOOM = register(new PegasusRainboomAbility(), "rainboom", AbilitySlot.PRIMARY);
|
||||
|
|
|
@ -74,6 +74,18 @@ public class CarryAbility implements Ability<Hit> {
|
|||
PlayerEntity player = iplayer.asEntity();
|
||||
LivingEntity rider = findRider(player, iplayer.asWorld());
|
||||
|
||||
dropAllPassengers(player);
|
||||
|
||||
if (rider != null) {
|
||||
rider.startRiding(player, true);
|
||||
Living.getOrEmpty(rider).ifPresent(living -> living.setCarrier(player));
|
||||
}
|
||||
|
||||
Living.transmitPassengers(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void dropAllPassengers(PlayerEntity player) {
|
||||
if (player.hasPassengers()) {
|
||||
List<Entity> passengers = StreamSupport.stream(player.getPassengersDeep().spliterator(), false).toList();
|
||||
player.removeAllPassengers();
|
||||
|
@ -85,14 +97,6 @@ public class CarryAbility implements Ability<Hit> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rider != null) {
|
||||
rider.startRiding(player, true);
|
||||
Living.getOrEmpty(rider).ifPresent(living -> living.setCarrier(player));
|
||||
}
|
||||
|
||||
Living.transmitPassengers(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package com.minelittlepony.unicopia.ability;
|
||||
|
||||
import com.minelittlepony.unicopia.Race;
|
||||
import com.minelittlepony.unicopia.ability.data.Hit;
|
||||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
||||
import com.minelittlepony.unicopia.entity.Living;
|
||||
import com.minelittlepony.unicopia.entity.mob.FriendlyCreeperEntity;
|
||||
import com.minelittlepony.unicopia.entity.mob.UEntities;
|
||||
import com.minelittlepony.unicopia.entity.player.Pony;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.mob.CreeperEntity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.particle.ParticleTypes;
|
||||
|
||||
/**
|
||||
* Ability to hug mobs. Not all of them are receptive to your advances though, so be careful!
|
||||
*/
|
||||
public class HugAbility extends CarryAbility {
|
||||
|
||||
@Override
|
||||
public boolean canUse(Race race) {
|
||||
return race.canUseEarth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Pony pony, Hit data) {
|
||||
PlayerEntity player = pony.asEntity();
|
||||
LivingEntity rider = findRider(player, pony.asWorld());
|
||||
|
||||
dropAllPassengers(player);
|
||||
|
||||
pony.setAnimation(Animation.ARMS_FORWARD, Animation.Recipient.ANYONE);
|
||||
|
||||
if (rider instanceof CreeperEntity creeper) {
|
||||
FriendlyCreeperEntity friendlyCreeper = creeper.convertTo(UEntities.FRIENDLY_CREEPER, true);
|
||||
player.getWorld().spawnEntity(friendlyCreeper);
|
||||
|
||||
friendlyCreeper.startRiding(player, true);
|
||||
Living.getOrEmpty(friendlyCreeper).ifPresent(living -> living.setCarrier(player));
|
||||
} else if (rider instanceof FriendlyCreeperEntity creeper) {
|
||||
creeper.startRiding(player, true);
|
||||
Living.getOrEmpty(creeper).ifPresent(living -> living.setCarrier(player));
|
||||
} else if (rider != null) {
|
||||
rider.teleport(player.getX(), player.getY() + 0.5, player.getZ());
|
||||
rider.setYaw(player.getYaw() + 180);
|
||||
|
||||
if (rider instanceof FriendlyCreeperEntity) {
|
||||
pony.spawnParticles(ParticleTypes.HEART, 10);
|
||||
}
|
||||
}
|
||||
|
||||
Living.transmitPassengers(player);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -10,20 +10,17 @@ import com.minelittlepony.unicopia.ability.data.Hit;
|
|||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation;
|
||||
import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation.Recipient;
|
||||
import com.minelittlepony.unicopia.entity.player.Pony;
|
||||
import net.minecraft.block.BlockState;
|
||||
import com.minelittlepony.unicopia.util.ExplosionUtil;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.damage.DamageTypes;
|
||||
import net.minecraft.particle.ParticleTypes;
|
||||
import net.minecraft.predicate.entity.EntityPredicates;
|
||||
import net.minecraft.sound.SoundEvents;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.util.math.random.Random;
|
||||
import net.minecraft.world.BlockView;
|
||||
import net.minecraft.world.World.ExplosionSourceType;
|
||||
import net.minecraft.world.explosion.Explosion;
|
||||
import net.minecraft.world.explosion.ExplosionBehavior;
|
||||
|
||||
/**
|
||||
* Kirin ability to transform into a nirik
|
||||
|
@ -63,12 +60,7 @@ public class NirikBlastAbility implements Ability<Hit> {
|
|||
@Override
|
||||
public boolean apply(Pony player, Hit data) {
|
||||
|
||||
player.asWorld().createExplosion(player.asEntity(), player.damageOf(DamageTypes.FIREBALL), new ExplosionBehavior(){
|
||||
@Override
|
||||
public boolean canDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float power) {
|
||||
return false;
|
||||
}
|
||||
}, player.getOriginVector(), 5, true, ExplosionSourceType.MOB);
|
||||
player.asWorld().createExplosion(player.asEntity(), player.damageOf(DamageTypes.FIREBALL), ExplosionUtil.NON_DESTRUCTIVE, player.getOriginVector(), 5, true, ExplosionSourceType.MOB);
|
||||
player.setInvulnerabilityTicks(5);
|
||||
|
||||
player.setAnimation(Animation.ARMS_UP, Recipient.ANYONE, 12);
|
||||
|
|
|
@ -84,6 +84,7 @@ public interface URenderers {
|
|||
EntityRendererRegistry.register(UEntities.CRYSTAL_SHARDS, CrystalShardsEntityRenderer::new);
|
||||
EntityRendererRegistry.register(UEntities.STORM_CLOUD, StormCloudEntityRenderer::new);
|
||||
EntityRendererRegistry.register(UEntities.AIR_BALLOON, AirBalloonEntityRenderer::new);
|
||||
EntityRendererRegistry.register(UEntities.FRIENDLY_CREEPER, FriendlyCreeperEntityRenderer::new);
|
||||
|
||||
BlockEntityRendererFactories.register(UBlockEntities.WEATHER_VANE, WeatherVaneBlockEntityRenderer::new);
|
||||
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
package com.minelittlepony.unicopia.client.render.entity;
|
||||
|
||||
import net.minecraft.client.render.entity.feature.EnergySwirlOverlayFeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
|
||||
import com.minelittlepony.unicopia.Unicopia;
|
||||
import com.minelittlepony.unicopia.entity.mob.FriendlyCreeperEntity;
|
||||
|
||||
import net.minecraft.client.model.ModelPart;
|
||||
import net.minecraft.client.render.entity.EntityRendererFactory;
|
||||
import net.minecraft.client.render.entity.MobEntityRenderer;
|
||||
import net.minecraft.client.render.entity.model.CreeperEntityModel;
|
||||
import net.minecraft.client.render.entity.model.EntityModel;
|
||||
import net.minecraft.client.render.entity.model.EntityModelLayers;
|
||||
import net.minecraft.client.render.entity.model.EntityModelLoader;
|
||||
import net.minecraft.client.render.entity.model.EntityModelPartNames;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
||||
public class FriendlyCreeperEntityRenderer extends MobEntityRenderer<FriendlyCreeperEntity, FriendlyCreeperEntityRenderer.Model> {
|
||||
private static final Identifier FRIENDLY_TEXTURE = Unicopia.id("textures/entity/creeper/friendly.png");
|
||||
private static final Identifier UNFIRENDLY_TEXTURE = new Identifier("textures/entity/creeper/creeper.png");
|
||||
|
||||
public FriendlyCreeperEntityRenderer(EntityRendererFactory.Context context) {
|
||||
super(context, new Model(context.getPart(EntityModelLayers.CREEPER)), 0.5f);
|
||||
addFeature(new ChargeFeature(this, context.getModelLoader()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scale(FriendlyCreeperEntity creeperEntity, MatrixStack matrixStack, float f) {
|
||||
float g = creeperEntity.getClientFuseTime(f);
|
||||
float h = 1.0f + MathHelper.sin(g * 100.0f) * g * 0.01f;
|
||||
g = MathHelper.clamp(g, 0.0f, 1.0f);
|
||||
g *= g;
|
||||
g *= g;
|
||||
float i = (1.0f + g * 0.4f) * h;
|
||||
float j = (1.0f + g * 0.1f) / h;
|
||||
matrixStack.scale(i, j, i);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupTransforms(FriendlyCreeperEntity entity, MatrixStack matrices, float animationProgress, float bodyYaw, float tickDelta) {
|
||||
super.setupTransforms(entity, matrices, animationProgress, bodyYaw, tickDelta);
|
||||
if (entity.isSitting()) {
|
||||
matrices.translate(0, -0.25, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isShaking(FriendlyCreeperEntity entity) {
|
||||
return super.isShaking(entity) || entity.isConverting();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getAnimationCounter(FriendlyCreeperEntity entity, float f) {
|
||||
float fuseTime = entity.getClientFuseTime(f);
|
||||
if ((int)(fuseTime * 10) % 2 == 0) {
|
||||
return 0;
|
||||
}
|
||||
return MathHelper.clamp(fuseTime, 0.5f, 1.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getTexture(FriendlyCreeperEntity entity) {
|
||||
return entity.isConverting() ? UNFIRENDLY_TEXTURE : FRIENDLY_TEXTURE;
|
||||
}
|
||||
|
||||
public static class Model extends CreeperEntityModel<FriendlyCreeperEntity> {
|
||||
private final ModelPart leftHindLeg;
|
||||
private final ModelPart rightHindLeg;
|
||||
private final ModelPart leftFrontLeg;
|
||||
private final ModelPart rightFrontLeg;
|
||||
public Model(ModelPart root) {
|
||||
super(root);
|
||||
this.rightHindLeg = root.getChild(EntityModelPartNames.RIGHT_HIND_LEG);
|
||||
this.leftHindLeg = root.getChild(EntityModelPartNames.LEFT_HIND_LEG);
|
||||
this.rightFrontLeg = root.getChild(EntityModelPartNames.RIGHT_FRONT_LEG);
|
||||
this.leftFrontLeg = root.getChild(EntityModelPartNames.LEFT_FRONT_LEG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAngles(FriendlyCreeperEntity entity, float limbAngle, float limbDistance, float animationProgress, float headYaw, float headPitch) {
|
||||
leftHindLeg.resetTransform();
|
||||
rightHindLeg.resetTransform();
|
||||
leftFrontLeg.resetTransform();
|
||||
rightFrontLeg.resetTransform();
|
||||
super.setAngles(entity, limbAngle, limbDistance, animationProgress, headYaw, headPitch);
|
||||
if (entity.isSitting()) {
|
||||
float legSpread = 0.001F;
|
||||
leftHindLeg.pivotZ -= 3;
|
||||
leftHindLeg.pitch = MathHelper.HALF_PI;
|
||||
leftHindLeg.yaw = legSpread;
|
||||
rightHindLeg.pivotZ -= 3;
|
||||
rightHindLeg.pitch = MathHelper.HALF_PI;
|
||||
rightHindLeg.yaw = -legSpread;
|
||||
leftFrontLeg.pivotZ += 3;
|
||||
leftFrontLeg.pitch = -MathHelper.HALF_PI;
|
||||
leftFrontLeg.yaw = -legSpread;
|
||||
rightFrontLeg.pivotZ += 3;
|
||||
rightFrontLeg.pitch = -MathHelper.HALF_PI;
|
||||
rightFrontLeg.yaw = legSpread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChargeFeature extends EnergySwirlOverlayFeatureRenderer<FriendlyCreeperEntity, Model> {
|
||||
private static final Identifier SKIN = new Identifier("textures/entity/creeper/creeper_armor.png");
|
||||
private final CreeperEntityModel<FriendlyCreeperEntity> model;
|
||||
|
||||
public ChargeFeature(FeatureRendererContext<FriendlyCreeperEntity, Model> context, EntityModelLoader loader) {
|
||||
super(context);
|
||||
model = new Model(loader.getModelPart(EntityModelLayers.CREEPER_ARMOR));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getEnergySwirlX(float partialAge) {
|
||||
return partialAge * 0.01f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Identifier getEnergySwirlTexture() {
|
||||
return SKIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EntityModel<FriendlyCreeperEntity> getEnergySwirlModel() {
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import com.minelittlepony.unicopia.ability.magic.spell.effect.TargetSelecter;
|
|||
import com.minelittlepony.unicopia.entity.ai.BreakHeartGoal;
|
||||
import com.minelittlepony.unicopia.entity.ai.DynamicTargetGoal;
|
||||
import com.minelittlepony.unicopia.entity.ai.EatMuffinGoal;
|
||||
import com.minelittlepony.unicopia.entity.ai.FleeExplosionGoal;
|
||||
import com.minelittlepony.unicopia.entity.ai.WantItTakeItGoal;
|
||||
import com.minelittlepony.unicopia.entity.mob.UEntityAttributes;
|
||||
|
||||
|
@ -152,6 +153,9 @@ public class Creature extends Living<LivingEntity> implements WeaklyOwned.Mutabl
|
|||
eatMuffinGoal = new EatMuffinGoal(pig, targetter);
|
||||
goals.add(3, eatMuffinGoal);
|
||||
}
|
||||
if (entity instanceof TameableEntity tameable) {
|
||||
goals.add(3, new FleeExplosionGoal(tameable, 6, 1, 1.2));
|
||||
}
|
||||
|
||||
initMinionAi(targets);
|
||||
initDiscordedAi();
|
||||
|
|
|
@ -334,7 +334,7 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
|
|||
|
||||
if (isBeingCarried()) {
|
||||
Pony carrier = Pony.of(entity.getVehicle()).orElse(null);
|
||||
if (!Abilities.CARRY.canUse(carrier.getCompositeRace())) {
|
||||
if (!Abilities.CARRY.canUse(carrier.getCompositeRace()) && !Abilities.HUG.canUse(carrier.getCompositeRace())) {
|
||||
entity.stopRiding();
|
||||
entity.refreshPositionAfterTeleport(carrier.getOriginVector());
|
||||
Living.transmitPassengers(carrier.asEntity());
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
package com.minelittlepony.unicopia.entity.ai;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.minelittlepony.unicopia.entity.Creature;
|
||||
import com.minelittlepony.unicopia.entity.Living;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.TntEntity;
|
||||
import net.minecraft.entity.ai.NoPenaltyTargeting;
|
||||
import net.minecraft.entity.ai.goal.Goal;
|
||||
import net.minecraft.entity.ai.goal.PrioritizedGoal;
|
||||
import net.minecraft.entity.ai.pathing.Path;
|
||||
import net.minecraft.entity.mob.PathAwareEntity;
|
||||
import net.minecraft.entity.vehicle.TntMinecartEntity;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
public class FleeExplosionGoal extends Goal {
|
||||
private static final Predicate<Entity> SOURCE_PREDICATE = e -> e instanceof TntMinecartEntity || e instanceof TntEntity;
|
||||
|
||||
private final PathAwareEntity mob;
|
||||
private final double slowSpeed;
|
||||
private final double fastSpeed;
|
||||
private final Comparator<Entity> sorting;
|
||||
|
||||
@Nullable
|
||||
private Entity targetEntity;
|
||||
@Nullable
|
||||
private Path fleePath;
|
||||
|
||||
public FleeExplosionGoal(PathAwareEntity mob, float distance, double slowSpeed, double fastSpeed) {
|
||||
this.setControls(EnumSet.of(Goal.Control.MOVE));
|
||||
this.mob = mob;
|
||||
this.slowSpeed = slowSpeed;
|
||||
this.fastSpeed = fastSpeed;
|
||||
this.sorting = Comparator.comparingDouble(e -> e.squaredDistanceTo(mob));
|
||||
}
|
||||
|
||||
public void setFleeTarget(@Nullable Entity target) {
|
||||
this.targetEntity = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canStart() {
|
||||
if (targetEntity == null || targetEntity.isRemoved()) {
|
||||
targetEntity = mob.getWorld().getOtherEntities(mob, mob.getBoundingBox().expand(5, 3, 5), SOURCE_PREDICATE).stream().sorted(sorting).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
if (targetEntity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec3d targetPosition = NoPenaltyTargeting.findFrom(mob, 16, 7, targetEntity.getPos());
|
||||
if (targetPosition == null
|
||||
|| targetEntity.squaredDistanceTo(targetPosition.x, targetPosition.y, targetPosition.z) < targetEntity.squaredDistanceTo(mob)) {
|
||||
return false;
|
||||
}
|
||||
fleePath = mob.getNavigation().findPathTo(targetPosition.x, targetPosition.y, targetPosition.z, 0);
|
||||
return fleePath != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinue() {
|
||||
return !mob.getNavigation().isIdle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mob.getNavigation().startMovingAlong(fleePath, slowSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
targetEntity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (mob.squaredDistanceTo(targetEntity) < 49.0) {
|
||||
mob.getNavigation().setSpeed(fastSpeed);
|
||||
} else {
|
||||
mob.getNavigation().setSpeed(slowSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
public static void notifySurroundings(Entity explosionSource, float radius) {
|
||||
explosionSource.getWorld().getOtherEntities(explosionSource, explosionSource.getBoundingBox().expand(radius), e -> {
|
||||
return Living.getOrEmpty(e).filter(l -> l instanceof Creature c).isPresent();
|
||||
}).forEach(e -> {
|
||||
getGoals((Creature)Living.living(e)).forEach(goal -> goal.setFleeTarget(explosionSource));
|
||||
});
|
||||
}
|
||||
|
||||
private static Stream<FleeExplosionGoal> getGoals(Creature creature) {
|
||||
return creature.getGoals().stream()
|
||||
.flatMap(goals -> goals.getGoals().stream())
|
||||
.map(PrioritizedGoal::getGoal)
|
||||
.filter(g -> g instanceof FleeExplosionGoal)
|
||||
.map(FleeExplosionGoal.class::cast);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,511 @@
|
|||
package com.minelittlepony.unicopia.entity.mob;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.minelittlepony.unicopia.entity.Creature;
|
||||
import com.minelittlepony.unicopia.entity.Equine;
|
||||
import com.minelittlepony.unicopia.entity.ai.FleeExplosionGoal;
|
||||
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.render.entity.feature.SkinOverlayOwner;
|
||||
import net.minecraft.entity.AreaEffectCloudEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityStatuses;
|
||||
import net.minecraft.entity.EntityType;
|
||||
import net.minecraft.entity.LightningEntity;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.ai.goal.ActiveTargetGoal;
|
||||
import net.minecraft.entity.ai.goal.AttackWithOwnerGoal;
|
||||
import net.minecraft.entity.ai.goal.EscapeDangerGoal;
|
||||
import net.minecraft.entity.ai.goal.FleeEntityGoal;
|
||||
import net.minecraft.entity.ai.goal.Goal;
|
||||
import net.minecraft.entity.ai.goal.LookAroundGoal;
|
||||
import net.minecraft.entity.ai.goal.LookAtEntityGoal;
|
||||
import net.minecraft.entity.ai.goal.MeleeAttackGoal;
|
||||
import net.minecraft.entity.ai.goal.RevengeGoal;
|
||||
import net.minecraft.entity.ai.goal.SitGoal;
|
||||
import net.minecraft.entity.ai.goal.SwimGoal;
|
||||
import net.minecraft.entity.ai.goal.TrackOwnerAttackerGoal;
|
||||
import net.minecraft.entity.ai.goal.UniversalAngerGoal;
|
||||
import net.minecraft.entity.ai.goal.WanderAroundFarGoal;
|
||||
import net.minecraft.entity.ai.pathing.PathNodeType;
|
||||
import net.minecraft.entity.attribute.DefaultAttributeContainer;
|
||||
import net.minecraft.entity.attribute.EntityAttributes;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.entity.data.DataTracker;
|
||||
import net.minecraft.entity.data.TrackedData;
|
||||
import net.minecraft.entity.data.TrackedDataHandlerRegistry;
|
||||
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||
import net.minecraft.entity.mob.AbstractSkeletonEntity;
|
||||
import net.minecraft.entity.mob.Angerable;
|
||||
import net.minecraft.entity.mob.CreeperEntity;
|
||||
import net.minecraft.entity.mob.HostileEntity;
|
||||
import net.minecraft.entity.passive.CatEntity;
|
||||
import net.minecraft.entity.passive.GoatEntity;
|
||||
import net.minecraft.entity.passive.OcelotEntity;
|
||||
import net.minecraft.entity.passive.PassiveEntity;
|
||||
import net.minecraft.entity.passive.TameableEntity;
|
||||
import net.minecraft.entity.passive.WolfEntity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.Items;
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.nbt.NbtElement;
|
||||
import net.minecraft.particle.ParticleTypes;
|
||||
import net.minecraft.registry.tag.ItemTags;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.sound.SoundEvent;
|
||||
import net.minecraft.sound.SoundEvents;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.ActionResult;
|
||||
import net.minecraft.util.Hand;
|
||||
import net.minecraft.util.TimeHelper;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.util.math.intprovider.UniformIntProvider;
|
||||
import net.minecraft.world.BlockView;
|
||||
import net.minecraft.world.EntityView;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.event.GameEvent;
|
||||
import net.minecraft.world.explosion.Explosion;
|
||||
|
||||
public class FriendlyCreeperEntity extends TameableEntity implements SkinOverlayOwner, Angerable {
|
||||
private static final TrackedData<Integer> FUSE_SPEED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
||||
private static final TrackedData<Boolean> CHARGED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
|
||||
private static final TrackedData<Boolean> IGNITED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
|
||||
private static final TrackedData<Integer> ANGER_TIME = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
||||
private static final UniformIntProvider ANGER_TIME_RANGE = TimeHelper.betweenSeconds(20, 39);
|
||||
|
||||
private int lastFuseTime;
|
||||
private int currentFuseTime;
|
||||
private short fuseTime = 30;
|
||||
private byte explosionRadius = 3;
|
||||
private int headsDropped;
|
||||
private short hugTime;
|
||||
private short lastHugTime;
|
||||
|
||||
protected FriendlyCreeperEntity(EntityType<? extends FriendlyCreeperEntity> type, World world) {
|
||||
super(type, world);
|
||||
setTamed(false);
|
||||
setPathfindingPenalty(PathNodeType.POWDER_SNOW, -1);
|
||||
setPathfindingPenalty(PathNodeType.DANGER_POWDER_SNOW, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataTracker() {
|
||||
super.initDataTracker();
|
||||
dataTracker.startTracking(FUSE_SPEED, -1);
|
||||
dataTracker.startTracking(CHARGED, false);
|
||||
dataTracker.startTracking(IGNITED, false);
|
||||
dataTracker.startTracking(ANGER_TIME, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initGoals() {
|
||||
goalSelector.add(1, new SwimGoal(this));
|
||||
goalSelector.add(1, new EscapeGoal(1.5));
|
||||
goalSelector.add(2, new SitGoal(this));
|
||||
goalSelector.add(3, new IgniteGoal());
|
||||
goalSelector.add(4, new FleeEntityGoal<>(this, OcelotEntity.class, 6, 1, 1.2));
|
||||
goalSelector.add(4, new FleeEntityGoal<>(this, CatEntity.class, 6, 1, 1.2));
|
||||
goalSelector.add(5, new MeleeAttackGoal(this, 1, false));
|
||||
goalSelector.add(6, new WanderAroundFarGoal(this, 0.8));
|
||||
goalSelector.add(7, new LookAtEntityGoal(this, PlayerEntity.class, 8));
|
||||
goalSelector.add(7, new LookAroundGoal(this));
|
||||
targetSelector.add(1, new TrackOwnerAttackerGoal(this));
|
||||
targetSelector.add(2, new AttackWithOwnerGoal(this));
|
||||
targetSelector.add(3, new RevengeGoal(this).setGroupRevenge(WolfEntity.class));
|
||||
targetSelector.add(7, new ActiveTargetGoal<>(this, AbstractSkeletonEntity.class, false));
|
||||
targetSelector.add(8, new UniversalAngerGoal<>(this, true));
|
||||
}
|
||||
|
||||
public static DefaultAttributeContainer.Builder createCreeperAttributes() {
|
||||
return HostileEntity.createHostileAttributes().add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Text getDefaultName() {
|
||||
return EntityType.CREEPER.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSafeFallDistance() {
|
||||
return 3 + (getTarget() == null ? 0 : ((int)(getHealth() - 1)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) {
|
||||
boolean bl = super.handleFallDamage(fallDistance, damageMultiplier, damageSource);
|
||||
currentFuseTime += (int)(fallDistance * 1.5f);
|
||||
if (currentFuseTime > fuseTime - 5) {
|
||||
currentFuseTime = fuseTime - 5;
|
||||
}
|
||||
return bl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCustomDataToNbt(NbtCompound nbt) {
|
||||
super.writeCustomDataToNbt(nbt);
|
||||
if (dataTracker.get(CHARGED).booleanValue()) {
|
||||
nbt.putBoolean("powered", true);
|
||||
}
|
||||
nbt.putShort("Fuse", fuseTime);
|
||||
nbt.putShort("Hugged", hugTime);
|
||||
nbt.putByte("ExplosionRadius", explosionRadius);
|
||||
nbt.putBoolean("ignited", isIgnited());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readCustomDataFromNbt(NbtCompound nbt) {
|
||||
super.readCustomDataFromNbt(nbt);
|
||||
dataTracker.set(CHARGED, nbt.getBoolean("powered"));
|
||||
if (nbt.contains("Fuse", NbtElement.NUMBER_TYPE)) {
|
||||
fuseTime = nbt.getShort("Fuse");
|
||||
}
|
||||
if (nbt.contains("Hugged", NbtElement.NUMBER_TYPE)) {
|
||||
hugTime = nbt.getShort("Hugged");
|
||||
}
|
||||
if (nbt.contains("ExplosionRadius", NbtElement.NUMBER_TYPE)) {
|
||||
explosionRadius = nbt.getByte("ExplosionRadius");
|
||||
}
|
||||
if (nbt.getBoolean("ignited")) {
|
||||
ignite();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
setSitting(isInSittingPose());
|
||||
|
||||
if (isAlive()) {
|
||||
lastFuseTime = currentFuseTime;
|
||||
if (isIgnited()) {
|
||||
setFuseSpeed(1);
|
||||
}
|
||||
int fuseSpeed = getFuseSpeed();
|
||||
if (fuseSpeed > 0 && currentFuseTime == 0) {
|
||||
playSound(SoundEvents.ENTITY_CREEPER_PRIMED, 1.0f, 0.5f);
|
||||
emitGameEvent(GameEvent.PRIME_FUSE);
|
||||
}
|
||||
currentFuseTime = Math.max(0, currentFuseTime + fuseSpeed);
|
||||
if (currentFuseTime >= fuseTime) {
|
||||
currentFuseTime = fuseTime;
|
||||
explode();
|
||||
}
|
||||
|
||||
lastHugTime = hugTime;
|
||||
|
||||
if (!isTamed()) {
|
||||
if (isConverting()) {
|
||||
if (++hugTime >= 100) {
|
||||
if (!getWorld().isClient) {
|
||||
setOwner(getCreature().getCarrierId().map(getWorld()::getPlayerByUuid).orElse(null));
|
||||
getWorld().sendEntityStatus(this, EntityStatuses.ADD_POSITIVE_PLAYER_REACTION_PARTICLES);
|
||||
}
|
||||
}
|
||||
|
||||
if (hugTime % 5 == 0) {
|
||||
playHurtSound(getDamageSources().generic());
|
||||
}
|
||||
} else {
|
||||
hugTime = 0;
|
||||
if (!getWorld().isClient) {
|
||||
getWorld().spawnEntity(convertTo(EntityType.CREEPER, true));
|
||||
discard();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (random.nextInt(30) == 0) {
|
||||
spawnHeart();
|
||||
}
|
||||
hugTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
super.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tickMovement() {
|
||||
super.tickMovement();
|
||||
|
||||
if (!getWorld().isClient) {
|
||||
tickAngerLogic((ServerWorld)getWorld(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnHeart() {
|
||||
getWorld().addParticle(ParticleTypes.HEART, random.nextTriangular(getX(), 0.5), getY() + getHeight(), random.nextTriangular(getZ(), 0.5), 0, 0, 0);
|
||||
}
|
||||
|
||||
private Creature getCreature() {
|
||||
return Equine.<FriendlyCreeperEntity, Creature>of(this, c -> c instanceof Creature).get();
|
||||
}
|
||||
|
||||
public boolean isConverting() {
|
||||
return !isTamed() && getCreature().isBeingCarried();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTarget(@Nullable LivingEntity target) {
|
||||
if (target instanceof GoatEntity) {
|
||||
return;
|
||||
}
|
||||
super.setTarget(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SoundEvent getHurtSound(DamageSource source) {
|
||||
return SoundEvents.ENTITY_CREEPER_HURT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SoundEvent getDeathSound() {
|
||||
return SoundEvents.ENTITY_CREEPER_DEATH;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dropEquipment(DamageSource source, int lootingMultiplier, boolean allowDrops) {
|
||||
super.dropEquipment(source, lootingMultiplier, allowDrops);
|
||||
if (source.getAttacker() instanceof CreeperEntity c && c.shouldDropHead()) {
|
||||
c.onHeadDropped();
|
||||
dropItem(Items.CREEPER_HEAD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryAttack(Entity target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRenderOverlay() {
|
||||
return dataTracker.get(CHARGED);
|
||||
}
|
||||
|
||||
public float getClientFuseTime(float timeDelta) {
|
||||
return MathHelper.lerp(timeDelta, (float)lastFuseTime, (float)currentFuseTime) / (fuseTime - 2)
|
||||
+ MathHelper.lerp(timeDelta, (float)lastHugTime, (float)hugTime) / 98F;
|
||||
}
|
||||
|
||||
public int getFuseSpeed() {
|
||||
return dataTracker.get(FUSE_SPEED);
|
||||
}
|
||||
|
||||
public void setFuseSpeed(int fuseSpeed) {
|
||||
dataTracker.set(FUSE_SPEED, fuseSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStruckByLightning(ServerWorld world, LightningEntity lightning) {
|
||||
super.onStruckByLightning(world, lightning);
|
||||
dataTracker.set(CHARGED, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult interactMob(PlayerEntity player, Hand hand) {
|
||||
ItemStack stack = player.getStackInHand(hand);
|
||||
|
||||
Consumer<PlayerEntity> statusCallback = p -> p.sendToolBreakStatus(hand);
|
||||
|
||||
if (stack.isEmpty() && isOwner(player)) {
|
||||
setSitting(!isSitting());
|
||||
setInSittingPose(isSitting());
|
||||
return ActionResult.success(getWorld().isClient);
|
||||
}
|
||||
|
||||
if (stack.isIn(ItemTags.CREEPER_IGNITERS)) {
|
||||
SoundEvent soundEvent = stack.isOf(Items.FIRE_CHARGE) ? SoundEvents.ITEM_FIRECHARGE_USE : SoundEvents.ITEM_FLINTANDSTEEL_USE;
|
||||
getWorld().playSound(player, getX(), getY(), getZ(), soundEvent, getSoundCategory(), 1, random.nextFloat() * 0.4f + 0.8f);
|
||||
|
||||
if (!getWorld().isClient) {
|
||||
ignite();
|
||||
if (!stack.isDamageable()) {
|
||||
stack.decrement(1);
|
||||
} else {
|
||||
stack.damage(1, player, statusCallback);
|
||||
}
|
||||
}
|
||||
|
||||
return ActionResult.success(getWorld().isClient);
|
||||
}
|
||||
|
||||
if (stack.isOf(Items.GUNPOWDER) && getHealth() < getMaxHealth()) {
|
||||
getWorld().playSound(player, getX(), getY(), getZ(), SoundEvents.ENTITY_CAT_EAT, getSoundCategory(), 1, random.nextFloat() * 0.4f + 0.8f);
|
||||
|
||||
currentFuseTime = fuseTime - 1;
|
||||
if (!getWorld().isClient) {
|
||||
heal(3);
|
||||
getWorld().sendEntityStatus(this, EntityStatuses.ADD_POSITIVE_PLAYER_REACTION_PARTICLES);
|
||||
if (!stack.isDamageable()) {
|
||||
stack.decrement(1);
|
||||
} else {
|
||||
stack.damage(1, player, statusCallback);
|
||||
}
|
||||
}
|
||||
|
||||
return ActionResult.success(getWorld().isClient);
|
||||
}
|
||||
|
||||
return super.interactMob(player, hand);
|
||||
}
|
||||
|
||||
private void explode() {
|
||||
if (!getWorld().isClient) {
|
||||
dead = true;
|
||||
getWorld().createExplosion(this, getX(), getY(), getZ(), getExplosionRadius(), World.ExplosionSourceType.MOB);
|
||||
discard();
|
||||
spawnEffectsCloud();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExplosionDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float explosionPower) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private float getExplosionRadius() {
|
||||
return explosionRadius * (shouldRenderOverlay() ? 2 : 1);
|
||||
}
|
||||
|
||||
private void spawnEffectsCloud() {
|
||||
Collection<StatusEffectInstance> effects = getStatusEffects();
|
||||
if (!effects.isEmpty()) {
|
||||
AreaEffectCloudEntity cloud = new AreaEffectCloudEntity(getWorld(), getX(), getY(), getZ());
|
||||
cloud.setRadius(2.5f);
|
||||
cloud.setRadiusOnUse(-0.5f);
|
||||
cloud.setWaitTime(10);
|
||||
cloud.setDuration(cloud.getDuration() / 2);
|
||||
cloud.setRadiusGrowth(-cloud.getRadius() / cloud.getDuration());
|
||||
effects.forEach(effect -> cloud.addEffect(new StatusEffectInstance(effect)));
|
||||
getWorld().spawnEntity(cloud);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isIgnited() {
|
||||
return dataTracker.get(IGNITED);
|
||||
}
|
||||
|
||||
public void ignite() {
|
||||
dataTracker.set(IGNITED, true);
|
||||
FleeExplosionGoal.notifySurroundings(this, getExplosionRadius());
|
||||
}
|
||||
|
||||
public boolean shouldDropHead() {
|
||||
return shouldRenderOverlay() && headsDropped < 1;
|
||||
}
|
||||
|
||||
public void onHeadDropped() {
|
||||
headsDropped++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityView method_48926() {
|
||||
return getWorld();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PassiveEntity createChild(ServerWorld world, PassiveEntity partner) {
|
||||
FriendlyCreeperEntity child = (FriendlyCreeperEntity)getType().create(world);
|
||||
UUID uUID = getOwnerUuid();
|
||||
if (uUID != null) {
|
||||
child.setOwnerUuid(uUID);
|
||||
child.setTamed(true);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private UUID angerTarget;
|
||||
|
||||
@Override
|
||||
public int getAngerTime() {
|
||||
return dataTracker.get(ANGER_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAngerTime(int time) {
|
||||
dataTracker.set(ANGER_TIME, time);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UUID getAngryAt() {
|
||||
return angerTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAngryAt(UUID target) {
|
||||
angerTarget = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chooseRandomAngerTime() {
|
||||
setAngerTime(ANGER_TIME_RANGE.get(random));
|
||||
}
|
||||
|
||||
class IgniteGoal extends Goal {
|
||||
@Nullable
|
||||
private LivingEntity target;
|
||||
|
||||
public IgniteGoal() {
|
||||
this.setControls(EnumSet.of(Goal.Control.MOVE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canStart() {
|
||||
LivingEntity livingEntity = getTarget();
|
||||
return getFuseSpeed() > 0 || livingEntity != null && squaredDistanceTo(livingEntity) < 9.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
getNavigation().stop();
|
||||
target = getTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
target = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRunEveryTick() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (target == null) {
|
||||
setFuseSpeed(-1);
|
||||
return;
|
||||
}
|
||||
if (squaredDistanceTo(target) > 49.0) {
|
||||
setFuseSpeed(-1);
|
||||
return;
|
||||
}
|
||||
if (!getVisibilityCache().canSee(target)) {
|
||||
setFuseSpeed(-1);
|
||||
return;
|
||||
}
|
||||
setFuseSpeed(1);
|
||||
}
|
||||
}
|
||||
|
||||
class EscapeGoal extends EscapeDangerGoal {
|
||||
|
||||
public EscapeGoal(double speed) {
|
||||
super(FriendlyCreeperEntity.this, speed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInDanger() {
|
||||
return mob.shouldEscapePowderSnow() || mob.isOnFire();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,6 +45,10 @@ public interface UEntities {
|
|||
EntityType<FairyEntity> TWITTERMITE = register("twittermite", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FairyEntity::new)
|
||||
.trackRangeBlocks(200)
|
||||
.dimensions(EntityDimensions.fixed(0.1F, 0.1F)));
|
||||
EntityType<FriendlyCreeperEntity> FRIENDLY_CREEPER = register("friendly_creeper", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FriendlyCreeperEntity::new)
|
||||
.trackRangeBlocks(8)
|
||||
.dimensions(EntityDimensions.fixed(0.6f, 1.7f))
|
||||
);
|
||||
EntityType<SpellbookEntity> SPELLBOOK = register("spellbook", FabricEntityTypeBuilder.create(SpawnGroup.MISC, SpellbookEntity::new)
|
||||
.trackRangeBlocks(200)
|
||||
.dimensions(EntityDimensions.fixed(0.9F, 0.5F)));
|
||||
|
@ -72,6 +76,7 @@ public interface UEntities {
|
|||
FabricDefaultAttributeRegistry.register(TWITTERMITE, FairyEntity.createMobAttributes());
|
||||
FabricDefaultAttributeRegistry.register(AIR_BALLOON, FlyingEntity.createMobAttributes());
|
||||
FabricDefaultAttributeRegistry.register(SOMBRA, SombraEntity.createMobAttributes());
|
||||
FabricDefaultAttributeRegistry.register(FRIENDLY_CREEPER, FriendlyCreeperEntity.createCreeperAttributes());
|
||||
|
||||
if (!Unicopia.getConfig().disableButterflySpawning.get()) {
|
||||
final Predicate<BiomeSelectionContext> butterflySpawnable = BiomeSelectors.foundInOverworld()
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package com.minelittlepony.unicopia.util;
|
||||
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.BlockView;
|
||||
import net.minecraft.world.explosion.Explosion;
|
||||
import net.minecraft.world.explosion.ExplosionBehavior;
|
||||
|
||||
public interface ExplosionUtil {
|
||||
ExplosionBehavior NON_DESTRUCTIVE = new ExplosionBehavior(){
|
||||
@Override
|
||||
public boolean canDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float power) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -406,6 +406,7 @@
|
|||
"ability.unicopia.grow": "Nourish Earth",
|
||||
"ability.unicopia.stomp": "Ground Pound",
|
||||
"ability.unicopia.kick": "Crushing Blow",
|
||||
"ability.unicopia.hug": "Hug",
|
||||
"ability.unicopia.pummel": "Devestating Smash",
|
||||
"ability.unicopia.carry": "Pickup/Drop Passenger",
|
||||
"ability.unicopia.toggle_flight": "Take-off/Land",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
BIN
src/main/resources/assets/unicopia/textures/gui/ability/hug.png
Normal file
BIN
src/main/resources/assets/unicopia/textures/gui/ability/hug.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
Loading…
Reference in a new issue