1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-18 02:16:23 -05:00
OpenDiablo2/d2game/d2player/inventory_grid.go

284 lines
7.1 KiB
Go
Raw Normal View History

package d2player
import (
"errors"
"fmt"
Removing d2datadict singletons (#738) * Remove weapons, armor, misc, itemCommon, itemTyps datadict singletons - removed loader calls from d2app - removed the HeroObjects singleton from `d2core/d2inventory` - added an InventoryItemFactory in d2inventory - package-level functions that use data records are now methods of the InventoryItemFactory - renamed ItemGenerator in d2item to ItemFactory - package-level functions that use records are now methods of ItemFactory - d2map.MapEntityFactory now has an item factory instance for creating items - fixed a bug in unique item record loader where it loaded an empty record - added a PlayerStateFactory for creating a player state (uses the asset manager) - updated the test inventory/equipment code in d2player to handle errors from the ItemFactory - character select and character creation screens have a player state and inventory item factory - updated item tests to use the item factory * minor edit * Removed d2datadict.Experience singleton added a HeroStatsFactory, much like the other factories. The factory gets an asset manager reference in order to use data records. * removed d2datadict.AutoMagic singleton * removed d2datadict.AutoMap singleton * removed d2datadict.BodyLocations singleton * removed d2datadict.Books singleton * Removed singletons for level records - removed loader calls in d2app - changed type references from d2datadict to d2records - added a `MapGenerator` in d2mapgen which uses thew asset manager and map engine - package-level map generation functions are now MapGenerator methods - `d2datadict.GetLevelDetails(id int)` is now a method of the RecordManager * remove SkillCalc and MissileCalc singletons * Removed CharStats and ItemStatCost singletons - added an ItemStatFactory which uses the asset manager to create stats - package-level functions for stats in d2item are now StatFactory methods - changed type references from d2datadict to d2records - `d2player.GetAllPlayerStates` is now a method of the `PlayerStateFactory` * Removed DkillDesc and Skills singletons from d2datadict - removed loader calls from d2app - diablo2stats.Stat instances are given a reference to the factory for doing record lookups * update the stats test to use mock a asset manager and stat factory * fixed diablo2stats tests and diablo2item tests * removed CompCodes singleton from d2datadict * remove cubemain singleton from d2datadict * removed DifficultyLevels singleton from d2datadict * removed ElemTypes singleton from d2datadict * removed events.go loader from d2datadict (was unused) * removed Gems singleton from d2datadict * removed Hireling and Inventory singletons from d2datadict * removed MagicPrefix and MagicSuffix singletons from d2datadict * removed ItemRatios singleton from d2datadict * removed Missiles singleton from d2datadict * removed MonModes singleton * Removed all monster and npc singletons from d2datadict - MapStamp instances now get a reference to their factory for doing record lookups * removed SoundEntry and SoundEnviron singletons from d2datadict
2020-09-20 17:52:01 -04:00
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// 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
const (
fmtFlippyFile = "/data/global/items/inv%s.dc6"
)
// InventoryItem is an interface for an items that can be placed in the inventory grid
type InventoryItem interface {
InventoryGridSize() (width int, height int)
GetItemCode() string
InventoryGridSlot() (x, y int)
SetInventoryGridSlot(x, y int)
GetItemDescription() []string
}
2020-09-12 16:25:09 -04:00
var errorInventoryFull = errors.New("inventory full")
// NewItemGrid creates a new ItemGrid instance
func NewItemGrid(asset *d2asset.AssetManager,
ui *d2ui.UIManager,
l d2util.LogLevel,
Removing d2datadict singletons (#738) * Remove weapons, armor, misc, itemCommon, itemTyps datadict singletons - removed loader calls from d2app - removed the HeroObjects singleton from `d2core/d2inventory` - added an InventoryItemFactory in d2inventory - package-level functions that use data records are now methods of the InventoryItemFactory - renamed ItemGenerator in d2item to ItemFactory - package-level functions that use records are now methods of ItemFactory - d2map.MapEntityFactory now has an item factory instance for creating items - fixed a bug in unique item record loader where it loaded an empty record - added a PlayerStateFactory for creating a player state (uses the asset manager) - updated the test inventory/equipment code in d2player to handle errors from the ItemFactory - character select and character creation screens have a player state and inventory item factory - updated item tests to use the item factory * minor edit * Removed d2datadict.Experience singleton added a HeroStatsFactory, much like the other factories. The factory gets an asset manager reference in order to use data records. * removed d2datadict.AutoMagic singleton * removed d2datadict.AutoMap singleton * removed d2datadict.BodyLocations singleton * removed d2datadict.Books singleton * Removed singletons for level records - removed loader calls in d2app - changed type references from d2datadict to d2records - added a `MapGenerator` in d2mapgen which uses thew asset manager and map engine - package-level map generation functions are now MapGenerator methods - `d2datadict.GetLevelDetails(id int)` is now a method of the RecordManager * remove SkillCalc and MissileCalc singletons * Removed CharStats and ItemStatCost singletons - added an ItemStatFactory which uses the asset manager to create stats - package-level functions for stats in d2item are now StatFactory methods - changed type references from d2datadict to d2records - `d2player.GetAllPlayerStates` is now a method of the `PlayerStateFactory` * Removed DkillDesc and Skills singletons from d2datadict - removed loader calls from d2app - diablo2stats.Stat instances are given a reference to the factory for doing record lookups * update the stats test to use mock a asset manager and stat factory * fixed diablo2stats tests and diablo2item tests * removed CompCodes singleton from d2datadict * remove cubemain singleton from d2datadict * removed DifficultyLevels singleton from d2datadict * removed ElemTypes singleton from d2datadict * removed events.go loader from d2datadict (was unused) * removed Gems singleton from d2datadict * removed Hireling and Inventory singletons from d2datadict * removed MagicPrefix and MagicSuffix singletons from d2datadict * removed ItemRatios singleton from d2datadict * removed Missiles singleton from d2datadict * removed MonModes singleton * Removed all monster and npc singletons from d2datadict - MapStamp instances now get a reference to their factory for doing record lookups * removed SoundEntry and SoundEnviron singletons from d2datadict
2020-09-20 17:52:01 -04:00
record *d2records.InventoryRecord) *ItemGrid {
grid := record.Grid
itemGrid := &ItemGrid{
remove d2asset singleton (#726) * export d2asset singleton * add *d2asset.AssetManager to d2app - d2app now has a reference to an asset manager which it will use for loading - added asset loader methods to the asset manager - functions in d2asset are now wrappers for asset manager methods * add asset manager reference to audio provider - d2app asset manager reference is now passed to audio provider - asset manager is created in main.go for now to pass into audio provider - CreateSoundEffect is now a method, no longer exported, uses the asset manager reference * d2app passes asset manager refence to map engine test * in d2asset, all calls to LoadFile replaced with call to Singleton.Loadfile * blizzard intro and credits screen - d2app passes reference to the asset manager to these screens * asset manager for d2map - adding MapStampFactory, takes an asset manager reference - embedded MapStampFactory into the MapEngine - LoadStamp is now a method of the MapStampFactory * d2asset: removed LoadFileStream, LoadFile, and FileExists * d2gui changes - singleton now has an asset manager reference - calls to d2asset loader functions removed - createButton is now a method of LayoutManager - moved LayoutEntry to its own file * map entity factory - Map engine has an embedded map entity factory - Map stamp factory gets a reference to the map engine's entity factory - Stamps are given a reference to the map engine entity factory when created - Character select gets a map entity factory - Embedded the stamp factory into the MapEngine * asset manager for d2ui - d2ui is passed an asset manager reference when created - all calls to d2asset loader functions in d2ui now refer to the asset manager - d2gamescreen gets a ui manager when created - help overlay is now passed a ui manager when created * d2gamescreen + d2player: asset manager references added an asset manager reference to - inventory panel + inventory grid - mini panel - game controls - help overlay - character select - main menu - select hero class - hero stats panel * Removed d2asset.LoadAnimation all references to this function have been replaced with calls to the asset manager method * adding asset to help overlay, bugfix for 4d59c91 * Removed d2asset.LoadFont and d2asset.LoadAnimationWithEffect all references to these have been replaced with calls to the asset manager methods * MapRenderer now gets an asset manager reference * removed d2asset.LoadPalette all references have been replaced with calls to an asset manager instance * merged d2object with d2mapentity d2object was only being used to create objects in the map, so the provider function is now a method of the map entity factory. calls to d2asset have been removed. * removed d2asset.LoadComposite all calls are now made to the asset manager method * removed d2asset singleton all singleton references have been removed, a single instance of the asset manager is passed around the entire app * rename Initialize to NewAssetManager
2020-09-12 16:51:30 -04:00
asset: asset,
uiManager: ui,
width: grid.Box.Width,
height: grid.Box.Height,
originX: grid.Box.Left,
originY: grid.Box.Top + (grid.Rows * cellPadding),
slotSize: grid.CellWidth,
sprites: make(map[string]*d2ui.Sprite),
equipmentSlots: genEquipmentSlotsMap(record),
}
itemGrid.Logger = d2util.NewLogger()
itemGrid.Logger.SetLevel(l)
itemGrid.Logger.SetPrefix(logPrefix)
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
*d2util.Logger
}
// 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) {
screenX = g.originX + slotX*g.slotSize
screenY = g.originY + slotY*g.slotSize
return screenX, screenY
}
// 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) {
slotX = (screenX - g.originX) / g.slotSize
slotY = (screenY - g.originY) / g.slotSize
return slotX, slotY
}
// GetSlot returns the inventory item at a given slot (can return nil)
func (g *ItemGrid) GetSlot(x, y int) InventoryItem {
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
}
// 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) {
var curItem = g.equipmentSlots[slot]
curItem.item = item
g.equipmentSlots[slot] = curItem
}
// 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
var err error
for _, item := range items {
if g.add(item) {
added++
} else {
2020-09-12 16:25:09 -04:00
err = errorInventoryFull
break
}
}
g.Load(items...)
return added, err
}
func (g *ItemGrid) loadItem(item InventoryItem) {
if _, exists := g.sprites[item.GetItemCode()]; !exists {
var itemSprite *d2ui.Sprite
imgPath := fmt.Sprintf(fmtFlippyFile, item.GetItemCode())
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)
if err != nil {
g.Error("Failed to load sprite, error: " + err.Error())
}
g.sprites[item.GetItemCode()] = itemSprite
}
}
// 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)
}
for _, eq := range g.equipmentSlots {
if eq.item != nil {
g.loadItem(eq.item)
}
}
}
// 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)
return true
}
}
return false
}
// canFit loops over all items to determine if any other items would overlap the given position.
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
}
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
}
// Set an inventory item at the given grid coordinate
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, y int, item InventoryItem) {
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
for _, compItem := range g.items {
if compItem == item {
continue
}
g.items[n] = compItem
n++
}
g.items = g.items[:n]
}
func (g *ItemGrid) renderItem(item InventoryItem, target d2interface.Surface, x, y int) {
itemSprite := g.sprites[item.GetItemCode()]
if itemSprite != nil {
itemSprite.SetPosition(x, y)
itemSprite.GetCurrentFrameSize()
itemSprite.Render(target)
}
}
// Render the item grid to the given surface
func (g *ItemGrid) Render(target d2interface.Surface) {
g.renderInventoryItems(target)
g.renderEquippedItems(target)
}
func (g *ItemGrid) renderInventoryItems(target d2interface.Surface) {
for _, item := range g.items {
itemSprite := g.sprites[item.GetItemCode()]
slotX, slotY := g.SlotToScreen(item.InventoryGridSlot())
_, h := itemSprite.GetCurrentFrameSize()
slotY += h
g.renderItem(item, target, slotX, slotY)
}
}
func (g *ItemGrid) renderEquippedItems(target d2interface.Surface) {
for _, eq := range g.equipmentSlots {
if eq.item == nil {
continue
}
itemSprite := g.sprites[eq.item.GetItemCode()]
itemWidth, itemHeight := itemSprite.GetCurrentFrameSize()
2021-02-02 06:00:31 -05:00
// nolint:gomnd // 1/2 ov width
x := eq.x + ((eq.width - itemWidth) / 2)
2021-02-02 06:00:31 -05:00
// nolint:gomnd // 1/2 ov height
y := eq.y - ((eq.height - itemHeight) / 2)
g.renderItem(eq.item, target, x, y)
}
}