Files
ngzz-mc/internal/inventory/inventory.go
2026-09-19 14:21:08 +08:00

473 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]
}