2019-12-28 23:32:53 -05:00
|
|
|
package d2player
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-01-31 23:18:11 -05:00
|
|
|
|
2020-09-20 17:52:01 -04:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
|
|
|
|
2020-06-29 00:41:58 -04:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
|
|
|
2020-02-01 18:55:56 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
2020-01-31 23:18:11 -05:00
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
2020-11-18 16:02:49 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
2019-12-28 23:32:53 -05:00
|
|
|
)
|
|
|
|
|
2020-07-11 11:25:34 -04:00
|
|
|
// images for 1x1 grid tile items (rings and stuff) are 28x28 pixel
|
|
|
|
// however, the grid cells are 29x29 pixels, this is for padding
|
|
|
|
// for each row in inventory, we need to account for this padding
|
|
|
|
const cellPadding = 1
|
|
|
|
|
2020-10-25 10:21:14 -04:00
|
|
|
const (
|
|
|
|
fmtFlippyFile = "/data/global/items/inv%s.dc6"
|
|
|
|
)
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// InventoryItem is an interface for an items that can be placed in the inventory grid
|
2019-12-28 23:32:53 -05:00
|
|
|
type InventoryItem interface {
|
|
|
|
InventoryGridSize() (width int, height int)
|
2020-06-13 18:32:09 -04:00
|
|
|
GetItemCode() string
|
2020-08-03 13:44:00 -04:00
|
|
|
InventoryGridSlot() (x, y int)
|
|
|
|
SetInventoryGridSlot(x, y int)
|
|
|
|
GetItemDescription() []string
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
|
|
|
|
2020-09-12 16:25:09 -04:00
|
|
|
var errorInventoryFull = errors.New("inventory full")
|
2019-12-28 23:32:53 -05:00
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// NewItemGrid creates a new ItemGrid instance
|
2020-11-18 16:02:49 -05:00
|
|
|
func NewItemGrid(asset *d2asset.AssetManager,
|
|
|
|
ui *d2ui.UIManager,
|
|
|
|
l d2util.LogLevel,
|
2020-09-20 17:52:01 -04:00
|
|
|
record *d2records.InventoryRecord) *ItemGrid {
|
2020-07-11 11:25:34 -04:00
|
|
|
grid := record.Grid
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2020-11-18 16:02:49 -05:00
|
|
|
itemGrid := &ItemGrid{
|
2020-09-12 16:51:30 -04:00
|
|
|
asset: asset,
|
2020-08-06 10:30:23 -04:00
|
|
|
uiManager: ui,
|
2020-07-11 11:25:34 -04:00
|
|
|
width: grid.Box.Width,
|
|
|
|
height: grid.Box.Height,
|
|
|
|
originX: grid.Box.Left,
|
|
|
|
originY: grid.Box.Top + (grid.Rows * cellPadding),
|
|
|
|
slotSize: grid.CellWidth,
|
2020-06-23 14:12:30 -04:00
|
|
|
sprites: make(map[string]*d2ui.Sprite),
|
2020-07-11 11:25:34 -04:00
|
|
|
equipmentSlots: genEquipmentSlotsMap(record),
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
2020-11-18 16:02:49 -05:00
|
|
|
|
2020-11-22 00:36:59 -05:00
|
|
|
itemGrid.Logger = d2util.NewLogger()
|
|
|
|
itemGrid.Logger.SetLevel(l)
|
|
|
|
itemGrid.Logger.SetPrefix(logPrefix)
|
2020-11-18 16:02:49 -05:00
|
|
|
|
|
|
|
return itemGrid
|
|
|
|
}
|
|
|
|
|
|
|
|
// ItemGrid is a reusable grid for use with player and merchant inventory.
|
|
|
|
// Handles layout and rendering item icons based on code.
|
|
|
|
type ItemGrid struct {
|
|
|
|
asset *d2asset.AssetManager
|
|
|
|
uiManager *d2ui.UIManager
|
|
|
|
items []InventoryItem
|
|
|
|
equipmentSlots map[d2enum.EquippedSlot]EquipmentSlot
|
|
|
|
width int
|
|
|
|
height int
|
|
|
|
originX int
|
|
|
|
originY int
|
|
|
|
sprites map[string]*d2ui.Sprite
|
|
|
|
slotSize int
|
|
|
|
|
2020-11-22 00:36:59 -05:00
|
|
|
*d2util.Logger
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// SlotToScreen translates slot coordinates to screen coordinates
|
2020-09-12 16:25:09 -04:00
|
|
|
func (g *ItemGrid) SlotToScreen(slotX, slotY int) (screenX, screenY int) {
|
2019-12-28 23:32:53 -05:00
|
|
|
screenX = g.originX + slotX*g.slotSize
|
|
|
|
screenY = g.originY + slotY*g.slotSize
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
return screenX, screenY
|
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// ScreenToSlot translates screen coordinates to slot coordinates
|
2020-09-12 16:25:09 -04:00
|
|
|
func (g *ItemGrid) ScreenToSlot(screenX, screenY int) (slotX, slotY int) {
|
2019-12-28 23:32:53 -05:00
|
|
|
slotX = (screenX - g.originX) / g.slotSize
|
|
|
|
slotY = (screenY - g.originY) / g.slotSize
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
return slotX, slotY
|
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// GetSlot returns the inventory item at a given slot (can return nil)
|
2020-08-03 13:44:00 -04:00
|
|
|
func (g *ItemGrid) GetSlot(x, y int) InventoryItem {
|
2019-12-28 23:32:53 -05:00
|
|
|
for _, item := range g.items {
|
|
|
|
slotX, slotY := item.InventoryGridSlot()
|
|
|
|
width, height := item.InventoryGridSize()
|
|
|
|
|
|
|
|
if x >= slotX && x < slotX+width && y >= slotY && y < slotY+height {
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// ChangeEquippedSlot sets the item for an equipment slot
|
2020-07-08 19:08:07 -04:00
|
|
|
func (g *ItemGrid) ChangeEquippedSlot(slot d2enum.EquippedSlot, item InventoryItem) {
|
2020-06-23 14:12:30 -04:00
|
|
|
var curItem = g.equipmentSlots[slot]
|
|
|
|
curItem.item = item
|
|
|
|
g.equipmentSlots[slot] = curItem
|
|
|
|
}
|
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
// Add places a given set of items into the first available slots.
|
|
|
|
// Returns a count of the number of items which could be inserted.
|
|
|
|
func (g *ItemGrid) Add(items ...InventoryItem) (int, error) {
|
|
|
|
added := 0
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
var err error
|
|
|
|
|
|
|
|
for _, item := range items {
|
|
|
|
if g.add(item) {
|
|
|
|
added++
|
|
|
|
} else {
|
2020-09-12 16:25:09 -04:00
|
|
|
err = errorInventoryFull
|
2019-12-28 23:32:53 -05:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
g.Load(items...)
|
|
|
|
|
|
|
|
return added, err
|
|
|
|
}
|
|
|
|
|
2020-06-23 14:12:30 -04:00
|
|
|
func (g *ItemGrid) loadItem(item InventoryItem) {
|
|
|
|
if _, exists := g.sprites[item.GetItemCode()]; !exists {
|
|
|
|
var itemSprite *d2ui.Sprite
|
2019-12-28 23:32:53 -05:00
|
|
|
|
2020-10-25 10:21:14 -04:00
|
|
|
imgPath := fmt.Sprintf(fmtFlippyFile, item.GetItemCode())
|
2020-10-22 01:12:06 -04:00
|
|
|
|
Decouple asset manager from renderer (#730)
* improve AssetManager implementation
Notable changes are:
* removed the individual managers inside of d2asset, only one asset manager
* AssetManager now has caches for the types of files it loads
* created a type for TextDictionary (the txt file structs)
* fixed a file path bug in d2loader Source
* fixed a asset stream bug in d2loader Asset
* d2loader.Loader now needs a d2config.Config on creation (for resolving locale files)
* updated the mpq file in d2asset test data, added test case for "sub-directory"
* added a Data method to d2asset.Asset. The data is cached on first full read.
* renamed ArchiveDataStream to DataStream in d2interface
* moved palette utility func out of d2asset and into d2util
* bugfix for MacOS mpq loader issue
* lint fixes, added data caching to filesystem asset
* adding comment for mpq asset close
* Decouple d2asset from d2render
Notable changes in d2common:
* d2dcc.Load now fully decodes the dcc and stores the directions/frames in the dcc struct
* un-exported dcc.decodeDirection, it is only used in d2dcc
* removed font interface from d2interface, we only have one font implementation
* added `Renderer` method to d2interface.Surface, animations use this to bind to a renderer and create surfaces as they need
* added `BindRenderer` method to animation interface
Notable changes in d2common/d2asset:
* **d2asset.NewAssetManager only needs to be passed a d2config.Config**, it is decoupled from d2render
* exported Animation
* Animation implementation binds to the renderer to create surfaces only on the first time it is rendered
* font, dcc, dc6 initialization logic moved out of asset_manager.go
* for dc6 and dcc animations, the process of decoding and creating render surfaces has been broken into different methods
* the d2asset.Font struct now stores font table data for initialization purposes
Notable changes in d2core/d2render:
* Surfaces store a renderer reference, this allows animations to bind to the renderer and create a surface just-in-time
**These last changes should have been a separate PR, sorry.**
Notable changes in d2core/d2ui:
* ui.NewSprite now handles creating an animation internally, only needs image and palette path as arguments
Notable Changes in d2game:
Because of the change in d2ui, all instances of this code pattern...
```golang
animation, err := screen.asset.LoadAnimation(imgPath, palettePath)
sprite, err := screen.ui.NewSprite(animation)
```
... becomes this ...
```golang
sprite, err := screen.ui.NewSprite(imgPath, palettePath)
```
2020-09-14 17:31:45 -04:00
|
|
|
itemSprite, err := g.uiManager.NewSprite(imgPath, d2resource.PaletteSky)
|
2020-06-23 14:12:30 -04:00
|
|
|
if err != nil {
|
2020-11-22 00:36:59 -05:00
|
|
|
g.Error("Failed to load sprite, error: " + err.Error())
|
2020-06-23 14:12:30 -04:00
|
|
|
}
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2020-06-13 18:32:09 -04:00
|
|
|
g.sprites[item.GetItemCode()] = itemSprite
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
2020-06-23 14:12:30 -04:00
|
|
|
}
|
2019-12-28 23:32:53 -05:00
|
|
|
|
2020-06-23 14:12:30 -04:00
|
|
|
// Load reads the inventory sprites for items into local cache for rendering.
|
|
|
|
func (g *ItemGrid) Load(items ...InventoryItem) {
|
|
|
|
for _, item := range items {
|
|
|
|
g.loadItem(item)
|
|
|
|
}
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2020-06-23 14:12:30 -04:00
|
|
|
for _, eq := range g.equipmentSlots {
|
|
|
|
if eq.item != nil {
|
|
|
|
g.loadItem(eq.item)
|
|
|
|
}
|
|
|
|
}
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Walk from top left to bottom right until a position large enough to hold the item is found.
|
|
|
|
// This is inefficient but simplifies the storage. At most a hundred or so cells will be looped, so impact is minimal.
|
|
|
|
func (g *ItemGrid) add(item InventoryItem) bool {
|
|
|
|
for y := 0; y < g.height; y++ {
|
|
|
|
for x := 0; x < g.width; x++ {
|
|
|
|
if !g.canFit(x, y, item) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
g.set(x, y, item)
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// canFit loops over all items to determine if any other items would overlap the given position.
|
2020-08-03 13:44:00 -04:00
|
|
|
func (g *ItemGrid) canFit(x, y int, item InventoryItem) bool {
|
2019-12-28 23:32:53 -05:00
|
|
|
insertWidth, insertHeight := item.InventoryGridSize()
|
|
|
|
if x+insertWidth > g.width || y+insertHeight > g.height {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, compItem := range g.items {
|
|
|
|
slotX, slotY := compItem.InventoryGridSlot()
|
|
|
|
compWidth, compHeight := compItem.InventoryGridSize()
|
|
|
|
|
|
|
|
if x+insertWidth >= slotX &&
|
|
|
|
x < slotX+compWidth &&
|
|
|
|
y+insertHeight >= slotY &&
|
|
|
|
y < slotY+compHeight {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// Set an inventory item at the given grid coordinate
|
2020-08-03 13:44:00 -04:00
|
|
|
func (g *ItemGrid) Set(x, y int, item InventoryItem) error {
|
2019-12-28 23:32:53 -05:00
|
|
|
if !g.canFit(x, y, item) {
|
2020-06-13 18:32:09 -04:00
|
|
|
return fmt.Errorf("can not set item (%s) to position (%v, %v)", item.GetItemCode(), x, y)
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
g.set(x, y, item)
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-03 13:44:00 -04:00
|
|
|
func (g *ItemGrid) set(x, y int, item InventoryItem) {
|
2019-12-28 23:32:53 -05:00
|
|
|
item.SetInventoryGridSlot(x, y)
|
|
|
|
g.items = append(g.items, item)
|
|
|
|
|
|
|
|
g.Load(item)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove does an in place filter to remove the element from the slice of items.
|
|
|
|
func (g *ItemGrid) Remove(item InventoryItem) {
|
|
|
|
n := 0
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
for _, compItem := range g.items {
|
|
|
|
if compItem == item {
|
|
|
|
continue
|
|
|
|
}
|
2020-08-03 13:44:00 -04:00
|
|
|
|
2019-12-28 23:32:53 -05:00
|
|
|
g.items[n] = compItem
|
|
|
|
n++
|
|
|
|
}
|
|
|
|
|
|
|
|
g.items = g.items[:n]
|
|
|
|
}
|
|
|
|
|
2020-08-03 13:44:00 -04:00
|
|
|
func (g *ItemGrid) renderItem(item InventoryItem, target d2interface.Surface, x, y int) {
|
2020-06-23 14:12:30 -04:00
|
|
|
itemSprite := g.sprites[item.GetItemCode()]
|
|
|
|
if itemSprite != nil {
|
|
|
|
itemSprite.SetPosition(x, y)
|
|
|
|
itemSprite.GetCurrentFrameSize()
|
2020-11-11 09:05:04 -05:00
|
|
|
itemSprite.Render(target)
|
2020-06-23 14:12:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-22 02:41:21 -04:00
|
|
|
// Render the item grid to the given surface
|
2020-06-29 00:41:58 -04:00
|
|
|
func (g *ItemGrid) Render(target d2interface.Surface) {
|
2020-06-23 14:12:30 -04:00
|
|
|
g.renderInventoryItems(target)
|
|
|
|
g.renderEquippedItems(target)
|
|
|
|
}
|
2019-12-28 23:32:53 -05:00
|
|
|
|
2020-06-29 00:41:58 -04:00
|
|
|
func (g *ItemGrid) renderInventoryItems(target d2interface.Surface) {
|
2020-06-23 14:12:30 -04:00
|
|
|
for _, item := range g.items {
|
2020-06-13 18:32:09 -04:00
|
|
|
itemSprite := g.sprites[item.GetItemCode()]
|
2019-12-28 23:32:53 -05:00
|
|
|
slotX, slotY := g.SlotToScreen(item.InventoryGridSlot())
|
|
|
|
_, h := itemSprite.GetCurrentFrameSize()
|
2020-08-03 13:44:00 -04:00
|
|
|
slotY += h
|
|
|
|
|
2020-06-23 14:12:30 -04:00
|
|
|
g.renderItem(item, target, slotX, slotY)
|
|
|
|
}
|
|
|
|
}
|
2019-12-28 23:32:53 -05:00
|
|
|
|
2020-06-29 00:41:58 -04:00
|
|
|
func (g *ItemGrid) renderEquippedItems(target d2interface.Surface) {
|
2020-06-23 14:12:30 -04:00
|
|
|
for _, eq := range g.equipmentSlots {
|
2020-08-03 13:44:00 -04:00
|
|
|
if eq.item == nil {
|
|
|
|
continue
|
2020-06-23 14:12:30 -04:00
|
|
|
}
|
2020-08-03 13:44:00 -04:00
|
|
|
|
|
|
|
itemSprite := g.sprites[eq.item.GetItemCode()]
|
|
|
|
itemWidth, itemHeight := itemSprite.GetCurrentFrameSize()
|
2021-02-02 06:00:31 -05:00
|
|
|
// nolint:gomnd // 1/2 ov width
|
2020-08-03 13:44:00 -04:00
|
|
|
x := eq.x + ((eq.width - itemWidth) / 2)
|
2021-02-02 06:00:31 -05:00
|
|
|
// nolint:gomnd // 1/2 ov height
|
2020-08-03 13:44:00 -04:00
|
|
|
y := eq.y - ((eq.height - itemHeight) / 2)
|
|
|
|
|
|
|
|
g.renderItem(eq.item, target, x, y)
|
2019-12-28 23:32:53 -05:00
|
|
|
}
|
|
|
|
}
|