/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.entity;
import static com.google.common.base.Preconditions.checkNotNull;
import com.flowpowered.math.vector.Vector3d;
import org.lanternpowered.server.data.key.LanternKeys;
import org.lanternpowered.server.entity.event.CollectEntityEvent;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.lanternpowered.server.inventory.LanternItemStackSnapshot;
import org.lanternpowered.server.network.entity.EntityProtocolTypes;
import org.spongepowered.api.block.BlockTypes;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.effect.particle.ParticleEffect;
import org.spongepowered.api.effect.particle.ParticleTypes;
import org.spongepowered.api.entity.Entity;
import org.spongepowered.api.entity.Item;
import org.spongepowered.api.entity.living.Living;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.item.ItemType;
import org.spongepowered.api.item.ItemTypes;
import org.spongepowered.api.item.inventory.Carrier;
import org.spongepowered.api.item.inventory.Inventory;
import org.spongepowered.api.item.inventory.ItemStack;
import org.spongepowered.api.item.inventory.ItemStackSnapshot;
import org.spongepowered.api.item.inventory.entity.PlayerInventory;
import org.spongepowered.api.item.inventory.transaction.InventoryTransactionResult;
import org.spongepowered.api.statistic.achievement.Achievements;
import org.spongepowered.api.util.AABB;
import org.spongepowered.api.util.Direction;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
public class LanternItem extends LanternEntity implements Item {
private static final class EffectHolder {
private static final ParticleEffect DEATH_EFFECT =
ParticleEffect.builder().type(ParticleTypes.CLOUD).quantity(3).offset(Vector3d.ONE.mul(0.1)).build();
}
private static final AABB BOUNDING_BOX_BASE = new AABB(new Vector3d(-0.125, 0, -0.125), new Vector3d(0.125, 0.25, 0.125));
private static final int NO_DESPAWN_DELAY = 59536;
private static final int NO_PICKUP_DELAY = 32767;
private int counter;
public LanternItem(UUID uniqueId) {
super(uniqueId);
setEntityProtocolType(EntityProtocolTypes.ITEM);
setBoundingBoxBase(BOUNDING_BOX_BASE);
}
@Override
public void registerKeys() {
super.registerKeys();
registerKey(Keys.REPRESENTED_ITEM, null);
registerKey(Keys.PICKUP_DELAY, 60);
registerKey(Keys.DESPAWN_DELAY, 6000);
registerKey(LanternKeys.GRAVITY_FACTOR, 0.002);
}
@Override
public void pulse() {
super.pulse();
int pickupDelay = get(Keys.PICKUP_DELAY).orElse(0);
int despawnDelay = get(Keys.DESPAWN_DELAY).orElse(NO_DESPAWN_DELAY);
final int oldPickupDelay = pickupDelay;
final int oldDespawnDelay = despawnDelay;
if (pickupDelay != NO_PICKUP_DELAY && pickupDelay > 0) {
pickupDelay--;
}
if (despawnDelay != NO_DESPAWN_DELAY && despawnDelay > 0) {
despawnDelay--;
}
if (this.counter++ % 20 == 0) {
final CombineData data = combineItemStacks(pickupDelay, despawnDelay);
if (data != null) {
pickupDelay = data.pickupDelay;
despawnDelay = data.despawnDelay;
}
}
if (this.counter % 10 == 0 && pickupDelay != NO_PICKUP_DELAY && pickupDelay <= 0) {
tryToPickupItems();
}
if (pickupDelay != oldPickupDelay) {
offer(Keys.PICKUP_DELAY, pickupDelay);
}
if (despawnDelay != oldDespawnDelay) {
offer(Keys.DESPAWN_DELAY, despawnDelay);
}
if (despawnDelay <= 0) {
// A death animation/particle?
getWorld().spawnParticles(EffectHolder.DEATH_EFFECT, getBoundingBox().get().getCenter());
remove();
} else {
pulsePhysics();
}
}
private void pulsePhysics() {
// Get the current velocity
Vector3d velocity = getVelocity();
// Update the position based on the velocity
setPosition(getPosition().add(velocity));
// We will check if there is a collision box under the entity
boolean ground = false;
final AABB thisBox = getBoundingBox().get().offset(0, -0.1, 0);
final Set<AABB> boxes = getWorld().getIntersectingBlockCollisionBoxes(thisBox);
for (AABB box : boxes) {
final Vector3d factor = box.getCenter().sub(thisBox.getCenter());
if (Direction.getClosest(factor).isUpright()) {
ground = true;
}
}
if (!ground) {
final Optional<Double> gravityFactor = get(LanternKeys.GRAVITY_FACTOR);
if (gravityFactor.isPresent()) {
// Apply the gravity factor
velocity = velocity.add(0, -gravityFactor.get(), 0);
}
}
velocity = velocity.mul(0.98, 0.98, 0.98);
if (ground) {
velocity = velocity.mul(1, -0.5, 1);
}
// Offer the velocity back
offer(Keys.VELOCITY, velocity);
}
private void tryToPickupItems() {
final Set<Entity> entities = getWorld().getIntersectingEntities(
getBoundingBox().get().expand(2.0, 0.5, 2.0), entity -> entity != this && entity instanceof Carrier);
if (entities.isEmpty()) {
return;
}
ItemStack itemStack = get(Keys.REPRESENTED_ITEM).get().createStack();
for (Entity entity : entities) {
Inventory inventory = ((Carrier) entity).getInventory();
if (inventory instanceof PlayerInventory) {
inventory = ((PlayerInventory) inventory).getMain();
}
final InventoryTransactionResult result = inventory.offer(itemStack);
final Collection<ItemStackSnapshot> rejected = result.getRejectedItems();
final int added;
if (!rejected.isEmpty()) {
final ItemStack itemStack1 = rejected.iterator().next().createStack();
added = itemStack.getQuantity() - itemStack1.getQuantity();
itemStack = itemStack1;
} else {
added = itemStack.getQuantity();
}
if (added != 0 && entity instanceof Living) {
// Trigger achievements
if (entity instanceof Player) {
final LanternPlayer player = (LanternPlayer) entity;
final ItemType itemType = itemStack.getItem();
if (itemType == BlockTypes.LOG.getItem().get() ||
itemType == BlockTypes.LOG2.getItem().get()) {
player.triggerAchievement(Achievements.MINE_WOOD);
} else if (itemType == ItemTypes.LEATHER) {
player.triggerAchievement(Achievements.KILL_COW);
} else if (itemType == ItemTypes.DIAMOND) {
player.triggerAchievement(Achievements.DIAMONDS);
} else if (itemType == ItemTypes.BLAZE_ROD) {
player.triggerAchievement(Achievements.BLAZE_ROD);
}
}
triggerEvent(new CollectEntityEvent((Living) entity, added));
}
if (rejected.isEmpty()) {
itemStack = null;
}
if (itemStack == null) {
break;
}
}
if (itemStack != null) {
offer(Keys.REPRESENTED_ITEM, itemStack.createSnapshot());
} else {
remove();
}
}
private final class CombineData {
private final int pickupDelay;
private final int despawnDelay;
private CombineData(int pickupDelay, int despawnDelay) {
this.despawnDelay = despawnDelay;
this.pickupDelay = pickupDelay;
}
}
@Nullable
private CombineData combineItemStacks(int pickupDelay, int despawnDelay) {
ItemStackSnapshot itemStackSnapshot1 = get(Keys.REPRESENTED_ITEM).get();
if (itemStackSnapshot1.getCount() >= itemStackSnapshot1.getType().getMaxStackQuantity()) {
return null;
}
checkNotNull(getWorld());
Set<Entity> entities = getWorld().getIntersectingEntities(
getBoundingBox().get().expand(0.6, 0.0, 0.6), entity -> entity != this && entity instanceof LanternItem);
if (!entities.isEmpty()) {
ItemStack itemStack = null;
for (Entity entity : entities) {
final int pickupDelay1 = entity.get(Keys.PICKUP_DELAY).orElse(0);
if (pickupDelay1 == NO_PICKUP_DELAY) {
continue;
}
final ItemStackSnapshot itemStackSnapshot2 = entity.get(Keys.REPRESENTED_ITEM).get();
if (itemStackSnapshot2.getCount() < itemStackSnapshot1.getCount()) {
continue;
}
if (((LanternItemStackSnapshot) itemStackSnapshot1).isSimilar(itemStackSnapshot2)) {
final int max = itemStackSnapshot1.getType().getMaxStackQuantity();
int quantity = itemStackSnapshot1.getCount() + itemStackSnapshot2.getCount();
if (quantity > max) {
final ItemStack itemStack2 = itemStackSnapshot2.createStack();
itemStack2.setQuantity(quantity - max);
entity.offer(Keys.REPRESENTED_ITEM, itemStack2.createSnapshot());
quantity = max;
} else {
entity.remove();
}
if (itemStack == null) {
itemStack = itemStackSnapshot1.createStack();
}
itemStack.setQuantity(quantity);
pickupDelay = Math.max(pickupDelay, pickupDelay1);
despawnDelay = Math.max(despawnDelay, entity.get(Keys.DESPAWN_DELAY).orElse(NO_DESPAWN_DELAY));
if (quantity >= max) {
break;
}
}
}
if (itemStack != null) {
offer(Keys.REPRESENTED_ITEM, itemStack.createSnapshot());
return new CombineData(pickupDelay, despawnDelay);
}
}
return null;
}
}