473 lines
11 KiB
Go
473 lines
11 KiB
Go
// 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]
|
||
}
|