diff --git a/d2common/d2data/d2datadict/missiles.go b/d2common/d2data/d2datadict/missiles.go index e8a1f6a5..249fb9cd 100644 --- a/d2common/d2data/d2datadict/missiles.go +++ b/d2common/d2data/d2datadict/missiles.go @@ -306,10 +306,17 @@ func createMissileRecord(line string) MissileRecord { // Missiles stores all of the MissileRecords //nolint:gochecknoglobals // Currently global by design, only written once var Missiles map[int]*MissileRecord +var missilesByName map[string]*MissileRecord + +// GetMissileByName allows lookup of a MissileRecord by a given name. The name will be lowercased and stripped of whitespaces. +func GetMissileByName(missileName string) *MissileRecord { + return missilesByName[sanitize(missileName)] +} // LoadMissiles loads MissileRecords from missiles.txt func LoadMissiles(file []byte) { Missiles = make(map[int]*MissileRecord) + missilesByName = make(map[string]*MissileRecord) data := strings.Split(string(file), "\r\n")[1:] for _, line := range data { @@ -319,11 +326,16 @@ func LoadMissiles(file []byte) { rec := createMissileRecord(line) Missiles[rec.Id] = &rec + missilesByName[sanitize(rec.Name)] = &rec } log.Printf("Loaded %d missiles", len(Missiles)) } +func sanitize(missileName string) string { + return strings.ToLower(strings.ReplaceAll(missileName, " ", "")) +} + func loadMissileCalcParam(r *[]string, inc func() int) MissileCalcParam { result := MissileCalcParam{ Param: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])), diff --git a/d2common/d2data/d2datadict/skilldesc.go b/d2common/d2data/d2datadict/skilldesc.go index 7a74115b..b2d156bf 100644 --- a/d2common/d2data/d2datadict/skilldesc.go +++ b/d2common/d2data/d2datadict/skilldesc.go @@ -17,7 +17,7 @@ type SkillDescriptionRecord struct { SkillColumn string // SkillColumn ListRow string // ListRow ListPool string // ListPool - IconCel string // IconCel + IconCel int // IconCel NameKey string // str name ShortKey string // str short LongKey string // str long @@ -146,7 +146,7 @@ func LoadSkillDescriptions(file []byte) { //nolint:funlen // doesn't make sense d.String("SkillColumn"), d.String("ListRow"), d.String("ListPool"), - d.String("IconCel"), + d.Number("IconCel"), d.String("str name"), d.String("str short"), d.String("str long"), diff --git a/d2common/d2data/d2datadict/skills.go b/d2common/d2data/d2datadict/skills.go index e5fc7b2b..ecb4d9d1 100644 --- a/d2common/d2data/d2datadict/skills.go +++ b/d2common/d2data/d2datadict/skills.go @@ -12,6 +12,8 @@ import ( //nolint:gochecknoglobals // Currently global by design, only written once var SkillDetails map[int]*SkillRecord +var skillDetailsByName map[string]*SkillRecord + // SkillRecord is a row from the skills.txt file. Here are two resources for more info on each field // [https://d2mods.info/forum/viewtopic.php?t=41556, https://d2mods.info/forum/kb/viewarticle?a=246] type SkillRecord struct { @@ -263,6 +265,7 @@ type SkillRecord struct { // LoadCharStats loads charstats.txt file contents into map[d2enum.Hero]*CharStatsRecord func LoadSkills(file []byte) { SkillDetails = make(map[int]*SkillRecord) + skillDetailsByName = make(map[string]*SkillRecord) parser := d2parser.New() @@ -515,6 +518,7 @@ func LoadSkills(file []byte) { CostAdd: d.Number("cost add"), } SkillDetails[record.ID] = record + skillDetailsByName[record.Skill] = record } if d.Err != nil { @@ -523,3 +527,8 @@ func LoadSkills(file []byte) { log.Printf("Loaded %d Skill records", len(SkillDetails)) } + +// GetSkillByName returns the skill record for the given Skill name. +func GetSkillByName(skillName string) *SkillRecord { + return skillDetailsByName[skillName] +} diff --git a/d2core/d2hero/hero_skill.go b/d2core/d2hero/hero_skill.go new file mode 100644 index 00000000..0d2717d1 --- /dev/null +++ b/d2core/d2hero/hero_skill.go @@ -0,0 +1,51 @@ +package d2hero + +import ( + "encoding/json" + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" +) + +// HeroSkill stores additional payload for a skill of a hero. +type HeroSkill struct { + *d2datadict.SkillRecord + *d2datadict.SkillDescriptionRecord + SkillPoints int +} + +// An auxilary struct which only stores the ID of the SkillRecord, instead of the whole SkillRecord and SkillDescrptionRecord. +type shallowHeroSkill struct { + SkillID int `json:"skillId"` + SkillPoints int `json:"skillPoints"` +} + +// MarshalJSON overrides the default logic used when the HeroSkill is serialized to a byte array. +func (hs *HeroSkill) MarshalJSON() ([]byte, error) { + // only serialize the ID instead of the whole SkillRecord object. + shallow := shallowHeroSkill{ + SkillID: hs.SkillRecord.ID, + SkillPoints: hs.SkillPoints, + } + + bytes, err := json.Marshal(shallow) + if err != nil { + log.Fatalln(err) + } + + return bytes, err +} + +// UnmarshalJSON overrides the default logic used when the HeroSkill is deserialized from a byte array. +func (hs *HeroSkill) UnmarshalJSON(data []byte) error { + shallow := shallowHeroSkill{} + if err := json.Unmarshal(data, &shallow); err != nil { + return err + } + + hs.SkillRecord = d2datadict.SkillDetails[shallow.SkillID] + hs.SkillDescriptionRecord = d2datadict.SkillDescriptions[hs.SkillRecord.Skilldesc] + hs.SkillPoints = shallow.SkillPoints + + return nil +} diff --git a/d2core/d2hero/hero_skills_state.go b/d2core/d2hero/hero_skills_state.go new file mode 100644 index 00000000..9d611a75 --- /dev/null +++ b/d2core/d2hero/hero_skills_state.go @@ -0,0 +1,26 @@ +package d2hero + +import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + +// HeroSkillsState hold all spells that a hero has. +type HeroSkillsState map[int] *HeroSkill + +// CreateHeroSkillsState will assemble the hero skills from the class stats record. +func CreateHeroSkillsState(classStats *d2datadict.CharStatsRecord) *HeroSkillsState { + baseSkills := HeroSkillsState{} + + for idx := range classStats.BaseSkill { + skillName := &classStats.BaseSkill[idx] + if len(*skillName) == 0 { + continue + } + + skillRecord := d2datadict.GetSkillByName(*skillName) + baseSkills[skillRecord.ID] = &HeroSkill{SkillPoints: 1, SkillRecord: skillRecord} + } + + skillRecord := d2datadict.GetSkillByName("Attack") + baseSkills[skillRecord.ID] = &HeroSkill{SkillPoints: 1, SkillRecord: skillRecord} + + return &baseSkills +} diff --git a/d2core/d2hero/hero_stats_state.go b/d2core/d2hero/hero_stats_state.go index 64103f90..4f8626f4 100644 --- a/d2core/d2hero/hero_stats_state.go +++ b/d2core/d2hero/hero_stats_state.go @@ -30,8 +30,8 @@ type HeroStatsState struct { PoisonResistance int `json:"poisonResistance"` // values which are not saved/loaded(computed) - Stamina int // only MaxStamina is saved, Stamina gets reset on entering world - NextLevelExp int + Stamina int `json:"-"` // only MaxStamina is saved, Stamina gets reset on entering world + NextLevelExp int `json:"-"` } // CreateHeroStatsState generates a running state from a hero stats. @@ -56,8 +56,8 @@ func CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2datadict.CharStat result.Stamina = result.MaxStamina // TODO: For demonstration purposes (hp, mana, exp, & character stats panel gets updated depending on stats) - result.Health = 20 - result.Mana = 9 + result.Health = 50 + result.Mana = 30 result.Experience = 166 return &result diff --git a/d2core/d2map/d2mapentity/factory.go b/d2core/d2map/d2mapentity/factory.go index f223acb9..ec1f4a5b 100644 --- a/d2core/d2map/d2mapentity/factory.go +++ b/d2core/d2map/d2mapentity/factory.go @@ -41,7 +41,7 @@ func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntit // NewPlayer creates a new player entity and returns a pointer to it. func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero, - stats *d2hero.HeroStatsState, equipment *d2inventory.CharacterEquipment) *Player { + stats *d2hero.HeroStatsState, skills *d2hero.HeroSkillsState, equipment *d2inventory.CharacterEquipment) *Player { layerEquipment := &[d2enum.CompositeTypeMax]string{ d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(), d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(), @@ -62,11 +62,16 @@ func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroT stats.NextLevelExp = d2datadict.GetExperienceBreakpoint(heroType, stats.Level) stats.Stamina = stats.MaxStamina + attackSkillID := 0 result := &Player{ mapEntity: newMapEntity(x, y), composite: composite, Equipment: equipment, Stats: stats, + Skills: skills, + //TODO: active left & right skill should be loaded from save file instead + LeftSkill: (*skills)[attackSkillID], + RightSkill: (*skills)[attackSkillID], name: name, Class: heroType, //nameLabel: d2ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic), diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index ee9e469b..85cc3eb0 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -19,6 +19,9 @@ type Player struct { composite *d2asset.Composite Equipment *d2inventory.CharacterEquipment Stats *d2hero.HeroStatsState + Skills *d2hero.HeroSkillsState + LeftSkill *d2hero.HeroSkill + RightSkill *d2hero.HeroSkill Class d2enum.Hero lastPathSize int isInTown bool diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index 0b9ec80a..c92a6777 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -288,6 +288,7 @@ func (v *CharacterSelect) updateCharacterBoxes() { v.characterImage[i] = v.NewPlayer("", "", 0, 0, 0, v.gameStates[idx].HeroType, v.gameStates[idx].Stats, + v.gameStates[idx].Skills, &equipment, ) } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index b13af534..5c46a353 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -27,7 +27,7 @@ const hideZoneTextAfterSeconds = 2.0 const ( moveErrStr = "failed to send MovePlayer packet to the server, playerId: %s, x: %g, x: %g\n" bindControlsErrStr = "failed to add gameControls as input handler for player: %s\n" - castErrStr = "failed to send CastSkill packet to the server, playerId: %s, missileId: %d, x: %g, x: %g\n" + castErrStr = "failed to send CastSkill packet to the server, playerId: %s, skillId: %d, x: %g, x: %g\n" spawnItemErrStr = "failed to send SpawnItem packet to the server: (%d, %d) %+v" ) @@ -312,10 +312,10 @@ func (v *Game) OnPlayerMove(targetX, targetY float64) { } // OnPlayerCast sends the casting skill action to the server -func (v *Game) OnPlayerCast(missileID int, targetX, targetY float64) { - err := v.gameClient.SendPacketToServer(d2netpacket.CreateCastPacket(v.gameClient.PlayerID, missileID, targetX, targetY)) +func (v *Game) OnPlayerCast(skillID int, targetX, targetY float64) { + err := v.gameClient.SendPacketToServer(d2netpacket.CreateCastPacket(v.gameClient.PlayerID, skillID, targetX, targetY)) if err != nil { - fmt.Printf(castErrStr, v.gameClient.PlayerID, missileID, targetX, targetY) + fmt.Printf(castErrStr, v.gameClient.PlayerID, skillID, targetX, targetY) } } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index c2d5638e..f74cd8f4 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -16,6 +16,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" @@ -35,7 +36,6 @@ type Panel interface { } const ( - initialMissileID = 59 expBarWidth = 120.0 staminaBarWidth = 102.0 globeHeight = 80 @@ -65,7 +65,8 @@ type GameControls struct { hpManaStatusSprite *d2ui.Sprite mainPanel *d2ui.Sprite menuButton *d2ui.Sprite - skillIcon *d2ui.Sprite + leftSkill *SkillResource + rightSkill *SkillResource zoneChangeText *d2ui.Label nameLabel *d2ui.Label hpManaStatsLabel *d2ui.Label @@ -86,15 +87,21 @@ type ActionableRegion struct { Rect d2geom.Rectangle } +type SkillResource struct { + SkillResourcePath string + IconNumber int + SkillIcon *d2ui.Sprite +} + const ( // Since they require special handling, not considering (1) globes, (2) content of the mini panel, (3) belt leftSkill ActionableType = iota - leftSelect + newStats xp walkRun stamina miniPnl - rightSelect + newSkills rightSkill hpGlobe manaGlobe @@ -115,14 +122,6 @@ func NewGameControls( guiManager *d2gui.GuiManager, isSinglePlayer bool, ) (*GameControls, error) { - missileID := initialMissileID - - err := term.BindAction("setmissile", "set missile id to summon on right click", func(id int) { - missileID = id - }) - if err != nil { - return nil, err - } zoneLabel := ui.NewLabel(d2resource.Font30, d2resource.PaletteUnits) zoneLabel.Alignment = d2gui.HorizontalAlignCenter @@ -175,18 +174,17 @@ func NewGameControls( heroStatsPanel: NewHeroStatsPanel(asset, ui, hero.Name(), hero.Class, hero.Stats), helpOverlay: help.NewHelpOverlay(asset, renderer, ui, guiManager), miniPanel: newMiniPanel(asset, ui, isSinglePlayer), - missileID: missileID, nameLabel: hoverLabel, zoneChangeText: zoneLabel, hpManaStatsLabel: globeStatsLabel, actionableRegions: []ActionableRegion{ {leftSkill, d2geom.Rectangle{Left: 115, Top: 550, Width: 50, Height: 50}}, - {leftSelect, d2geom.Rectangle{Left: 206, Top: 563, Width: 30, Height: 30}}, + {newStats, d2geom.Rectangle{Left: 206, Top: 563, Width: 30, Height: 30}}, {xp, d2geom.Rectangle{Left: 253, Top: 560, Width: 125, Height: 5}}, {walkRun, d2geom.Rectangle{Left: 255, Top: 573, Width: 17, Height: 20}}, {stamina, d2geom.Rectangle{Left: 273, Top: 573, Width: 105, Height: 20}}, {miniPnl, d2geom.Rectangle{Left: 393, Top: 563, Width: 12, Height: 23}}, - {rightSelect, d2geom.Rectangle{Left: 562, Top: 563, Width: 30, Height: 30}}, + {newSkills, d2geom.Rectangle{Left: 562, Top: 563, Width: 30, Height: 30}}, {rightSkill, d2geom.Rectangle{Left: 634, Top: 550, Width: 50, Height: 50}}, {hpGlobe, d2geom.Rectangle{Left: 30, Top: 525, Width: 65, Height: 50}}, {manaGlobe, d2geom.Rectangle{Left: 700, Top: 525, Width: 65, Height: 50}}, @@ -198,7 +196,7 @@ func NewGameControls( isSinglePlayer: isSinglePlayer, } - err = term.BindAction("freecam", "toggle free camera movement", func() { + err := term.BindAction("freecam", "toggle free camera movement", func() { gc.FreeCam = !gc.FreeCam }) @@ -206,6 +204,20 @@ func NewGameControls( return nil, err } + err = term.BindAction("setleftskill", "set skill to fire on left click", func(id int) { + skillRecord := d2datadict.SkillDetails[id] + gc.hero.LeftSkill = &d2hero.HeroSkill{SkillPoints: 0, SkillRecord: skillRecord, SkillDescriptionRecord: d2datadict.SkillDescriptions[skillRecord.Skilldesc]} + }) + + err = term.BindAction("setrightskill", "set skill to fire on right click", func(id int) { + skillRecord := d2datadict.SkillDetails[id] + gc.hero.RightSkill = &d2hero.HeroSkill{SkillPoints: 0, SkillRecord: skillRecord, SkillDescriptionRecord: d2datadict.SkillDescriptions[skillRecord.Skilldesc]} + }) + + if err != nil { + return nil, err + } + return gc, nil } @@ -297,7 +309,11 @@ func (g *GameControls) OnMouseButtonRepeat(event d2interface.MouseEvent) bool { if isLeft && shouldDoLeft && inRect && !g.hero.IsCasting() { g.lastLeftBtnActionTime = now - g.inputListener.OnPlayerMove(px, py) + if event.KeyMod() == d2enum.KeyModShift { + g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py) + } else { + g.inputListener.OnPlayerMove(px, py) + } if g.FreeCam { if event.Button() == d2enum.MouseButtonLeft { @@ -319,7 +335,7 @@ func (g *GameControls) OnMouseButtonRepeat(event d2interface.MouseEvent) bool { if isRight && shouldDoRight && inRect && !g.hero.IsCasting() { g.lastRightBtnActionTime = now - g.inputListener.OnPlayerCast(g.missileID, px, py) + g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py) return true } @@ -364,7 +380,11 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool { if event.Button() == d2enum.MouseButtonLeft && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() { g.lastLeftBtnActionTime = d2util.Now() - g.inputListener.OnPlayerMove(px, py) + if event.KeyMod() == d2enum.KeyModShift { + g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py) + } else { + g.inputListener.OnPlayerMove(px, py) + } return true } @@ -372,7 +392,7 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool { if event.Button() == d2enum.MouseButtonRight && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() { g.lastRightBtnActionTime = d2util.Now() - g.inputListener.OnPlayerCast(g.missileID, px, py) + g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py) return true } @@ -391,7 +411,12 @@ func (g *GameControls) Load() { g.menuButton, _ = g.uiManager.NewSprite(d2resource.MenuButton, d2resource.PaletteSky) _ = g.menuButton.SetCurrentFrame(2) - g.skillIcon, _ = g.uiManager.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky) + // TODO: temporarily hardcoded to Attack, should come from saved state for hero + genericSkillsSprite, _ := g.uiManager.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky) + attackIconID := 2 + + g.leftSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} + g.rightSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills} g.loadUIButtons() @@ -570,21 +595,26 @@ func (g *GameControls) Render(target d2interface.Surface) error { offset += w // Left skill - if err := g.skillIcon.SetCurrentFrame(2); err != nil { + skillResourcePath := g.getSkillResourceByClass(g.hero.LeftSkill.Charclass) + if skillResourcePath != g.leftSkill.SkillResourcePath { + g.leftSkill.SkillIcon, _ = g.uiManager.NewSprite(skillResourcePath, d2resource.PaletteSky) + } + + if err := g.leftSkill.SkillIcon.SetCurrentFrame(g.hero.LeftSkill.IconCel); err != nil { return err } - w, _ = g.skillIcon.GetCurrentFrameSize() + w, _ = g.leftSkill.SkillIcon.GetCurrentFrameSize() - g.skillIcon.SetPosition(offset, height) + g.leftSkill.SkillIcon.SetPosition(offset, height) - if err := g.skillIcon.Render(target); err != nil { + if err := g.leftSkill.SkillIcon.Render(target); err != nil { return err } offset += w - // Left skill selector + // New Stats Selector if err := g.mainPanel.SetCurrentFrame(1); err != nil { return err } @@ -668,7 +698,7 @@ func (g *GameControls) Render(target d2interface.Surface) error { offset += w - // Right skill selector + // New Skills Selector if err := g.mainPanel.SetCurrentFrame(4); err != nil { return err } @@ -684,15 +714,20 @@ func (g *GameControls) Render(target d2interface.Surface) error { offset += w // Right skill - if err := g.skillIcon.SetCurrentFrame(2); err != nil { + skillResourcePath = g.getSkillResourceByClass(g.hero.RightSkill.Charclass) + if skillResourcePath != g.rightSkill.SkillResourcePath { + g.rightSkill.SkillIcon, _ = g.uiManager.NewSprite(skillResourcePath, d2resource.PaletteSky) + } + + if err := g.rightSkill.SkillIcon.SetCurrentFrame(g.hero.RightSkill.IconCel); err != nil { return err } - w, _ = g.skillIcon.GetCurrentFrameSize() + w, _ = g.rightSkill.SkillIcon.GetCurrentFrameSize() - g.skillIcon.SetPosition(offset, height) + g.rightSkill.SkillIcon.SetPosition(offset, height) - if err := g.skillIcon.Render(target); err != nil { + if err := g.rightSkill.SkillIcon.Render(target); err != nil { return err } @@ -800,7 +835,7 @@ func (g *GameControls) ManaStatsIsVisible() bool { return g.manaStatsIsVisible } -// ToggleHpStats toggles the visibility of the hp and mana stats placed above their respective globe +// ToggleHpStats toggles the visibility of the hp and mana stats placed above their respective globe and load only if they do not match func (g *GameControls) ToggleHpStats() { g.hpStatsIsVisible = !g.hpStatsIsVisible } @@ -815,7 +850,7 @@ func (g *GameControls) onHoverActionable(item ActionableType) { switch item { case leftSkill: return - case leftSelect: + case newStats: return case xp: return @@ -825,7 +860,7 @@ func (g *GameControls) onHoverActionable(item ActionableType) { return case miniPnl: return - case rightSelect: + case newSkills: return case rightSkill: return @@ -843,8 +878,8 @@ func (g *GameControls) onClickActionable(item ActionableType) { switch item { case leftSkill: log.Println("Left Skill Action Pressed") - case leftSelect: - log.Println("Left Skill Selector Action Pressed") + case newStats: + log.Println("New Stats Selector Action Pressed") case xp: log.Println("XP Action Pressed") case walkRun: @@ -855,8 +890,8 @@ func (g *GameControls) onClickActionable(item ActionableType) { log.Println("Mini Panel Action Pressed") g.miniPanel.Toggle() - case rightSelect: - log.Println("Right Skill Selector Action Pressed") + case newSkills: + log.Println("New Skills Selector Action Pressed") case rightSkill: log.Println("Right Skill Action Pressed") case hpGlobe: @@ -879,3 +914,30 @@ func (g *GameControls) onClickActionable(item ActionableType) { log.Printf("Unrecognized ActionableType(%d) being clicked\n", item) } } + +func (g *GameControls) getSkillResourceByClass(class string) string { + resource := "" + + switch class { + case "": + resource = d2resource.GenericSkills + case "bar": + resource = d2resource.BarbarianSkills + case "nec": + resource = d2resource.NecromancerSkills + case "pal": + resource = d2resource.PaladinSkills + case "ass": + resource = d2resource.AssassinSkills + case "sor": + resource = d2resource.SorcererSkills + case "ama": + resource = d2resource.AmazonSkills + case "dru": + resource = d2resource.DruidSkills + default: + log.Fatalf("Unknown class token: '%s'", class) + } + + return resource +} diff --git a/d2game/d2player/player_state.go b/d2game/d2player/player_state.go index fb74da51..37bb5dae 100644 --- a/d2game/d2player/player_state.go +++ b/d2game/d2player/player_state.go @@ -24,6 +24,7 @@ type PlayerState struct { FilePath string `json:"-"` Equipment d2inventory.CharacterEquipment `json:"equipment"` Stats *d2hero.HeroStatsState `json:"stats"` + Skills *d2hero.HeroSkillsState `json:"skills"` X float64 `json:"x"` Y float64 `json:"y"` } @@ -50,17 +51,20 @@ func GetAllPlayerStates() []*PlayerState { gameState := LoadPlayerState(path.Join(basePath, file.Name())) if gameState == nil || gameState.HeroType == d2enum.HeroNone { - // temporarily loading default class stats if the character was created before saving stats was introduced - // to be removed in the future continue - } else if gameState.Stats == nil { - gameState.Stats = d2hero.CreateHeroStatsState(gameState.HeroType, d2datadict.CharStats[gameState.HeroType]) + } 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 := d2datadict.CharStats[gameState.HeroType] + gameState.Stats = d2hero.CreateHeroStatsState(gameState.HeroType, classStats) + gameState.Skills = d2hero.CreateHeroSkillsState(classStats) + if err := gameState.Save(); err != nil { fmt.Printf("failed to save game state!, err: %v\n", err) } } - result = append(result, gameState) + } return result @@ -98,6 +102,7 @@ func CreatePlayerState(heroName string, hero d2enum.Hero, classStats *d2datadict HeroType: hero, Act: 1, Stats: d2hero.CreateHeroStatsState(hero, classStats), + Skills: d2hero.CreateHeroSkillsState(classStats), Equipment: d2inventory.HeroObjects[hero], FilePath: "", } diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index b22589e8..916d2586 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -186,7 +186,7 @@ func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error { } newPlayer := g.MapEngine.NewPlayer(player.ID, player.Name, player.X, player.Y, 0, - player.HeroType, player.Stats, &player.Equipment) + player.HeroType, player.Stats, player.Skills, &player.Equipment) g.Players[newPlayer.ID()] = newPlayer g.MapEngine.AddEntity(newPlayer) @@ -264,12 +264,19 @@ func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { direction := player.Position.DirectionTo(*d2vector.NewVector(castX, castY)) player.SetDirection(direction) + skill := d2datadict.SkillDetails[playerCast.SkillID] + missileRecord := d2datadict.GetMissileByName(skill.Cltmissile) + + if missileRecord == nil { + //TODO: handle casts that have no missiles(or have multiple missiles and require additional logic) + log.Println("Missile not found for skill ID", skill.ID) + return nil + } - // currently hardcoded to missile skill missile, err := g.MapEngine.NewMissile( int(player.Position.X()), int(player.Position.Y()), - d2datadict.Missiles[playerCast.SkillID], + d2datadict.Missiles[missileRecord.Id], ) if err != nil { @@ -281,6 +288,7 @@ func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { }) player.StartCasting(func() { + // shoot the missile after the player finished casting g.MapEngine.AddEntity(missile) }) diff --git a/d2networking/d2netpacket/packet_add_player.go b/d2networking/d2netpacket/packet_add_player.go index 98c1c810..b1095451 100644 --- a/d2networking/d2netpacket/packet_add_player.go +++ b/d2networking/d2netpacket/packet_add_player.go @@ -20,12 +20,13 @@ type AddPlayerPacket struct { HeroType d2enum.Hero `json:"hero"` Equipment d2inventory.CharacterEquipment `json:"equipment"` Stats *d2hero.HeroStatsState `json:"heroStats"` + Skills *d2hero.HeroSkillsState `json:"heroSkills"` } // CreateAddPlayerPacket returns a NetPacket which declares an // AddPlayerPacket with the data in given parameters. func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, - stats *d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) NetPacket { + stats *d2hero.HeroStatsState, skills *d2hero.HeroSkillsState, equipment d2inventory.CharacterEquipment) NetPacket { addPlayerPacket := AddPlayerPacket{ ID: id, Name: name, @@ -34,6 +35,7 @@ func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, HeroType: heroType, Equipment: equipment, Stats: stats, + Skills: skills, } b, _ := json.Marshal(addPlayerPacket) diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index cef44ea3..4d4f4008 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -346,6 +346,7 @@ func handleClientConnection(gameServer *GameServer, client ClientConnection, x, playerY, playerState.HeroType, playerState.Stats, + playerState.Skills, playerState.Equipment, ) @@ -370,6 +371,7 @@ func handleClientConnection(gameServer *GameServer, client ClientConnection, x, playerY, conPlayerState.HeroType, conPlayerState.Stats, + conPlayerState.Skills, conPlayerState.Equipment, ), )