Files
mc/server/item/inventory/armour.go
T

250 lines
8.1 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
package inventory
import (
"fmt"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/enchantment"
"github.com/df-mc/dragonfly/server/world"
"math"
"math/rand/v2"
)
// Armour represents an inventory for armour. It has 4 slots, one for a helmet, chestplate, leggings and
// boots respectively. NewArmour() must be used to create a valid armour inventory.
// Armour inventories, like normal Inventories, are safe for concurrent usage.
type Armour struct {
inv *Inventory
}
// NewArmour returns an armour inventory that is ready to be used. The zero value of an inventory.Armour is
// not valid for usage.
// The function passed is called when a slot is changed. It may be nil to not call anything.
func NewArmour(f func(slot int, before, after item.Stack)) *Armour {
inv := New(4, f)
inv.validator = canAddArmour
return &Armour{inv: inv}
}
// canAddArmour checks if the item passed can be worn as armour in the slot passed.
func canAddArmour(s item.Stack, slot int) bool {
if s.Empty() {
return true
}
switch slot {
case 0:
if h, ok := s.Item().(item.HelmetType); ok {
return h.Helmet()
}
case 1:
if c, ok := s.Item().(item.ChestplateType); ok {
return c.Chestplate()
}
case 2:
if l, ok := s.Item().(item.LeggingsType); ok {
return l.Leggings()
}
case 3:
if b, ok := s.Item().(item.BootsType); ok {
return b.Boots()
}
}
return false
}
// Set sets all individual pieces of armour in one go. It is equivalent to calling SetHelmet, SetChestplate, SetLeggings
// and SetBoots sequentially.
func (a *Armour) Set(helmet, chestplate, leggings, boots item.Stack) {
a.SetHelmet(helmet)
a.SetChestplate(chestplate)
a.SetLeggings(leggings)
a.SetBoots(boots)
}
// SetHelmet sets the item stack passed as the helmet in the inventory.
func (a *Armour) SetHelmet(helmet item.Stack) {
_ = a.inv.SetItem(0, helmet)
}
// Helmet returns the item stack set as helmet in the inventory.
func (a *Armour) Helmet() item.Stack {
i, _ := a.inv.Item(0)
return i
}
// SetChestplate sets the item stack passed as the chestplate in the inventory.
func (a *Armour) SetChestplate(chestplate item.Stack) {
_ = a.inv.SetItem(1, chestplate)
}
// Chestplate returns the item stack set as chestplate in the inventory.
func (a *Armour) Chestplate() item.Stack {
i, _ := a.inv.Item(1)
return i
}
// SetLeggings sets the item stack passed as the leggings in the inventory.
func (a *Armour) SetLeggings(leggings item.Stack) {
_ = a.inv.SetItem(2, leggings)
}
// Leggings returns the item stack set as leggings in the inventory.
func (a *Armour) Leggings() item.Stack {
i, _ := a.inv.Item(2)
return i
}
// SetBoots sets the item stack passed as the boots in the inventory.
func (a *Armour) SetBoots(boots item.Stack) {
_ = a.inv.SetItem(3, boots)
}
// Boots returns the item stack set as boots in the inventory.
func (a *Armour) Boots() item.Stack {
i, _ := a.inv.Item(3)
return i
}
// DamageReduction returns the amount of damage that is reduced by the Armour for
// an amount of damage and damage source. The value returned takes into account
// the armour itself and its enchantments.
func (a *Armour) DamageReduction(dmg float64, src world.DamageSource) float64 {
var (
original = dmg
defencePoints, toughness float64
enchantments []item.Enchantment
)
for _, it := range a.Items() {
enchantments = append(enchantments, it.Enchantments()...)
if armour, ok := it.Item().(item.Armour); ok {
defencePoints += armour.DefencePoints()
toughness += armour.Toughness()
}
}
dmg -= dmg * enchantment.ProtectionFactor(src, enchantments)
if src.ReducedByArmour() {
// Armour in Bedrock edition reduces the damage taken by 4% for each effective armour point. Effective
// armour point decreases as damage increases, with 1 point lost for every 2 HP of damage. The defence
// reduction is decreased by the toughness armour value. Effective armour points will at minimum be 20% of
// armour points.
dmg -= dmg * 0.04 * math.Max(defencePoints*0.2, defencePoints-dmg/(2+toughness/4))
}
return original - dmg
}
// HighestEnchantmentLevel looks up the highest level of an item.EnchantmentType
// that any of the Armour items have and returns it, or 0 if none of the items
// have the enchantment.
func (a *Armour) HighestEnchantmentLevel(t item.EnchantmentType) int {
lvl := 0
for _, it := range a.Items() {
if e, ok := it.Enchantment(t); ok && e.Level() > lvl {
lvl = e.Level()
}
}
return lvl
}
// DamageFunc is a function that deals d damage points to an item stack s. The
// resulting item.Stack is returned. Depending on the game mode of a player,
// damage may not be dealt at all.
type DamageFunc func(s item.Stack, d int) item.Stack
// Damage deals damage (hearts) to Armour. The resulting item damage depends on the
// dmg passed and the DamageFunc used.
func (a *Armour) Damage(dmg float64, f DamageFunc) {
armourDamage := int(math.Max(math.Floor(dmg/4), 1))
for slot, it := range a.Slots() {
_ = a.inv.SetItem(slot, f(it, armourDamage))
}
}
// ThornsDamage checks if any of the Armour items are enchanted with thorns. If
// this is the case and the thorns enchantment activates (15% chance per level),
// a random Armour piece is damaged. The damage to be dealt to the attacker is
// returned.
func (a *Armour) ThornsDamage(f DamageFunc) float64 {
slots := a.Slots()
dmg := 0.0
for _, i := range slots {
thorns, _ := i.Enchantment(enchantment.Thorns)
if level := float64(thorns.Level()); rand.Float64() < level*0.15 {
// 15%/level chance of thorns activation per item. Total damage from
// normal thorns armour (max thorns III) should never exceed 4.0 in
// total.
dmg = math.Min(dmg+float64(1+rand.IntN(4)), 4.0)
}
}
if highest := a.HighestEnchantmentLevel(enchantment.Thorns); highest > 10 {
// When we find an armour piece with thorns XI or above, the logic
// changes: We have to find the armour piece with the highest level
// of thorns and subtract 10 from its level to calculate the final
// damage.
dmg = float64(highest - 10)
}
if dmg > 0 {
// Deal 2 damage to one random thorns item. Bedrock Edition and Java Edition
// both have different behaviour here and neither seem to match the expected
// behaviour. Java Edition deals 2 damage to a random thorns item for every
// thorns armour item worn, while Bedrock Edition deals 1 additional damage
// for every thorns item and another 2 for every thorns item when it
// activates.
slot := rand.IntN(len(slots))
_ = a.Inventory().SetItem(slot, f(slots[slot], 2))
}
return dmg
}
// KnockBackResistance returns the combined knock back resistance of all Armour
// items. A value of 0 means normal knock back force, while a value of 1 means
// all knock back is ignored.
func (a *Armour) KnockBackResistance() float64 {
resistance := 0.0
for _, i := range a.Items() {
if a, ok := i.Item().(item.Armour); ok {
resistance += a.KnockBackResistance()
}
}
return resistance
}
// Slots returns all items (including) air of the armour inventory in the order of helmet, chestplate, leggings,
// boots.
func (a *Armour) Slots() []item.Stack {
return a.inv.Slots()
}
// Items returns a slice of all non-empty armour items equipped.
func (a *Armour) Items() []item.Stack {
return a.inv.Items()
}
// Clear clears the armour inventory, removing all items currently present.
func (a *Armour) Clear() []item.Stack {
return a.inv.Clear()
}
// String converts the armour to a readable string representation.
func (a *Armour) String() string {
return fmt.Sprintf("(helmet: %v, chestplate: %v, leggings: %v, boots: %v)", a.Helmet(), a.Chestplate(), a.Leggings(), a.Boots())
}
// Inventory returns the underlying Inventory instance.
func (a *Armour) Inventory() *Inventory {
return a.inv
}
// Handle assigns a Handler to an Armour inventory so that its methods are called for the respective events. Nil may be
// passed to set the default NopHandler.
// Handle is the equivalent of calling (*Armour).Inventory().H.
func (a *Armour) Handle(h Handler) {
a.inv.Handle(h)
}
// Close closes the armour inventory, removing the slot change function.
func (a *Armour) Close() error {
return a.inv.Close()
}