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() }