1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-19 10:56:07 -05:00

Initial inventory handling (#270)

Add struct to display inventory panel.
Add struct to handle inventory and merchant grids.

Hook up `i` key to toggle inventory panel.
This commit is contained in:
nicholas-eden 2019-12-28 20:32:53 -08:00 committed by Tim Sarbin
parent c01bedaedf
commit d6769975cd
9 changed files with 547 additions and 50 deletions

View File

@ -1,13 +1,13 @@
package d2core
type CharacterEquipment struct {
Head InventoryItemArmor // Head
Torso InventoryItemArmor // TR
Legs InventoryItemArmor // Legs
RightArm InventoryItemArmor // RA
LeftArm InventoryItemArmor // LA
LeftHand InventoryItemWeapon // LH
RightHand InventoryItemWeapon // RH
Shield InventoryItemArmor // SH
Head *InventoryItemArmor // Head
Torso *InventoryItemArmor // TR
Legs *InventoryItemArmor // Legs
RightArm *InventoryItemArmor // RA
LeftArm *InventoryItemArmor // LA
LeftHand *InventoryItemWeapon // LH
RightHand *InventoryItemWeapon // RH
Shield *InventoryItemArmor // SH
// S1-S8?
}

View File

@ -99,5 +99,5 @@ func (v *Game) Advance(tickTime float64) {
rx, ry := v.mapEngine.WorldToOrtho(v.hero.AnimatedEntity.LocationX/5, v.hero.AnimatedEntity.LocationY/5)
v.mapEngine.MoveCameraTo(rx, ry)
v.gameControls.Move(tickTime)
v.gameControls.Update(tickTime)
}

View File

@ -20,16 +20,16 @@ func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment Chara
Mode: d2enum.AnimationModePlayerNeutral.String(),
Base: "/data/global/chars",
Token: heroType.GetToken(),
Class: equipment.RightHand.GetWeaponClass(),
SH: equipment.Shield.GetItemCode(),
Class: equipment.RightHand.WeaponClass(),
SH: equipment.Shield.ItemCode(),
// TODO: Offhand class?
HD: equipment.Head.GetArmorClass(),
TR: equipment.Torso.GetArmorClass(),
LG: equipment.Legs.GetArmorClass(),
RA: equipment.RightArm.GetArmorClass(),
LA: equipment.LeftArm.GetArmorClass(),
RH: equipment.RightHand.GetItemCode(),
LH: equipment.LeftHand.GetItemCode(),
HD: equipment.Head.ArmorClass(),
TR: equipment.Torso.ArmorClass(),
LG: equipment.Legs.ArmorClass(),
RA: equipment.RightArm.ArmorClass(),
LA: equipment.LeftArm.ArmorClass(),
RH: equipment.RightHand.ItemCode(),
LH: equipment.LeftHand.ItemCode(),
}
entity, err := d2render.CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits)
@ -38,7 +38,7 @@ func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment Chara
}
result := &Hero{AnimatedEntity: entity, Equipment: equipment, mode: d2enum.AnimationModePlayerTownNeutral, direction: direction}
result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.GetWeaponClass(), direction)
result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.WeaponClass(), direction)
return result
}

View File

@ -10,17 +10,19 @@ import (
type InventoryItemArmor struct {
inventorySizeX int
inventorySizeY int
inventorySlotX int
inventorySlotY int
itemName string
itemCode string
armorClass string
}
func GetArmorItemByCode(code string) InventoryItemArmor {
func GetArmorItemByCode(code string) *InventoryItemArmor {
result := d2datadict.Armors[code]
if result == nil {
log.Fatalf("Could not find armor entry for code '%s'", code)
}
return InventoryItemArmor{
return &InventoryItemArmor{
inventorySizeX: result.InventoryWidth,
inventorySizeY: result.InventoryHeight,
itemName: result.Name,
@ -29,29 +31,45 @@ func GetArmorItemByCode(code string) InventoryItemArmor {
}
}
func (v InventoryItemArmor) GetArmorClass() string {
if v.itemCode == "" {
func (v *InventoryItemArmor) ArmorClass() string {
if v == nil || v.itemCode == "" {
return "lit"
}
return v.armorClass
}
func (v InventoryItemArmor) GetInventoryItemName() string {
func (v *InventoryItemArmor) InventoryItemName() string {
if v == nil {
return ""
}
return v.itemName
}
func (v InventoryItemArmor) GetInventoryItemType() d2enum.InventoryItemType {
func (v *InventoryItemArmor) InventoryItemType() d2enum.InventoryItemType {
return d2enum.InventoryItemTypeArmor
}
func (v InventoryItemArmor) GetInventoryGridSize() (int, int) {
func (v *InventoryItemArmor) InventoryGridSize() (int, int) {
return v.inventorySizeX, v.inventorySizeY
}
func (v InventoryItemArmor) Serialize() []byte {
func (v *InventoryItemArmor) InventoryGridSlot() (int, int) {
return v.inventorySlotX, v.inventorySlotY
}
func (v *InventoryItemArmor) SetInventoryGridSlot(x int, y int) {
v.inventorySlotX, v.inventorySlotY = x, y
}
func (v *InventoryItemArmor) Serialize() []byte {
return []byte{}
}
func (v InventoryItemArmor) GetItemCode() string {
func (v *InventoryItemArmor) ItemCode() string {
if v == nil {
return ""
}
return v.itemCode
}

View File

@ -10,19 +10,21 @@ import (
type InventoryItemWeapon struct {
inventorySizeX int
inventorySizeY int
inventorySlotX int
inventorySlotY int
itemName string
itemCode string
weaponClass string
weaponClassOffHand string
}
func GetWeaponItemByCode(code string) InventoryItemWeapon {
func GetWeaponItemByCode(code string) *InventoryItemWeapon {
// TODO: Non-normal codes will fail here...
result := d2datadict.Weapons[code]
if result == nil {
log.Fatalf("Could not find weapon entry for code '%s'", code)
}
return InventoryItemWeapon{
return &InventoryItemWeapon{
inventorySizeX: result.InventoryWidth,
inventorySizeY: result.InventoryHeight,
itemName: result.Name,
@ -32,36 +34,50 @@ func GetWeaponItemByCode(code string) InventoryItemWeapon {
}
}
func (v InventoryItemWeapon) GetWeaponClass() string {
if v.itemCode == "" {
func (v *InventoryItemWeapon) WeaponClass() string {
if v == nil || v.itemCode == "" {
return "hth"
}
return v.weaponClass
}
func (v InventoryItemWeapon) GetWeaponClassOffHand() string {
if v.itemCode == "" {
func (v *InventoryItemWeapon) WeaponClassOffHand() string {
if v == nil || v.itemCode == "" {
return ""
}
return v.weaponClassOffHand
}
func (v InventoryItemWeapon) GetInventoryItemName() string {
func (v *InventoryItemWeapon) InventoryItemName() string {
if v == nil {
return ""
}
return v.itemName
}
func (v InventoryItemWeapon) GetInventoryItemType() d2enum.InventoryItemType {
func (v *InventoryItemWeapon) InventoryItemType() d2enum.InventoryItemType {
return d2enum.InventoryItemTypeWeapon
}
func (v InventoryItemWeapon) GetInventoryGridSize() (int, int) {
func (v *InventoryItemWeapon) InventoryGridSize() (int, int) {
return v.inventorySizeX, v.inventorySizeY
}
func (v InventoryItemWeapon) Serialize() []byte {
func (v *InventoryItemWeapon) InventoryGridSlot() (int, int) {
return v.inventorySlotX, v.inventorySlotY
}
func (v *InventoryItemWeapon) SetInventoryGridSlot(x int, y int) {
v.inventorySlotX, v.inventorySlotY = x, y
}
func (v *InventoryItemWeapon) Serialize() []byte {
return []byte{}
}
func (v InventoryItemWeapon) GetItemCode() string {
func (v *InventoryItemWeapon) ItemCode() string {
if v == nil {
return ""
}
return v.itemCode
}

View File

@ -7,11 +7,20 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2render/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2render/d2surface"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/inpututil"
)
type Panel interface {
IsOpen() bool
Toggle()
Open()
Close()
}
type GameControls struct {
hero *d2core.Hero
mapEngine *d2mapengine.MapEngine
inventory *Inventory
// UI
globeSprite *d2render.Sprite
@ -24,10 +33,11 @@ func NewGameControls(hero *d2core.Hero, mapEngine *d2mapengine.MapEngine) *GameC
return &GameControls{
hero: hero,
mapEngine: mapEngine,
inventory: NewInventory(),
}
}
func (g *GameControls) Move(tickTime float64) {
func (g *GameControls) Update(tickTime float64) {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
px, py := g.mapEngine.ScreenToWorld(ebiten.CursorPosition())
@ -58,6 +68,10 @@ func (g *GameControls) Move(tickTime float64) {
g.hero.AnimatedEntity.SetTarget(g.hero.AnimatedEntity.LocationX+moveX, g.hero.AnimatedEntity.LocationY+moveY, 1)
}
if inpututil.IsKeyJustPressed(ebiten.KeyI) {
g.inventory.Toggle()
}
}
func (g *GameControls) Load() {
@ -65,43 +79,46 @@ func (g *GameControls) Load() {
g.mainPanel, _ = d2render.LoadSprite(d2resource.GamePanels, d2resource.PaletteSky)
g.menuButton, _ = d2render.LoadSprite(d2resource.MenuButton, d2resource.PaletteSky)
g.skillIcon, _ = d2render.LoadSprite(d2resource.GenericSkills, d2resource.PaletteSky)
g.inventory.Load()
}
// TODO: consider caching the panels to single image that is reused.
func (g *GameControls) Render(target *d2surface.Surface) {
g.inventory.Render(target)
width, height := target.GetSize()
offset := int(0)
// Left globe holder
g.mainPanel.SetCurrentFrame(0)
w, _ := g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
// Left globe
g.globeSprite.SetCurrentFrame(0)
g.globeSprite.SetPosition(int(offset+28), height-5)
g.globeSprite.SetPosition(offset+28, height-5)
g.globeSprite.Render(target)
offset += w
// Left skill
g.skillIcon.SetCurrentFrame(2)
w, _ = g.skillIcon.GetCurrentFrameSize()
g.skillIcon.SetPosition(int(offset), height)
g.skillIcon.SetPosition(offset, height)
g.skillIcon.Render(target)
offset += w
// Left skill selector
g.mainPanel.SetCurrentFrame(1)
w, _ = g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
offset += w
// Stamina
g.mainPanel.SetCurrentFrame(2)
w, _ = g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
offset += w
@ -114,33 +131,33 @@ func (g *GameControls) Render(target *d2surface.Surface) {
// Potions
g.mainPanel.SetCurrentFrame(3)
w, _ = g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
offset += w
// Right skill selector
g.mainPanel.SetCurrentFrame(4)
w, _ = g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
offset += w
// Right skill
g.skillIcon.SetCurrentFrame(10)
w, _ = g.skillIcon.GetCurrentFrameSize()
g.skillIcon.SetPosition(int(offset), height)
g.skillIcon.SetPosition(offset, height)
g.skillIcon.Render(target)
offset += w
// Right globe holder
g.mainPanel.SetCurrentFrame(5)
w, _ = g.mainPanel.GetCurrentFrameSize()
g.mainPanel.SetPosition(int(offset), height)
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
// Right globe
g.globeSprite.SetCurrentFrame(1)
g.globeSprite.SetPosition(int(offset)+8, height-8)
g.globeSprite.SetPosition(offset+8, height-8)
g.globeSprite.Render(target)
}

133
d2player/inventory.go Normal file
View File

@ -0,0 +1,133 @@
package d2player
import (
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core"
"github.com/OpenDiablo2/OpenDiablo2/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2render/d2surface"
)
type Inventory struct {
frame *d2render.Sprite
panel *d2render.Sprite
grid *ItemGrid
originX int
originY int
isOpen bool
}
func NewInventory() *Inventory {
originX := 400
originY := 0
return &Inventory{
grid: NewItemGrid(10, 4, originX+19, originY+320),
originX: originX,
originY: originY,
}
}
func (g *Inventory) IsOpen() bool {
return g.isOpen
}
func (g *Inventory) Toggle() {
g.isOpen = !g.isOpen
}
func (g *Inventory) Open() {
g.isOpen = true
}
func (g *Inventory) Close() {
g.isOpen = false
}
func (g *Inventory) Load() {
g.frame, _ = d2render.LoadSprite(d2resource.Frame, d2resource.PaletteSky)
g.panel, _ = d2render.LoadSprite(d2resource.InventoryCharacterPanel, d2resource.PaletteSky)
items := []InventoryItem{
d2core.GetWeaponItemByCode("wnd"),
d2core.GetWeaponItemByCode("sst"),
d2core.GetWeaponItemByCode("jav"),
d2core.GetArmorItemByCode("buc"),
d2core.GetWeaponItemByCode("clb"),
}
g.grid.Add(items...)
}
func (g *Inventory) Render(target *d2surface.Surface) {
if !g.isOpen {
return
}
x, y := g.originX, g.originY
// Frame
// Top left
g.frame.SetCurrentFrame(5)
w, h := g.frame.GetCurrentFrameSize()
g.frame.SetPosition(x, y+h)
g.frame.Render(target)
x += w
// Top right
g.frame.SetCurrentFrame(6)
w, h = g.frame.GetCurrentFrameSize()
g.frame.SetPosition(x, y+h)
g.frame.Render(target)
x += w
y += h
// Right
g.frame.SetCurrentFrame(7)
w, h = g.frame.GetCurrentFrameSize()
g.frame.SetPosition(x-w, y+h)
g.frame.Render(target)
y += h
// Bottom right
g.frame.SetCurrentFrame(8)
w, h = g.frame.GetCurrentFrameSize()
g.frame.SetPosition(x-w, y+h)
g.frame.Render(target)
x -= w
// Bottom left
g.frame.SetCurrentFrame(9)
w, h = g.frame.GetCurrentFrameSize()
g.frame.SetPosition(x-w, y+h)
g.frame.Render(target)
x, y = g.originX, g.originY
y += 64
// Panel
// Top left
g.panel.SetCurrentFrame(4)
w, h = g.panel.GetCurrentFrameSize()
g.panel.SetPosition(x, y+h)
g.panel.Render(target)
x += w
// Top right
g.panel.SetCurrentFrame(5)
w, h = g.panel.GetCurrentFrameSize()
g.panel.SetPosition(x, y+h)
g.panel.Render(target)
y += h
// Bottom right
g.panel.SetCurrentFrame(7)
w, h = g.panel.GetCurrentFrameSize()
g.panel.SetPosition(x, y+h)
g.panel.Render(target)
// Bottom left
g.panel.SetCurrentFrame(6)
w, h = g.panel.GetCurrentFrameSize()
g.panel.SetPosition(x-w, y+h)
g.panel.Render(target)
g.grid.Render(target)
}

202
d2player/inventory_grid.go Normal file
View File

@ -0,0 +1,202 @@
package d2player
import (
"errors"
"fmt"
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2render/d2surface"
"log"
)
type InventoryItem interface {
InventoryGridSize() (width int, height int)
ItemCode() string
InventoryGridSlot() (x int, y int)
SetInventoryGridSlot(x int, y int)
}
var ErrorInventoryFull = errors.New("inventory full")
// Reusable grid for use with player and merchant inventory.
// Handles layout and rendering item icons based on code.
type ItemGrid struct {
items []InventoryItem
width int
height int
originX int
originY int
sprites map[string]*d2render.Sprite
slotSize int
}
func NewItemGrid(width int, height int, originX int, originY int) *ItemGrid {
return &ItemGrid{
width: width,
height: height,
originX: originX,
originY: originY,
slotSize: 29,
sprites: make(map[string]*d2render.Sprite),
}
}
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 {
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
}
// 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 {
err = ErrorInventoryFull
break
}
}
g.Load(items...)
return added, err
}
// Load reads the inventory sprites for items into local cache for rendering.
func (g *ItemGrid) Load(items ...InventoryItem) {
var itemSprite *d2render.Sprite
var err error
for _, item := range items {
if _, exists := g.sprites[item.ItemCode()]; exists {
// Already loaded, don't reload.
continue
}
// TODO: Put the pattern into D2Shared
itemSprite, err = d2render.LoadSprite(
fmt.Sprintf("/data/global/items/inv%s.dc6", item.ItemCode()),
d2resource.PaletteSky,
)
if err != nil {
log.Printf("failed to load sprite for item (%s): %v", item.ItemCode(), err)
continue
}
g.sprites[item.ItemCode()] = itemSprite
}
}
// 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 int, 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
}
func (g *ItemGrid) Set(x int, y int, item InventoryItem) error {
if !g.canFit(x, y, item) {
return fmt.Errorf("can not set item (%s) to position (%v, %v)", item.ItemCode(), x, y)
}
g.set(x, y, item)
return nil
}
func (g *ItemGrid) set(x int, 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) Render(target *d2surface.Surface) {
for _, item := range g.items {
if item == nil {
continue
}
itemSprite := g.sprites[item.ItemCode()]
if itemSprite == nil {
// In case it failed to load.
// TODO: fallback to something
continue
}
slotX, slotY := g.SlotToScreen(item.InventoryGridSlot())
_, h := itemSprite.GetCurrentFrameSize()
itemSprite.SetPosition(slotX, slotY+h)
_ = itemSprite.Render(target)
}
}

View File

@ -0,0 +1,111 @@
package d2player
import (
"github.com/stretchr/testify/assert"
"testing"
)
type TestItem struct {
width int
height int
inventorySlotX int
inventorySlotY int
}
func (t *TestItem) InventoryGridSize() (int, int) {
return t.width, t.height
}
func (t *TestItem) ItemCode() string {
return ""
}
func (t *TestItem) InventoryGridSlot() (int, int) {
return t.inventorySlotX, t.inventorySlotY
}
func (t *TestItem) SetInventoryGridSlot(x int, y int) {
t.inventorySlotX, t.inventorySlotY = x, y
}
func NewTestItem(width int, height int) *TestItem {
return &TestItem{width: width, height: height}
}
func TestItemGrid_Add_Basic(t *testing.T) {
grid := NewItemGrid(2, 2, 0, 0)
tl := NewTestItem(1, 1)
tr := NewTestItem(1, 1)
bl := NewTestItem(1, 1)
br := NewTestItem(1, 1)
added, err := grid.Add(tl, tr, bl, br)
assert.Equal(t, 4, added)
assert.NoError(t, err)
assert.Equal(t, tl, grid.GetSlot(0, 0))
assert.Equal(t, tr, grid.GetSlot(1, 0))
assert.Equal(t, bl, grid.GetSlot(0, 1))
assert.Equal(t, br, grid.GetSlot(1, 1))
}
func TestItemGrid_Add_OverflowBasic(t *testing.T) {
grid := NewItemGrid(2, 2, 0, 0)
tl := NewTestItem(1, 1)
tr := NewTestItem(1, 1)
bl := NewTestItem(1, 1)
br := NewTestItem(1, 1)
o := NewTestItem(1, 1)
added, err := grid.Add(tl, tr, bl, br, o)
assert.Equal(t, 4, added)
assert.Error(t, err)
assert.Equal(t, tl, grid.GetSlot(0, 0))
assert.Equal(t, tr, grid.GetSlot(1, 0))
assert.Equal(t, bl, grid.GetSlot(0, 1))
assert.Equal(t, br, grid.GetSlot(1, 1))
}
func TestItemGrid_Add_LargeItem(t *testing.T) {
grid := NewItemGrid(3, 3, 0, 0)
tl := NewTestItem(1, 1)
o := NewTestItem(2, 2)
added, err := grid.Add(tl, o)
assert.Equal(t, 2, added)
assert.NoError(t, err)
assert.Equal(t, tl, grid.GetSlot(0, 0))
assert.Equal(t, o, grid.GetSlot(1, 0))
assert.Equal(t, o, grid.GetSlot(2, 0))
assert.Nil(t, grid.GetSlot(0, 1))
assert.Equal(t, o, grid.GetSlot(1, 1))
assert.Equal(t, o, grid.GetSlot(2, 1))
assert.Nil(t, grid.GetSlot(0, 2))
assert.Nil(t, grid.GetSlot(1, 2))
assert.Nil(t, grid.GetSlot(2, 2))
}
func TestItemGrid_Add_OverflowLargeItem(t *testing.T) {
grid := NewItemGrid(2, 2, 0, 0)
tl := NewTestItem(1, 1)
o := NewTestItem(2, 2)
added, err := grid.Add(tl, o)
assert.Equal(t, 1, added)
assert.Error(t, err)
assert.Equal(t, tl, grid.GetSlot(0, 0))
assert.Nil(t, grid.GetSlot(1, 0))
assert.Nil(t, grid.GetSlot(0, 1))
assert.Nil(t, grid.GetSlot(1, 1))
}