diff --git a/d2core/d2item/diablo2item/item.go b/d2core/d2item/diablo2item/item.go index 39742563..b9454582 100644 --- a/d2core/d2item/diablo2item/item.go +++ b/d2core/d2item/diablo2item/item.go @@ -2,6 +2,9 @@ package diablo2item import ( "fmt" + "math/rand" + "sort" + "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" @@ -9,7 +12,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" - "math/rand" ) // PropertyPool is used for separating properties by their source @@ -67,6 +69,9 @@ type Item struct { attributes *itemAttributes + GridX int + GridY int + sockets []*d2item.Item // there will be checks for handling the craziness this might entail } @@ -695,6 +700,8 @@ func (i *Item) GetStatStrings() []string { stats = diablo2stats.NewStatList(stats...).ReduceStats().Stats() } + sort.Slice(stats, func(i, j int) bool { return stats[i].Priority() > stats[j].Priority() }) + for statIdx := range stats { statStr := stats[statIdx].String() if statStr != "" { @@ -786,3 +793,144 @@ func findMatchingAffixes( return result } + +// these functions are to satisfy the inventory grid item interface +func (i *Item) GetInventoryItemName() string { + return i.Label() +} + +func (i *Item) GetInventoryItemType() d2enum.InventoryItemType { + typeCode := i.TypeRecord().Code + + armorEquiv := d2datadict.ItemEquivalenciesByTypeCode["armo"] + weaponEquiv := d2datadict.ItemEquivalenciesByTypeCode["weap"] + + for idx := range armorEquiv { + if armorEquiv[idx].Code == typeCode { + return d2enum.InventoryItemTypeArmor + } + } + + for idx := range weaponEquiv { + if weaponEquiv[idx].Code == typeCode { + return d2enum.InventoryItemTypeWeapon + } + } + + return d2enum.InventoryItemTypeItem +} + +func (i *Item) InventoryGridSize() (int, int) { + r := i.CommonRecord() + return r.InventoryWidth, r.InventoryHeight +} + +func (i *Item) GetItemCode() string { + return i.CommonRecord().Code +} + +func (i *Item) Serialize() []byte { + panic("item serialization not yet implemented") +} + +func (i *Item) InventoryGridSlot() (x, y int) { + return i.GridX, i.GridY +} + +func (i *Item) SetInventoryGridSlot(x, y int) { + i.GridX, i.GridY = x, y +} + +func (i *Item) GetInventoryGridSize() (int, int) { + return i.GridX, i.GridY +} + +func (i *Item) Identify() *Item { + i.attributes.identitified = true + return i +} + +// from a string table +const ( + reqNotMet = "ItemStats1a" // "Requirements not met", + unidentified = "ItemStats1b" // "Unidentified", + charges = "ItemStats1c" // "Charges:", + durability = "ItemStats1d" // "Durability:", + reqStrength = "ItemStats1e" // "Required Strength:", + reqDexterity = "ItemStats1f" // "Required Dexterity:", + damage = "ItemStats1g" // "Damage:", + defense = "ItemStats1h" // "Defense:", + quantity = "ItemStats1i" // "Quantity:", + of = "ItemStats1j" // "of", + to = "to" // "to" + damage1h = "ItemStats1l" // "One-Hand Damage:", + damage2h = "ItemStats1m" // "Two-Hand Damage:", + damageThrow = "ItemStats1n" // "Throw Damage:", + damageSmite = "ItemStats1o" // "Smite Damage:", + reqLevel = "ItemStats1p" // "Required Level:", +) + +func (i *Item) GetItemDescription() []string { + lines := make([]string, 0) + + common := i.CommonRecord() + + lines = append(lines, i.Label()) + + str := "" + + if common.MinAC > 0 { + min, max := common.MinAC, common.MaxAC + str = fmt.Sprintf("%s %v %s %v", d2common.TranslateString(defense), min, d2common.TranslateString(to), max) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.MinDamage > 0 { + min, max := common.MinDamage, common.MaxDamage + str = fmt.Sprintf("%s %v %s %v", d2common.TranslateString(damage1h), min, d2common.TranslateString(to), max) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.Min2HandDamage > 0 { + min, max := common.Min2HandDamage, common.Max2HandDamage + str = fmt.Sprintf("%s %v %s %v", d2common.TranslateString(damage2h), min, d2common.TranslateString(to), max) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.MinMissileDamage > 0 { + min, max := common.MinMissileDamage, common.MaxMissileDamage + str = fmt.Sprintf("%s %v %s %v", d2common.TranslateString(damageThrow), min, d2common.TranslateString(to), max) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.RequiredStrength > 1 { + str = fmt.Sprintf("%s %v", d2common.TranslateString(reqStrength), common.RequiredStrength) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.RequiredDexterity > 1 { + str = fmt.Sprintf("%s %v", d2common.TranslateString(reqDexterity), common.RequiredDexterity) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + if common.RequiredLevel > 1 { + str = fmt.Sprintf("%s %v", d2common.TranslateString(reqLevel), common.RequiredLevel) + str = d2ui.ColorTokenize(str, d2ui.ColorTokenWhite) + lines = append(lines, str) + } + + statStrings := i.GetStatStrings() + + for _, statStr := range statStrings { + str = d2ui.ColorTokenize(statStr, d2ui.ColorTokenBlue) + lines = append(lines, str) + } + + return lines +} diff --git a/d2core/d2stats/stat.go b/d2core/d2stats/stat.go index 8a8a40ae..398e3625 100644 --- a/d2core/d2stats/stat.go +++ b/d2core/d2stats/stat.go @@ -11,4 +11,5 @@ type Stat interface { String() string Values() []StatValue SetValues(...StatValue) + Priority() int } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index e20898db..3501a2c7 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -279,6 +279,8 @@ func (g *GameControls) OnMouseMove(event d2interface.MouseMoveEvent) bool { mx, my := event.X(), event.Y() g.lastMouseX = mx g.lastMouseY = my + g.inventory.lastMouseX = mx + g.inventory.lastMouseY = my for i := range g.actionableRegions { // Mouse over a game control element @@ -454,8 +456,8 @@ func (g *GameControls) Render(target d2interface.Surface) error { } } - g.inventory.Render(target) g.heroStatsPanel.Render(target) + g.inventory.Render(target) width, height := target.GetSize() offset := 0 diff --git a/d2game/d2player/inventory.go b/d2game/d2player/inventory.go index 4ccd2c6c..d560f20e 100644 --- a/d2game/d2player/inventory.go +++ b/d2game/d2player/inventory.go @@ -1,28 +1,42 @@ package d2player import ( + "image/color" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2item/diablo2item" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) type Inventory struct { - frame *d2ui.Sprite - panel *d2ui.Sprite - grid *ItemGrid - originX int - originY int - isOpen bool + frame *d2ui.Sprite + panel *d2ui.Sprite + grid *ItemGrid + + hoverLabel *d2ui.Label + hoverX, hoverY int + hovering bool + + originX, originY int + lastMouseX, lastMouseY int + + isOpen bool } func NewInventory(record *d2datadict.InventoryRecord) *Inventory { + + hoverLabel := d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic) + hoverLabel.Alignment = d2gui.HorizontalAlignCenter + return &Inventory{ - grid: NewItemGrid(record), - originX: record.Panel.Left, + grid: NewItemGrid(record), + originX: record.Panel.Left, + hoverLabel: &hoverLabel, // originY: record.Panel.Top, originY: 0, // expansion data has these all offset by +60 ... } @@ -51,23 +65,23 @@ func (g *Inventory) Load() { animation, _ = d2asset.LoadAnimation(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) g.panel, _ = d2ui.LoadSprite(animation) items := []InventoryItem{ - d2inventory.GetWeaponItemByCode("wnd"), - d2inventory.GetWeaponItemByCode("sst"), - d2inventory.GetWeaponItemByCode("jav"), - d2inventory.GetArmorItemByCode("buc"), - d2inventory.GetWeaponItemByCode("clb"), + diablo2item.NewItem("kit", "Crimson", "of the Bat", "of Frost").Identify(), + diablo2item.NewItem("rin", "Steel", "of Shock").Identify(), + diablo2item.NewItem("jav").Identify(), + diablo2item.NewItem("buc").Identify(), + //diablo2item.NewItem("Arctic Furs", "qui"), // TODO: Load the player's actual items } - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftArm, d2inventory.GetWeaponItemByCode("wnd")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightArm, d2inventory.GetArmorItemByCode("buc")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotHead, d2inventory.GetArmorItemByCode("crn")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotTorso, d2inventory.GetArmorItemByCode("plt")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLegs, d2inventory.GetArmorItemByCode("vbt")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotBelt, d2inventory.GetArmorItemByCode("vbl")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotGloves, d2inventory.GetArmorItemByCode("lgl")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftHand, d2inventory.GetMiscItemByCode("rin")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightHand, d2inventory.GetMiscItemByCode("rin")) - g.grid.ChangeEquippedSlot(d2enum.EquippedSlotNeck, d2inventory.GetMiscItemByCode("amu")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftArm, diablo2item.NewItem("wnd")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightArm, diablo2item.NewItem("buc")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotHead, diablo2item.NewItem("crn")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotTorso, diablo2item.NewItem("plt")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLegs, diablo2item.NewItem("vbt")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotBelt, diablo2item.NewItem("vbl")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotGloves, diablo2item.NewItem("lgl")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftHand, diablo2item.NewItem("rin")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightHand, diablo2item.NewItem("rin")) + g.grid.ChangeEquippedSlot(d2enum.EquippedSlotNeck, diablo2item.NewItem("amu")) // TODO: Load the player's actual items g.grid.Add(items...) } @@ -215,5 +229,71 @@ func (g *Inventory) Render(target d2interface.Surface) error { g.grid.Render(target) + hovering := false + + for idx := range g.grid.items { + item := g.grid.items[idx] + ix, iy := g.grid.SlotToScreen(item.InventoryGridSlot()) + iw, ih := g.grid.sprites[item.GetItemCode()].GetCurrentFrameSize() + mx, my := g.lastMouseX, g.lastMouseY + hovering = hovering || ((mx > ix) && (mx < ix+iw) && (my > iy) && (my < iy+ih)) + + if hovering { + if !g.hovering { + // set the initial hover coordinates + // this is so that moving mouse doesnt move the description + g.hoverX, g.hoverY = mx, my + } + + g.renderItemDescription(target, item) + break + } + } + + g.hovering = hovering + return nil } + +func (g *Inventory) renderItemDescription(target d2interface.Surface, i InventoryItem) { + lines := i.GetItemDescription() + + maxW, maxH := 0, 0 + _, iy := g.grid.SlotToScreen(i.InventoryGridSlot()) + + for idx := range lines { + w, h := g.hoverLabel.GetTextMetrics(lines[idx]) + + if maxW < w { + maxW = w + } + + maxH += h + } + + halfW, halfH := maxW/2, maxH/2 + centerX, centerY := g.hoverX, iy - halfH + + if (centerX + halfW) > 800 { + centerX = 800 - halfW + } + + if (centerY + halfH) > 600 { + centerY = 600 - halfH + } + + target.PushTranslation(centerX, centerY) + target.PushTranslation(-halfW, -halfH) + target.DrawRect(maxW, maxH, color.RGBA{0, 0, 0, uint8(200)}) + target.PushTranslation(halfW, 0) + + for idx := range lines { + g.hoverLabel.SetText(lines[idx]) + _, h := g.hoverLabel.GetTextMetrics(lines[idx]) + g.hoverLabel.Render(target) + target.PushTranslation(0, h) + } + + target.PopN(len(lines)) + target.PopN(3) +} diff --git a/d2game/d2player/inventory_grid.go b/d2game/d2player/inventory_grid.go index 56310bcc..fff92c63 100644 --- a/d2game/d2player/inventory_grid.go +++ b/d2game/d2player/inventory_grid.go @@ -3,9 +3,9 @@ package d2player import ( "errors" "fmt" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "log" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -23,8 +23,9 @@ const cellPadding = 1 type InventoryItem interface { InventoryGridSize() (width int, height int) GetItemCode() string - InventoryGridSlot() (x int, y int) - SetInventoryGridSlot(x int, y int) + InventoryGridSlot() (x, y int) + SetInventoryGridSlot(x, y int) + GetItemDescription() []string } var ErrorInventoryFull = errors.New("inventory full") @@ -44,6 +45,7 @@ type ItemGrid struct { func NewItemGrid(record *d2datadict.InventoryRecord) *ItemGrid { grid := record.Grid + return &ItemGrid{ width: grid.Box.Width, height: grid.Box.Height, @@ -58,16 +60,18 @@ func NewItemGrid(record *d2datadict.InventoryRecord) *ItemGrid { func (g *ItemGrid) SlotToScreen(slotX int, slotY int) (screenX int, screenY int) { screenX = g.originX + slotX*g.slotSize screenY = g.originY + slotY*g.slotSize + return screenX, screenY } func (g *ItemGrid) ScreenToSlot(screenX int, screenY int) (slotX int, slotY int) { slotX = (screenX - g.originX) / g.slotSize slotY = (screenY - g.originY) / g.slotSize + return slotX, slotY } -func (g *ItemGrid) GetSlot(x int, y int) InventoryItem { +func (g *ItemGrid) GetSlot(x, y int) InventoryItem { for _, item := range g.items { slotX, slotY := item.InventoryGridSlot() width, height := item.InventoryGridSize() @@ -90,6 +94,7 @@ func (g *ItemGrid) ChangeEquippedSlot(slot d2enum.EquippedSlot, item InventoryIt // Returns a count of the number of items which could be inserted. func (g *ItemGrid) Add(items ...InventoryItem) (int, error) { added := 0 + var err error for _, item := range items { @@ -115,14 +120,17 @@ func (g *ItemGrid) loadItem(item InventoryItem) { fmt.Sprintf("/data/global/items/inv%s.dc6", item.GetItemCode()), d2resource.PaletteSky, ) + if err != nil { log.Printf("failed to load sprite for item (%s): %v", item.GetItemCode(), err) return } + itemSprite, err = d2ui.LoadSprite(animation) if err != nil { log.Printf("Failed to load sprite, error: " + err.Error()) } + g.sprites[item.GetItemCode()] = itemSprite } } @@ -132,6 +140,7 @@ func (g *ItemGrid) Load(items ...InventoryItem) { for _, item := range items { g.loadItem(item) } + for _, eq := range g.equipmentSlots { if eq.item != nil { g.loadItem(eq.item) @@ -149,6 +158,7 @@ func (g *ItemGrid) add(item InventoryItem) bool { } g.set(x, y, item) + return true } } @@ -157,7 +167,7 @@ func (g *ItemGrid) add(item InventoryItem) bool { } // canFit loops over all items to determine if any other items would overlap the given position. -func (g *ItemGrid) canFit(x int, y int, item InventoryItem) bool { +func (g *ItemGrid) canFit(x, y int, item InventoryItem) bool { insertWidth, insertHeight := item.InventoryGridSize() if x+insertWidth > g.width || y+insertHeight > g.height { return false @@ -178,15 +188,17 @@ func (g *ItemGrid) canFit(x int, y int, item InventoryItem) bool { return true } -func (g *ItemGrid) Set(x int, y int, item InventoryItem) error { +func (g *ItemGrid) Set(x, y int, item InventoryItem) error { if !g.canFit(x, y, item) { return fmt.Errorf("can not set item (%s) to position (%v, %v)", item.GetItemCode(), x, y) } + g.set(x, y, item) + return nil } -func (g *ItemGrid) set(x int, y int, item InventoryItem) { +func (g *ItemGrid) set(x, y int, item InventoryItem) { item.SetInventoryGridSlot(x, y) g.items = append(g.items, item) @@ -196,10 +208,12 @@ func (g *ItemGrid) set(x int, y int, item InventoryItem) { // Remove does an in place filter to remove the element from the slice of items. func (g *ItemGrid) Remove(item InventoryItem) { n := 0 + for _, compItem := range g.items { if compItem == item { continue } + g.items[n] = compItem n++ } @@ -207,7 +221,7 @@ func (g *ItemGrid) Remove(item InventoryItem) { g.items = g.items[:n] } -func (g *ItemGrid) renderItem(item InventoryItem, target d2interface.Surface, x int, y int) { +func (g *ItemGrid) renderItem(item InventoryItem, target d2interface.Surface, x, y int) { itemSprite := g.sprites[item.GetItemCode()] if itemSprite != nil { itemSprite.SetPosition(x, y) @@ -226,19 +240,23 @@ func (g *ItemGrid) renderInventoryItems(target d2interface.Surface) { itemSprite := g.sprites[item.GetItemCode()] slotX, slotY := g.SlotToScreen(item.InventoryGridSlot()) _, h := itemSprite.GetCurrentFrameSize() - slotY = slotY + h + slotY += h + g.renderItem(item, target, slotX, slotY) } } func (g *ItemGrid) renderEquippedItems(target d2interface.Surface) { for _, eq := range g.equipmentSlots { - if eq.item != nil { - itemSprite := g.sprites[eq.item.GetItemCode()] - itemWidth, itemHeight := itemSprite.GetCurrentFrameSize() - var x = eq.x + ((eq.width - itemWidth) / 2) - var y = eq.y - ((eq.height - itemHeight) / 2) - g.renderItem(eq.item, target, x, y) + if eq.item == nil { + continue } + + itemSprite := g.sprites[eq.item.GetItemCode()] + itemWidth, itemHeight := itemSprite.GetCurrentFrameSize() + x := eq.x + ((eq.width - itemWidth) / 2) + y := eq.y - ((eq.height - itemHeight) / 2) + + g.renderItem(eq.item, target, x, y) } }