mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-12 15:27:31 -05:00
e5dae4e5d8
* d2ui/UIFrame: Refactor into its own class it's not useful to have the handling of frames for the inventory/herostate/skilltree/quest panels individually in each of those. * d2ui/button: Fix crash when a buttonlayout was not allowing FrameChange When AllowFrameChange is false we do not create pressedSurface. So if we press the button the game will crash. * d2ui/button: Allow label-only buttons At least for the skillmenu we need buttons were the graphic size does not match the buttonsize. So let's render the graphic in there and make the button label only. * d2hero/hero_state_factory: Give all heroes their class specific skills * d2player/gamecontrols: Fix wrong inventory/stats layouts for exp chars For Druid/Assassin the inventory frame was rendered for a 640x480 resolution. This brings it in line with all other characters. * d2player: Add inital Skilltree panel * d2player/game_controls: Enable skilltree Note here, that the inventory panel and skilltree panel can overlap. * d2player/skilltree: Add skillicon rendering Note here, that I couldn't figure out how to render them dark if no skillpoints are invested. Signed-off-by: juander <juander@rumtueddeln.de>
253 lines
6.3 KiB
Go
253 lines
6.3 KiB
Go
package d2hero
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
)
|
|
|
|
// NewHeroStateFactory creates a new HeroStateFactory and initializes it.
|
|
func NewHeroStateFactory(asset *d2asset.AssetManager) (*HeroStateFactory, error) {
|
|
inventoryItemFactory, err := d2inventory.NewInventoryItemFactory(asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
factory := &HeroStateFactory{
|
|
asset: asset,
|
|
InventoryItemFactory: inventoryItemFactory,
|
|
}
|
|
|
|
return factory, nil
|
|
}
|
|
|
|
// HeroStateFactory is responsible for creating player state objects
|
|
type HeroStateFactory struct {
|
|
asset *d2asset.AssetManager
|
|
*d2inventory.InventoryItemFactory
|
|
}
|
|
|
|
// CreateHeroState creates a HeroState instance and returns a pointer to it
|
|
func (f *HeroStateFactory) CreateHeroState(
|
|
heroName string,
|
|
hero d2enum.Hero,
|
|
statsState *HeroStatsState,
|
|
) (*HeroState, error) {
|
|
result := &HeroState{
|
|
HeroName: heroName,
|
|
HeroType: hero,
|
|
Act: 1,
|
|
Stats: statsState,
|
|
Equipment: f.DefaultHeroItems[hero],
|
|
FilePath: "",
|
|
}
|
|
|
|
defaultStats := f.asset.Records.Character.Stats[hero]
|
|
skillState, err := f.CreateHeroSkillsState(defaultStats, hero)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.Skills = skillState
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetAllHeroStates returns all player saves
|
|
func (f *HeroStateFactory) GetAllHeroStates() ([]*HeroState, error) {
|
|
basePath, _ := f.getGameBaseSavePath()
|
|
files, _ := ioutil.ReadDir(basePath)
|
|
result := make([]*HeroState, 0)
|
|
|
|
for _, file := range files {
|
|
fileName := file.Name()
|
|
if file.IsDir() || len(fileName) < 5 || !strings.EqualFold(fileName[len(fileName)-4:], ".od2") {
|
|
continue
|
|
}
|
|
|
|
gameState := f.LoadHeroState(path.Join(basePath, file.Name()))
|
|
if gameState == nil || gameState.HeroType == d2enum.HeroNone {
|
|
|
|
} else if gameState.Stats == nil || gameState.Skills == nil {
|
|
// temporarily loading default class stats if the character was created before saving stats/skills was introduced
|
|
// to be removed in the future
|
|
classStats := f.asset.Records.Character.Stats[gameState.HeroType]
|
|
gameState.Stats = f.CreateHeroStatsState(gameState.HeroType, classStats)
|
|
|
|
skillState, err := f.CreateHeroSkillsState(classStats, gameState.HeroType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gameState.Skills = skillState
|
|
|
|
if err := f.Save(gameState); err != nil {
|
|
fmt.Printf("failed to save game state!, err: %v\n", err)
|
|
}
|
|
}
|
|
|
|
result = append(result, gameState)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CreateHeroSkillsState will assemble the hero skills from the class stats record.
|
|
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatsRecord, heroType d2enum.Hero) (map[int]*HeroSkill, error) {
|
|
baseSkills := map[int]*HeroSkill{}
|
|
|
|
for idx := range classStats.BaseSkill {
|
|
skillName := &classStats.BaseSkill[idx]
|
|
|
|
if *skillName == "" {
|
|
continue
|
|
}
|
|
|
|
skill, err := f.CreateHeroSkill(1, *skillName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
baseSkills[skill.ID] = skill
|
|
}
|
|
|
|
skillList := f.asset.Records.Skill.Details
|
|
token := strings.ToLower(heroType.GetToken3())
|
|
for idx := range skillList {
|
|
if skillList[idx].Charclass == token {
|
|
skill, _ := f.CreateHeroSkill(0, skillList[idx].Skill)
|
|
baseSkills[skill.ID] = skill
|
|
}
|
|
}
|
|
|
|
|
|
skillRecord, err := f.CreateHeroSkill(1, "Attack")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseSkills[skillRecord.ID] = skillRecord
|
|
|
|
return baseSkills, nil
|
|
}
|
|
|
|
// CreateHeroSkill creates an instance of a skill
|
|
func (f *HeroStateFactory) CreateHeroSkill(points int, name string) (*HeroSkill, error) {
|
|
skillRecord := f.asset.Records.GetSkillByName(name)
|
|
if skillRecord == nil {
|
|
return nil, fmt.Errorf("Skill not found: %s", name)
|
|
}
|
|
|
|
skillDescRecord, found := f.asset.Records.Skill.Descriptions[skillRecord.Skilldesc]
|
|
if !found {
|
|
return nil, fmt.Errorf("Skill Description not found: %s", name)
|
|
}
|
|
|
|
result := &HeroSkill{
|
|
SkillPoints: points,
|
|
SkillRecord: skillRecord,
|
|
SkillDescriptionRecord: skillDescRecord,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// HasGameStates returns true if the player has any previously saved game
|
|
func (f *HeroStateFactory) HasGameStates() bool {
|
|
basePath, _ := f.getGameBaseSavePath()
|
|
files, _ := ioutil.ReadDir(basePath)
|
|
|
|
return len(files) > 0
|
|
}
|
|
|
|
// CreateTestGameState is used for the map engine previewer
|
|
func (f *HeroStateFactory) CreateTestGameState() *HeroState {
|
|
result := &HeroState{}
|
|
return result
|
|
}
|
|
|
|
// LoadHeroState loads the player state from the file
|
|
func (f *HeroStateFactory) LoadHeroState(filePath string) *HeroState {
|
|
strData, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
result := &HeroState{
|
|
FilePath: filePath,
|
|
}
|
|
|
|
err = json.Unmarshal(strData, result)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Here, we turn the shallow skill data back into records from the asset manager.
|
|
// This is because this factory has a reference to the asset manager with loaded records.
|
|
// We cant do this while unmarshalling because there is no reference to the asset manager.
|
|
for idx := range result.Skills {
|
|
hs := result.Skills[idx]
|
|
|
|
// TODO: figure out why this can be nil
|
|
if hs == nil {
|
|
continue
|
|
}
|
|
|
|
hs.SkillRecord = f.asset.Records.Skill.Details[hs.shallow.SkillID]
|
|
hs.SkillDescriptionRecord = f.asset.Records.Skill.Descriptions[hs.SkillRecord.Skilldesc]
|
|
hs.SkillPoints = hs.shallow.SkillPoints
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (f *HeroStateFactory) getGameBaseSavePath() (string, error) {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return path.Join(configDir, "OpenDiablo2/Saves"), nil
|
|
}
|
|
|
|
func (f *HeroStateFactory) getFirstFreeFileName() string {
|
|
i := 0
|
|
basePath, _ := f.getGameBaseSavePath()
|
|
|
|
for {
|
|
filePath := path.Join(basePath, strconv.Itoa(i)+".od2")
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
return filePath
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
// Save saves the player state to a file
|
|
func (f *HeroStateFactory) Save(state *HeroState) error {
|
|
if state.FilePath == "" {
|
|
state.FilePath = f.getFirstFreeFileName()
|
|
}
|
|
if err := os.MkdirAll(path.Dir(state.FilePath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
fileJSON, _ := json.MarshalIndent(state, "", " ")
|
|
if err := ioutil.WriteFile(state.FilePath, fileJSON, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|