diff --git a/packages/serenity/src/handlers/player-action.ts b/packages/serenity/src/handlers/player-action.ts index 0511216c..fc75cb86 100644 --- a/packages/serenity/src/handlers/player-action.ts +++ b/packages/serenity/src/handlers/player-action.ts @@ -307,6 +307,9 @@ class PlayerAction extends SerenityHandler { // If so, we will return. if (player.gamemode === Gamemode.Creative) return; + // Exhaust the player + player.exhaust(0.005); + // Create a new ItemStack. const itemType = ItemType.resolve(permutation.type) as ItemType; const itemStack = ItemStack.create(itemType, 1, permutation.index); diff --git a/packages/world/src/components/entity/attribute/absorption.ts b/packages/world/src/components/entity/attribute/absorption.ts new file mode 100644 index 00000000..e40b51ba --- /dev/null +++ b/packages/world/src/components/entity/attribute/absorption.ts @@ -0,0 +1,20 @@ +import { AttributeName } from "@serenityjs/protocol"; + +import { EntityAttributeComponent } from "./attribute"; + +import type { Player } from "../../../player"; + +class PlayerAbsorptionComponent extends EntityAttributeComponent { + public static readonly identifier = AttributeName.Absorption; + + public readonly effectiveMin: number = 0; + public readonly effectiveMax: number = 512; + public readonly defaultValue: number = 0; + + public constructor(player: Player) { + super(player, PlayerAbsorptionComponent.identifier); + this.setCurrentValue(this.defaultValue, false); + } +} + +export { PlayerAbsorptionComponent }; diff --git a/packages/world/src/components/entity/attribute/health.ts b/packages/world/src/components/entity/attribute/health.ts index cca8ddb1..28c4c46e 100644 --- a/packages/world/src/components/entity/attribute/health.ts +++ b/packages/world/src/components/entity/attribute/health.ts @@ -3,6 +3,7 @@ import { ActorEventIds, ActorEventPacket, AttributeName, + EffectType, Gamemode, ItemUseOnEntityInventoryTransactionType, Vector3f @@ -52,12 +53,25 @@ class EntityHealthComponent extends EntityAttributeComponent { * @param damage The amount of damage to apply to the entity. */ public applyDamage(damage: number, cause?: ActorDamageCause): void { - // Decrease the health of the entity - this.decreaseValue(damage); - // Create a new actor event packet const packet = new ActorEventPacket(); + if ( + this.entity.hasEffect(EffectType.Absorption) && + this.entity.hasComponent("minecraft:absorption") + ) { + // ? Get the current absorption value + const absorption = this.entity.getComponent("minecraft:absorption"); + // ? Get the new damage, to decrease the health if there is more damage than absorption points + const remainingDamage = damage - absorption.getCurrentValue(); + + // ? Decrease absorption points and health if remaining + absorption.decreaseValue(damage); + this.decreaseValue(Math.max(0, remainingDamage)); + } else { + // Decrease the health of the entity + this.decreaseValue(damage); + } // Assign the values to the packet packet.actorRuntimeId = this.entity.runtime; packet.eventId = ActorEventIds.HURT_ANIMATION; @@ -100,7 +114,6 @@ class EntityHealthComponent extends EntityAttributeComponent { // Decrease the health of the entity this.decreaseValue(damage); - // Return the current health of the entity return this.getCurrentValue(); } @@ -114,6 +127,8 @@ class EntityHealthComponent extends EntityAttributeComponent { */ // Check if the player is attacking the entity if (type !== ItemUseOnEntityInventoryTransactionType.Attack) return; + // Exhaust the player after attacking + player.exhaust(0.1); // Check if the entity is a player and the player is in creative mode if (this.entity.isPlayer() && this.entity.gamemode === Gamemode.Creative) diff --git a/packages/world/src/components/entity/attribute/index.ts b/packages/world/src/components/entity/attribute/index.ts index 72f698e3..b32ce544 100644 --- a/packages/world/src/components/entity/attribute/index.ts +++ b/packages/world/src/components/entity/attribute/index.ts @@ -4,3 +4,4 @@ export * from "./attribute"; // Concrete components export * from "./health"; export * from "./movement"; +export * from "./absorption"; diff --git a/packages/world/src/components/item/food.ts b/packages/world/src/components/item/food.ts new file mode 100644 index 00000000..d94fc0ab --- /dev/null +++ b/packages/world/src/components/item/food.ts @@ -0,0 +1,78 @@ +import { ItemIdentifier, type Items } from "@serenityjs/item"; + +import { ItemUseCause } from "../../enums"; +import { ItemStack } from "../../item"; + +import { ItemComponent } from "./item-component"; + +import type { Player } from "../../player"; + +class ItemFoodComponent extends ItemComponent { + public static readonly identifier = "minecraft:food"; + + /** + * The nutrition value that will be given to the player when eated + */ + public nutrition: number = 0; + + /** + * The saturation modifier to apply the saturation buff + */ + public saturationModifier: number = 0; + + /** + * Means if the food can be eaten whether the player is hungry or not + */ + public canAlwaysEat: boolean = false; + + /** + * It's the item that will be converted to when eaten + */ + public convertsTo: ItemIdentifier = ItemIdentifier.Air; + + public constructor(item: ItemStack) { + super(item, ItemFoodComponent.identifier); + } + + public onUse(player: Player, cause: ItemUseCause): boolean { + if (cause != ItemUseCause.Use || !player.usingItem) return false; + if (!this.canAlwaysEat && !player.isHungry()) return false; + // ? Get the player hunger, saturation and inventory + const hungerComponent = player.getComponent("minecraft:player.hunger"); + const saturationComponent = player.getComponent( + "minecraft:player.saturation" + ); + const { container, selectedSlot } = player.getComponent( + "minecraft:inventory" + ); + // ? Increase the food based on nutrition + hungerComponent.increaseValue(this.nutrition); + // ? Add a saturation buff using the formula nutrition * saturationModifier * 2 + saturationComponent.increaseValue( + this.nutrition * this.saturationModifier * 2 + ); + // ? Decrement the food item amount + player.usingItem.decrement(1); + + // ? If the item will be converted to a item different than air, convert it + if (this.convertsTo != ItemIdentifier.Air) { + const convertedItemStack = new ItemStack(this.convertsTo, 1); + + if (player.usingItem.amount > 0) { + container.addItem(convertedItemStack); + return true; + } + container.setItem(selectedSlot, convertedItemStack); + } + return true; + } + + /** + * ? Necessary methods to make it work lol + */ + public onStartUse(player: Player, cause: ItemUseCause): void {} + + public onStopUse(player: Player, cause: ItemUseCause): void {} +} + +export { ItemFoodComponent }; diff --git a/packages/world/src/components/item/index.ts b/packages/world/src/components/item/index.ts index 892fc894..a84ef82b 100644 --- a/packages/world/src/components/item/index.ts +++ b/packages/world/src/components/item/index.ts @@ -7,3 +7,4 @@ export * from "./enchantable"; export * from "./lore"; export * from "./durability"; export * from "./armor"; +export * from "./food"; diff --git a/packages/world/src/components/player/attribute/hunger.ts b/packages/world/src/components/player/attribute/hunger.ts index 0464c52a..19ac0a48 100644 --- a/packages/world/src/components/player/attribute/hunger.ts +++ b/packages/world/src/components/player/attribute/hunger.ts @@ -44,6 +44,9 @@ class PlayerHungerComponent extends EntityAttributeComponent { this.tickTimer++; // difficulty modifier + // Exhaust player if its running (Temporary) + if (this.entity.isSprinting) this.exhaust(0.05); + // Reset tick timer after 4 seconds if (this.tickTimer >= 80) this.tickTimer = 0; if (this.tickTimer == 0) { diff --git a/packages/world/src/effect/absorption.ts b/packages/world/src/effect/absorption.ts new file mode 100644 index 00000000..c6c5bd48 --- /dev/null +++ b/packages/world/src/effect/absorption.ts @@ -0,0 +1,29 @@ +import { Color, EffectType } from "@serenityjs/protocol"; + +import { Effect } from "./effect"; + +import type { Entity } from "../entity"; + +class AbsorptionEffect extends Effect { + public effectType: EffectType = EffectType.Absorption; + public color: Color = new Color(255, 37, 82, 165); + + public onTick?(entity: T): void; + + public onAdd?(entity: T): void { + if (!entity.isPlayer()) return; + const playerAbsorption = entity.getComponent("minecraft:absorption"); + + if (this.amplifier * 4 == playerAbsorption.getCurrentValue()) return; + playerAbsorption.setCurrentValue(this.amplifier * 4, true); + } + + public onRemove?(entity: T): void { + if (!entity.isPlayer()) return; + const playerAbsorption = entity.getComponent("minecraft:absorption"); + + playerAbsorption.resetToDefaultValue(); + } +} + +export { AbsorptionEffect }; diff --git a/packages/world/src/player/player.ts b/packages/world/src/player/player.ts index 85d2d5e0..9c99b58d 100644 --- a/packages/world/src/player/player.ts +++ b/packages/world/src/player/player.ts @@ -66,7 +66,8 @@ import { EntityHasGravityComponent, EntityBreathingComponent, PlayerExperienceLevelComponent, - PlayerExperienceComponent + PlayerExperienceComponent, + PlayerAbsorptionComponent } from "../components"; import { ItemStack } from "../item"; @@ -377,6 +378,28 @@ class Player extends Entity { super.kill(); } + /** + * Querys if the player is hungry + * @returns The player is hungry + */ + + public isHungry(): boolean { + if (!this.hasComponent("minecraft:player.hunger")) return false; + const hungerComponent = this.getComponent("minecraft:player.hunger"); + return hungerComponent.isHungry; + } + + /** + * Exhausts the player decreasing food over time + * @param amount The exhaustion amount + */ + public exhaust(amount: number): void { + if (!this.hasComponent("minecraft:player.hunger")) return; + const hungerComponent = this.getComponent("minecraft:player.hunger"); + + hungerComponent.exhaust(amount); + } + /** * Despawns the player from the world. * @param player The player to despawn the player from. @@ -769,3 +792,4 @@ PlayerChunkRenderingComponent.register(type); PlayerEntityRenderingComponent.register(type); PlayerExperienceLevelComponent.register(type); PlayerExperienceComponent.register(type); +PlayerAbsorptionComponent.register(type); diff --git a/packages/world/src/types/components/entity.ts b/packages/world/src/types/components/entity.ts index 757c5bc8..cfb12685 100644 --- a/packages/world/src/types/components/entity.ts +++ b/packages/world/src/types/components/entity.ts @@ -1,4 +1,5 @@ import type { + PlayerAbsorptionComponent, EntityAlwaysShowNametagComponent, EntityArmorComponent, EntityBoundingHeightComponent, @@ -23,8 +24,8 @@ import type { */ interface EntityAttributeComponents { "minecraft:health": EntityHealthComponent; + "minecraft:absorption": PlayerAbsorptionComponent; } - /** * The metadata components of an entity. */ diff --git a/packages/world/src/types/components/item.ts b/packages/world/src/types/components/item.ts index b97eacc3..25de8258 100644 --- a/packages/world/src/types/components/item.ts +++ b/packages/world/src/types/components/item.ts @@ -3,6 +3,7 @@ import type { ItemArmorComponent, ItemDurabilityComponent, ItemEnchantableComponent, + ItemFoodComponent, ItemLoreComponent, ItemNametagComponent } from "../../components"; @@ -13,6 +14,7 @@ interface ItemComponents { "minecraft:lore": ItemLoreComponent; "minecraft:durability": ItemDurabilityComponent; "minecraft:armor": ItemArmorComponent; + "minecraft:food": ItemFoodComponent; } export { ItemComponents };