From b97bf6353d5db905858f89a0953f825aec3b6ff5 Mon Sep 17 00:00:00 2001 From: Tim Sarbin Date: Thu, 14 Nov 2019 22:20:01 -0500 Subject: [PATCH] Added inventory objects. (#177) --- d2common/d2enum/hero.go | 24 +++++++++ d2common/d2enum/inventory_item_type.go | 9 ++++ d2common/d2interface/inventory_item.go | 16 ++++++ d2core/character_equipment.go | 13 +++++ d2core/d2scene/character_select.go | 15 ++++-- d2core/engine.go | 1 + d2core/game_state.go | 23 +++++---- d2core/hero.go | 48 ++++++++++++++++++ d2core/hero_objects.go | 41 ++++++++++++++++ d2core/inventory_item_armor.go | 57 ++++++++++++++++++++++ d2core/iventory_item_weapon.go | 67 ++++++++++++++++++++++++++ d2core/npc.go | 2 +- d2data/d2datadict/hero_objects.go | 43 ----------------- d2render/animated_entity.go | 25 ++++++---- d2render/d2mapengine/region.go | 2 +- 15 files changed, 319 insertions(+), 67 deletions(-) create mode 100644 d2common/d2enum/inventory_item_type.go create mode 100644 d2common/d2interface/inventory_item.go create mode 100644 d2core/character_equipment.go create mode 100644 d2core/hero.go create mode 100644 d2core/hero_objects.go create mode 100644 d2core/inventory_item_armor.go create mode 100644 d2core/iventory_item_weapon.go delete mode 100644 d2data/d2datadict/hero_objects.go diff --git a/d2common/d2enum/hero.go b/d2common/d2enum/hero.go index b68a4a64..e04c97d2 100644 --- a/d2common/d2enum/hero.go +++ b/d2common/d2enum/hero.go @@ -1,5 +1,7 @@ package d2enum +import "log" + type Hero int const ( @@ -13,5 +15,27 @@ const ( HeroDruid Hero = 7 // Druid ) +func (v Hero) GetToken() string { + switch v { + case HeroBarbarian: + return "BA" + case HeroNecromancer: + return "NE" + case HeroPaladin: + return "PA" + case HeroAssassin: + return "AI" + case HeroSorceress: + return "SO" + case HeroAmazon: + return "AM" + case HeroDruid: + return "DZ" + default: + log.Fatalf("Unknown hero token: %d", v) + } + return "" +} + //go:generate stringer -linecomment -type Hero //go:generate string2enum -samepkg -linecomment -type Hero diff --git a/d2common/d2enum/inventory_item_type.go b/d2common/d2enum/inventory_item_type.go new file mode 100644 index 00000000..85eab5f1 --- /dev/null +++ b/d2common/d2enum/inventory_item_type.go @@ -0,0 +1,9 @@ +package d2enum + +type InventoryItemType int + +const ( + InventoryItemTypeItem InventoryItemType = 0 // Item + InventoryItemTypeWeapon InventoryItemType = 1 // Weapon + InventoryItemTypeArmor InventoryItemType = 2 // Armor +) diff --git a/d2common/d2interface/inventory_item.go b/d2common/d2interface/inventory_item.go new file mode 100644 index 00000000..8ce67d57 --- /dev/null +++ b/d2common/d2interface/inventory_item.go @@ -0,0 +1,16 @@ +package d2interface + +import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + +type InventoryItem interface { + // GetInventoryItemName returns the name of this inventory item + GetInventoryItemName() string + // GetInventoryItemType returns the type of item this is + GetInventoryItemType() d2enum.InventoryItemType + // GetInventoryGridSize returns the width/height grid size of this inventory item + GetInventoryGridSize() (int, int) + // Returns the item code + GetItemCode() string + // Serializes the object for transport + Serialize() []byte +} diff --git a/d2core/character_equipment.go b/d2core/character_equipment.go new file mode 100644 index 00000000..e5c5dcd3 --- /dev/null +++ b/d2core/character_equipment.go @@ -0,0 +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 + // S1-S8? +} diff --git a/d2core/d2scene/character_select.go b/d2core/d2scene/character_select.go index 817d9ff6..a6f56a77 100644 --- a/d2core/d2scene/character_select.go +++ b/d2core/d2scene/character_select.go @@ -42,7 +42,7 @@ type CharacterSelect struct { characterNameLabel [8]d2ui.Label characterStatsLabel [8]d2ui.Label characterExpLabel [8]d2ui.Label - characterImage [8]*d2core.NPC + characterImage [8]*d2core.Hero gameStates []*d2core.GameState selectedCharacter int mouseButtonPressed bool @@ -167,7 +167,7 @@ func (v *CharacterSelect) onScrollUpdate() { } func (v *CharacterSelect) updateCharacterBoxes() { - expText := d2common.TranslateString("expansionchar2x") + expText := d2common.TranslateString("#803") for i := 0; i < 8; i++ { idx := i + (v.charScrollbar.GetCurrentOffset() * 2) if idx >= len(v.gameStates) { @@ -181,7 +181,14 @@ func (v *CharacterSelect) updateCharacterBoxes() { v.characterStatsLabel[i].SetText("Level 1 " + v.gameStates[idx].HeroType.String()) v.characterExpLabel[i].SetText(expText) // TODO: Generate or load the object from the actual player data... - v.characterImage[i] = d2core.CreateNPC(0, 0, d2datadict.HeroObjects[v.gameStates[idx].HeroType], v.fileProvider, 5) + v.characterImage[i] = d2core.CreateHero( + 0, + 0, + 5, + v.gameStates[idx].HeroType, + d2core.HeroObjects[v.gameStates[idx].HeroType], + v.fileProvider, + ) } } @@ -213,7 +220,7 @@ func (v *CharacterSelect) Render(screen *ebiten.Image) { v.characterNameLabel[i].Draw(screen) v.characterStatsLabel[i].Draw(screen) v.characterExpLabel[i].Draw(screen) - v.characterImage[i].Render(screen, v.characterNameLabel[i].X-40, v.characterNameLabel[i].Y+30) + v.characterImage[i].Render(screen, v.characterNameLabel[i].X-40, v.characterNameLabel[i].Y+50) } if v.showDeleteConfirmation { ebitenutil.DrawRect(screen, 0.0, 0.0, 800.0, 600.0, color.RGBA{0, 0, 0, 128}) diff --git a/d2core/engine.go b/d2core/engine.go index 88c99cdc..8dce3529 100644 --- a/d2core/engine.go +++ b/d2core/engine.go @@ -78,6 +78,7 @@ func CreateEngine() Engine { d2datadict.LoadSounds(&result) d2data.LoadAnimationData(&result) d2datadict.LoadMonStats(&result) + LoadHeroObjects() result.SoundManager = d2audio.CreateManager(&result) result.SoundManager.SetVolumes(result.Settings.BgmVolume, result.Settings.SfxVolume) result.UIManager = d2ui.CreateManager(&result, *result.SoundManager) diff --git a/d2core/game_state.go b/d2core/game_state.go index 7eedf068..d0490f38 100644 --- a/d2core/game_state.go +++ b/d2core/game_state.go @@ -21,6 +21,7 @@ import ( UINT32 GameState Version INT64 Game Seed BYTE Hero Type + BYTE Hero Level BYTE Act BYTE Hero Name Length BYTE[] Hero Name @@ -28,14 +29,16 @@ import ( */ type GameState struct { - Seed int64 - HeroName string - HeroType d2enum.Hero - Act int - FilePath string + Seed int64 + HeroName string + HeroType d2enum.Hero + HeroLevel int + Act int + FilePath string + Equipment CharacterEquipment } -const GameStateVersion = uint32(1) // Update this when you make breaking changes +const GameStateVersion = uint32(2) // Update this when you make breaking changes func HasGameStates() bool { files, _ := ioutil.ReadDir(getGameBaseSavePath()) @@ -83,12 +86,13 @@ func LoadGameState(path string) *GameState { } defer f.Close() sr := d2common.CreateStreamReader(bytes) - if sr.GetUInt32() > GameStateVersion { + if sr.GetUInt32() != GameStateVersion { // Unknown game version return nil } result.Seed = sr.GetInt64() result.HeroType = d2enum.Hero(sr.GetByte()) + result.HeroLevel = int(sr.GetByte()) result.Act = int(sr.GetByte()) heroNameLen := sr.GetByte() heroName, _ := sr.ReadBytes(int(heroNameLen)) @@ -129,7 +133,7 @@ func getGameBaseSavePath() string { return basePath } -func getFirstFreefileName() string { +func getFirstFreeFileName() string { i := 0 basePath := getGameBaseSavePath() for { @@ -143,7 +147,7 @@ func getFirstFreefileName() string { func (v *GameState) Save() { if v.FilePath == "" { - v.FilePath = getFirstFreefileName() + v.FilePath = getFirstFreeFileName() } f, err := os.Create(v.FilePath) if err != nil { @@ -154,6 +158,7 @@ func (v *GameState) Save() { sr.PushUint32(GameStateVersion) sr.PushInt64(v.Seed) sr.PushByte(byte(v.HeroType)) + sr.PushByte(byte(v.HeroLevel)) sr.PushByte(byte(v.Act)) sr.PushByte(byte(len(v.HeroName))) for _, ch := range v.HeroName { diff --git a/d2core/hero.go b/d2core/hero.go new file mode 100644 index 00000000..26bddd81 --- /dev/null +++ b/d2core/hero.go @@ -0,0 +1,48 @@ +package d2core + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2render" + "github.com/hajimehoshi/ebiten" +) + +type Hero struct { + AnimatedEntity d2render.AnimatedEntity + Equipment CharacterEquipment + mode d2enum.AnimationMode + direction int +} + +func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment CharacterEquipment, fileProvider d2interface.FileProvider) *Hero { + result := &Hero{ + AnimatedEntity: d2render.CreateAnimatedEntity(x, y, &d2datadict.ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: heroType.GetToken(), + Class: equipment.RightHand.GetWeaponClass(), + SH: equipment.Shield.GetItemCode(), + // 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(), + }, + fileProvider, + d2enum.Units, + ), + Equipment: equipment, + mode: d2enum.AnimationModePlayerTownNeutral, + direction: direction, + } + result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.GetWeaponClass(), direction) + return result +} + +func (v *Hero) Render(target *ebiten.Image, offsetX, offsetY int) { + v.AnimatedEntity.Render(target, offsetX, offsetY) +} diff --git a/d2core/hero_objects.go b/d2core/hero_objects.go new file mode 100644 index 00000000..155b0533 --- /dev/null +++ b/d2core/hero_objects.go @@ -0,0 +1,41 @@ +package d2core + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) + +var HeroObjects map[d2enum.Hero]CharacterEquipment + +func LoadHeroObjects() { + //Mode: d2enum.AnimationModePlayerNeutral.String(), + //Base: "/data/global/chars", + HeroObjects = map[d2enum.Hero]CharacterEquipment{ + d2enum.HeroBarbarian: { + RightHand: GetWeaponItemByCode("hax"), + Shield: GetArmorItemByCode("buc"), + }, + d2enum.HeroNecromancer: { + RightHand: GetWeaponItemByCode("wnd"), + }, + d2enum.HeroPaladin: { + RightHand: GetWeaponItemByCode("ssd"), + Shield: GetArmorItemByCode("buc"), + }, + d2enum.HeroAssassin: { + RightHand: GetWeaponItemByCode("ktr"), + Shield: GetArmorItemByCode("buc"), + }, + d2enum.HeroSorceress: { + RightHand: GetWeaponItemByCode("sst"), + LeftHand: GetWeaponItemByCode("sst"), + }, + d2enum.HeroAmazon: { + RightHand: GetWeaponItemByCode("jav"), + Shield: GetArmorItemByCode("buc"), + }, + d2enum.HeroDruid: { + RightHand: GetWeaponItemByCode("clb"), + Shield: GetArmorItemByCode("buc"), + }, + } +} diff --git a/d2core/inventory_item_armor.go b/d2core/inventory_item_armor.go new file mode 100644 index 00000000..795e81c3 --- /dev/null +++ b/d2core/inventory_item_armor.go @@ -0,0 +1,57 @@ +package d2core + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" +) + +type InventoryItemArmor struct { + inventorySizeX int + inventorySizeY int + itemName string + itemCode string + armorClass string +} + +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{ + inventorySizeX: result.InventoryWidth, + inventorySizeY: result.InventoryHeight, + itemName: result.Name, + itemCode: result.Code, + armorClass: "lit", // TODO: Where does this come from? + } +} + +func (v InventoryItemArmor) GetArmorClass() string { + if v.itemCode == "" { + return "lit" + } + return v.armorClass +} + +func (v InventoryItemArmor) GetInventoryItemName() string { + return v.itemName +} + +func (v InventoryItemArmor) GetInventoryItemType() d2enum.InventoryItemType { + return d2enum.InventoryItemTypeArmor +} + +func (v InventoryItemArmor) GetInventoryGridSize() (int, int) { + return v.inventorySizeX, v.inventorySizeY +} + +func (v InventoryItemArmor) Serialize() []byte { + return []byte{} +} + +func (v InventoryItemArmor) GetItemCode() string { + return v.itemCode +} diff --git a/d2core/iventory_item_weapon.go b/d2core/iventory_item_weapon.go new file mode 100644 index 00000000..00e95bc7 --- /dev/null +++ b/d2core/iventory_item_weapon.go @@ -0,0 +1,67 @@ +package d2core + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" +) + +type InventoryItemWeapon struct { + inventorySizeX int + inventorySizeY int + itemName string + itemCode string + weaponClass string + weaponClassOffHand string +} + +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{ + inventorySizeX: result.InventoryWidth, + inventorySizeY: result.InventoryHeight, + itemName: result.Name, + itemCode: result.Code, + weaponClass: result.WeaponClass, + weaponClassOffHand: result.WeaponClass2Hand, + } +} + +func (v InventoryItemWeapon) GetWeaponClass() string { + if v.itemCode == "" { + return "hth" + } + return v.weaponClass +} + +func (v InventoryItemWeapon) GetWeaponClassOffHand() string { + if v.itemCode == "" { + return "" + } + return v.weaponClassOffHand +} + +func (v InventoryItemWeapon) GetInventoryItemName() string { + return v.itemName +} + +func (v InventoryItemWeapon) GetInventoryItemType() d2enum.InventoryItemType { + return d2enum.InventoryItemTypeWeapon +} + +func (v InventoryItemWeapon) GetInventoryGridSize() (int, int) { + return v.inventorySizeX, v.inventorySizeY +} + +func (v InventoryItemWeapon) Serialize() []byte { + return []byte{} +} + +func (v InventoryItemWeapon) GetItemCode() string { + return v.itemCode +} diff --git a/d2core/npc.go b/d2core/npc.go index 8e88bb9a..9ab51f3d 100644 --- a/d2core/npc.go +++ b/d2core/npc.go @@ -18,7 +18,7 @@ func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d result := &NPC{ AnimatedEntity: d2render.CreateAnimatedEntity(x, y, object, fileProvider, d2enum.Units), } - result.AnimatedEntity.SetMode(object.Mode, object.Class, direction, fileProvider) + result.AnimatedEntity.SetMode(object.Mode, object.Class, direction) return result } diff --git a/d2data/d2datadict/hero_objects.go b/d2data/d2datadict/hero_objects.go deleted file mode 100644 index 86ee6792..00000000 --- a/d2data/d2datadict/hero_objects.go +++ /dev/null @@ -1,43 +0,0 @@ -package d2datadict - -import ( - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" -) - -var HeroObjects = map[d2enum.Hero]*ObjectLookupRecord{ - d2enum.HeroBarbarian: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "BA", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroNecromancer: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "NE", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroPaladin: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "PA", Class: "1HS", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroAssassin: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "AI", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroSorceress: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "SO", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroAmazon: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "AM", Class: "1HT", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, - d2enum.HeroDruid: &ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: "DZ", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", - }, -} diff --git a/d2render/animated_entity.go b/d2render/animated_entity.go index 0bde6396..f93c6f8c 100644 --- a/d2render/animated_entity.go +++ b/d2render/animated_entity.go @@ -31,6 +31,7 @@ var DccLayerNames = []string{"HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1 // AnimatedEntity represents an entity on the map that can be animated type AnimatedEntity struct { + fileProvider d2interface.FileProvider // LocationX represents the tile X position of the entity LocationX float64 // LocationY represents the tile Y position of the entity @@ -56,10 +57,11 @@ type AnimatedEntity struct { // CreateAnimatedEntity creates an instance of AnimatedEntity func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, palette d2enum.PaletteType) AnimatedEntity { result := AnimatedEntity{ - base: object.Base, - token: object.Token, - object: object, - palette: palette, + fileProvider: fileProvider, + base: object.Base, + token: object.Token, + object: object, + palette: palette, } result.dccLayers = make(map[string]d2dcc.DCC) result.LocationX = float64(x) / 5 @@ -75,9 +77,9 @@ func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fil var DirectionLookup = []int{3, 15, 4, 8, 0, 9, 5, 10, 1, 11, 6, 12, 2, 13, 7, 14} // SetMode changes the graphical mode of this animated entity -func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int, provider d2interface.FileProvider) { +func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) { cofPath := fmt.Sprintf("%s/%s/COF/%s%s%s.COF", v.base, v.token, v.token, animationMode, weaponClass) - v.Cof = d2cof.LoadCOF(cofPath, provider) + v.Cof = d2cof.LoadCOF(cofPath, v.fileProvider) v.animationMode = animationMode v.weaponClass = weaponClass v.direction = direction @@ -89,7 +91,7 @@ func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction in v.dccLayers = make(map[string]d2dcc.DCC) for _, cofLayer := range v.Cof.CofLayers { layerName := DccLayerNames[cofLayer.Type] - v.dccLayers[layerName] = v.LoadLayer(layerName, provider) + v.dccLayers[layerName] = v.LoadLayer(layerName, v.fileProvider) if !v.dccLayers[layerName].IsValid() { continue } @@ -99,7 +101,7 @@ func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction in } func (v *AnimatedEntity) LoadLayer(layer string, fileProvider d2interface.FileProvider) d2dcc.DCC { - layerName := "tr" + layerName := "TR" switch strings.ToUpper(layer) { case "HD": // Head layerName = v.object.HD @@ -138,7 +140,12 @@ func (v *AnimatedEntity) LoadLayer(layer string, fileProvider d2interface.FilePr return d2dcc.DCC{} } dccPath := fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, v.weaponClass) - return d2dcc.LoadDCC(dccPath, fileProvider) + result := d2dcc.LoadDCC(dccPath, fileProvider) + if !result.IsValid() { + dccPath = fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, "HTH") + result = d2dcc.LoadDCC(dccPath, fileProvider) + } + return result } // Render draws this animated entity onto the target diff --git a/d2render/d2mapengine/region.go b/d2render/d2mapengine/region.go index 4878965b..805ca1c2 100644 --- a/d2render/d2mapengine/region.go +++ b/d2render/d2mapengine/region.go @@ -112,7 +112,7 @@ func (v *Region) loadObjects(fileProvider d2interface.FileProvider) { return } entity := d2render.CreateAnimatedEntity(object.X, object.Y, object.Lookup, fileProvider, d2enum.Units) - entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0, fileProvider) + entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0) v.AnimationEntities = append(v.AnimationEntities, entity) } }(object)