// Package inventory 背包模型与规则表(场景/物品与背包.md §3、UI与输入.md §4)。 // // 规则表(CanPlace / ClickLeft / ClickRight / ShiftMove / Merge)为**单一来源**: // 客户端与服务器共用同一实现,防双端不一致(多人同步.md §5)。 package inventory import ( "mc/internal/item" ) // 背包槽位常量(UI还原.md §3.3、物品与背包.md §3)。 const ( HotbarSize = 9 MainSize = 27 StorageSize = HotbarSize + MainSize // 0–35,Add/Remove 只写这里 ArmorSize = 4 OffhandSize = 1 TotalSize = StorageSize + ArmorSize + OffhandSize // 41 SlotHelmet = 36 SlotChest = 37 SlotLegs = 38 SlotBoots = 39 SlotOffhand = 40 MaxCraftBulk = 64 // Shift 批量合成单次上限(工作台与合成表.md §5) ) // Inventory 背包(主线程所有权,无需锁——架构.md §4)。 type Inventory struct { Slots []item.Stack // 41 槽:0–8 热键栏、9–35 主区、36–39 盔甲、40 副手 Selected int // 热键栏选中索引 0–8 reg *item.Registry } // New 创建空背包。 func New(reg *item.Registry) *Inventory { return &Inventory{Slots: make([]item.Stack, TotalSize), reg: reg} } // CanMerge 两栈是否可合并:同名、同耐久、数量未满(物品与背包.md §3)。 func CanMerge(a, b item.Stack) bool { if a.Empty() || b.Empty() || a.Name != b.Name { return false } if a.Durability != b.Durability { return false } return int(a.Count)+int(b.Count) <= 127 } // Merge 合并两栈:返回合并结果与剩余量(调用方负责写回槽位)。 func Merge(limit uint8, a, b item.Stack) (item.Stack, item.Stack) { total := int(a.Count) + int(b.Count) if total <= int(limit) { a.Count = uint8(total) return a, item.Stack{} } a.Count = limit b.Count = uint8(total - int(limit)) return a, b } // CanPlace 槽位是否允许放入该栈(盔甲/副手按 Equip 限制,其余存储槽均可)。 func CanPlace(reg *item.Registry, slot int, s item.Stack) bool { if s.Empty() { return true } want := "" switch slot { case SlotHelmet: want = "helmet" case SlotChest: want = "chestplate" case SlotLegs: want = "leggings" case SlotBoots: want = "boots" case SlotOffhand: want = "offhand" default: return true } return item.EquipOf(s.Name, reg) == want } // ClickLeft 左键:拿起整栈 / 放下 / 同物品合并 / 交换。canPlace 为 false 时不改槽。 func ClickLeft(slot, held item.Stack, limit uint8, canPlace bool) (item.Stack, item.Stack) { switch { case held.Empty(): return item.Stack{}, slot case !canPlace: return slot, held case slot.Empty(): return held, item.Stack{} case slot.Name == held.Name: merged, remain := Merge(limit, slot, held) return merged, remain default: return held, slot } } // ClickRight 右键:空手拿一半(向上取整);手持时向空槽或同物品放 1 个。 func ClickRight(slot, held item.Stack, limit uint8, canPlace bool) (item.Stack, item.Stack) { if held.Empty() { if slot.Empty() { return slot, held } take := (int(slot.Count) + 1) / 2 held = slot held.Count = uint8(take) slot.Count -= uint8(take) if slot.Count == 0 { slot = item.Stack{} } return slot, held } if !canPlace { return slot, held } one := held one.Count = 1 switch { case slot.Empty(): slot = one held.Count-- case slot.Name == held.Name && slot.Count < limit: slot.Count++ held.Count-- default: return slot, held } if held.Count == 0 { held = item.Stack{} } return slot, held } func (inv *Inventory) storageEnd() int { if len(inv.Slots) < StorageSize { return len(inv.Slots) } return StorageSize } // Add 把物品栈放入存储区(0–35;先合并再空槽),返回剩余。 func (inv *Inventory) Add(s item.Stack) item.Stack { if s.Empty() { return s } limit := s.Limit(inv.reg) end := inv.storageEnd() for i := 0; i < end; i++ { if inv.Slots[i].Empty() { continue } if inv.Slots[i].Name == s.Name && inv.Slots[i].Durability == s.Durability && inv.Slots[i].Count < limit { merged, remain := Merge(limit, inv.Slots[i], s) inv.Slots[i] = merged s = remain if s.Empty() { return s } } } for i := 0; i < end; i++ { if inv.Slots[i].Empty() { inv.Slots[i] = s return item.Stack{} } } return s } // Fits 存储区能否完全放下该栈(不修改)。 func (inv *Inventory) Fits(s item.Stack) bool { if s.Empty() { return true } limit := s.Limit(inv.reg) left := int(s.Count) end := inv.storageEnd() for i := 0; i < end; i++ { st := inv.Slots[i] if st.Empty() { return true } if st.Name == s.Name && st.Durability == s.Durability && st.Count < limit { room := int(limit) - int(st.Count) if room >= left { return true } left -= room } } return false } // Remove 从存储区移除指定物品 count 个;不足返回 false。 func (inv *Inventory) Remove(name string, count int) bool { if count <= 0 { return false } have := 0 end := inv.storageEnd() for i := 0; i < end; i++ { if inv.Slots[i].Name == name { have += int(inv.Slots[i].Count) } } if have < count { return false } left := count for i := 0; i < end; i++ { if inv.Slots[i].Name != name { continue } if int(inv.Slots[i].Count) <= left { left -= int(inv.Slots[i].Count) inv.Slots[i] = item.Stack{} } else { inv.Slots[i].Count -= uint8(left) left = 0 } if left == 0 { return true } } return true } // CountItem 存储区中该物品数量(不含盔甲/副手)。 func (inv *Inventory) CountItem(name string) int { n := 0 end := inv.storageEnd() for i := 0; i < end; i++ { if inv.Slots[i].Name == name { n += int(inv.Slots[i].Count) } } return n } // TakeOne 从存储区取出 1 个指定物品;不足返回 false。 func (inv *Inventory) TakeOne(name string) bool { return inv.Remove(name, 1) } // ClickSlot 对背包槽执行左/右键(含 CanPlace)。 func (inv *Inventory) ClickSlot(idx int, held item.Stack, right bool) item.Stack { if idx < 0 || idx >= len(inv.Slots) { return held } cur := inv.Slots[idx] limit := held.Limit(inv.reg) if held.Empty() { limit = cur.Limit(inv.reg) } ok := CanPlace(inv.reg, idx, held) var ns, nh item.Stack if right { ns, nh = ClickRight(cur, held, limit, ok) } else { ns, nh = ClickLeft(cur, held, limit, ok) } inv.Slots[idx] = ns return nh } // ShiftFromInv 物品栏内 Shift:主区 ↔ 热键栏;盔甲/副手 → 存储区。 func (inv *Inventory) ShiftFromInv(idx int) bool { if idx < 0 || idx >= len(inv.Slots) || inv.Slots[idx].Empty() { return false } src := inv.Slots[idx] inv.Slots[idx] = item.Stack{} var rem item.Stack switch { case idx < HotbarSize: rem = addRange(inv, src, HotbarSize, StorageSize) case idx < StorageSize: rem = addRange(inv, src, 0, HotbarSize) default: rem = inv.Add(src) } if !rem.Empty() && CanPlace(inv.reg, idx, rem) { inv.Slots[idx] = rem } return rem.Empty() || rem.Count != src.Count } // ShiftIntoStorage 把一栈移入存储区(工作台网格 → 背包)。 func (inv *Inventory) ShiftIntoStorage(s item.Stack) item.Stack { return inv.Add(s) } // ShiftIntoSlots 把一栈尽量放入 dest 切片的空/可合并槽(背包 → 合成网格)。 func ShiftIntoSlots(reg *item.Registry, dest []item.Stack, s item.Stack) item.Stack { if s.Empty() { return s } limit := s.Limit(reg) for i := range dest { if dest[i].Empty() || dest[i].Name != s.Name || dest[i].Durability != s.Durability { continue } merged, remain := Merge(limit, dest[i], s) dest[i] = merged s = remain if s.Empty() { return s } } for i := range dest { if dest[i].Empty() { dest[i] = s return item.Stack{} } } return s } func addRange(inv *Inventory, s item.Stack, from, to int) item.Stack { if s.Empty() || from < 0 || to > len(inv.Slots) || from >= to { return s } limit := s.Limit(inv.reg) for i := from; i < to; i++ { if inv.Slots[i].Empty() || inv.Slots[i].Name != s.Name || inv.Slots[i].Durability != s.Durability { continue } if inv.Slots[i].Count >= limit { continue } merged, remain := Merge(limit, inv.Slots[i], s) inv.Slots[i] = merged s = remain if s.Empty() { return s } } for i := from; i < to; i++ { if inv.Slots[i].Empty() { inv.Slots[i] = s return item.Stack{} } } return s } // SlotPtr 槽位指针(拖拽均分用);越界返回 nil。 func SlotPtr(slots []item.Stack, i int) *item.Stack { if i < 0 || i >= len(slots) { return nil } return &slots[i] } // CanAcceptDrag 拖拽目标:空槽或同物品未满。 func CanAcceptDrag(reg *item.Registry, slot, held item.Stack, canPlace bool) bool { if held.Empty() || !canPlace { return false } if slot.Empty() { return true } return slot.Name == held.Name && slot.Durability == held.Durability && slot.Count < held.Limit(reg) } // SpreadEven 把 held 等分到 dest(已过滤可放入的槽)。返回剩余手持。 // dest 为槽指针列表;n=0 不改。 func SpreadEven(reg *item.Registry, held item.Stack, dest []*item.Stack) item.Stack { if held.Empty() || len(dest) == 0 { return held } limit := held.Limit(reg) n := len(dest) each := int(held.Count) / n if each < 1 { return held } if each > int(limit) { each = int(limit) } left := int(held.Count) for _, p := range dest { if p == nil || left <= 0 { continue } put := each if !p.Empty() { room := int(limit) - int(p.Count) if put > room { put = room } } if put <= 0 { continue } if p.Empty() { *p = held p.Count = uint8(put) } else { p.Count += uint8(put) } left -= put } if left <= 0 { return item.Stack{} } held.Count = uint8(left) return held } // SpreadOne 向单槽放 1 个(右键拖)。 func SpreadOne(reg *item.Registry, slot, held item.Stack, canPlace bool) (item.Stack, item.Stack) { return ClickRight(slot, held, held.Limit(reg), canPlace) } // Gather 从 sources 把与 name 同类的物品聚到 held,直到堆叠上限。 func Gather(reg *item.Registry, held item.Stack, name string, sources []item.Stack) (item.Stack, []item.Stack) { if name == "" { return held, sources } if held.Empty() { held = item.Stack{Name: name, Count: 0} } if held.Name != name { return held, sources } limit := held.Limit(reg) for i := range sources { if sources[i].Empty() || sources[i].Name != name || sources[i].Durability != held.Durability { continue } need := int(limit) - int(held.Count) if need <= 0 { break } take := int(sources[i].Count) if take > need { take = need } held.Count += uint8(take) if take >= int(sources[i].Count) { sources[i] = item.Stack{} } else { sources[i].Count -= uint8(take) } } if held.Count == 0 { held = item.Stack{} } return held, sources } // GatherStorage 从存储区 0–35 聚堆到 held。 func (inv *Inventory) GatherStorage(held item.Stack, name string) item.Stack { end := inv.storageEnd() src := inv.Slots[:end] held, src = Gather(inv.reg, held, name, src) copy(inv.Slots[:end], src) return held } // SelectedStack 返回选中槽(只读)。 func (inv *Inventory) SelectedStack() item.Stack { if inv.Selected < 0 || inv.Selected >= HotbarSize { return item.Stack{} } return inv.Slots[inv.Selected] }