Character stats (#458)

* Save/load hero stats and display them in stats panel.

* Load default hero state for characters created before saving stats was introduced

Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
This commit is contained in:
presiyan-ivanov 2020-06-25 21:56:49 +03:00 committed by GitHub
parent 6778658606
commit c64e9be78b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 472 additions and 165 deletions

View File

@ -18,6 +18,7 @@ Gürkan Kaymak
Maxime "malavv" Lavigne
Ripolak
dafe
presiyan
* DIABLO2 LOGO
Jose Pardilla (th3-prophetman)

View File

@ -98,8 +98,8 @@ func LoadCharStats(file []byte) {
InitStr: d.GetNumber("str", idx),
InitDex: d.GetNumber("dex", idx),
InitVit: d.GetNumber("int", idx),
InitEne: d.GetNumber("vit", idx),
InitVit: d.GetNumber("vit", idx),
InitEne: d.GetNumber("int", idx),
InitStamina: d.GetNumber("stamina", idx),
ManaRegen: d.GetNumber("ManaRegen", idx),

View File

@ -88,7 +88,7 @@ const (
GamePanels = "/data/global/ui/PANEL/800ctrlpnl7.dc6"
GameGlobeOverlap = "/data/global/ui/PANEL/overlap.DC6"
HealthMana = "/data/global/ui/PANEL/hlthmana.DC6"
HealthManaIndicator = "/data/global/ui/PANEL/hlthmana.DC6"
GameSmallMenuButton = "/data/global/ui/PANEL/menubutton.DC6" // TODO: Used for inventory popout
SkillIcon = "/data/global/ui/PANEL/Skillicon.DC6" // TODO: Used for skill icon button
AddSkillButton = "/data/global/ui/PANEL/level.DC6"

View File

@ -0,0 +1,61 @@
package d2hero
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
type HeroStatsState struct {
Level int `json:"level"`
Experience int `json:"experience"`
Vitality int `json:"vitality"`
Energy int `json:"energy"`
Strength int `json:"strength"`
Dexterity int `json:"dexterity"`
AttackRating int `json:"attackRating"`
DefenseRating int `json:"defenseRating"`
MaxStamina int `json:"maxStamina"`
Health int `json:"health"`
MaxHealth int `json:"maxHealth"`
Mana int `json:"mana"`
MaxMana int `json:"maxMana"`
FireResistance int `json:"fireResistance"`
ColdResistance int `json:"coldResistance"`
LightningResistance int `json:"lightningResistance"`
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
}
func CreateHeroStatsState(heroClass d2enum.Hero, classStats d2datadict.CharStatsRecord, level int, exp int) *HeroStatsState {
result := HeroStatsState{
Level: level,
Experience: exp,
NextLevelExp: d2datadict.GetExperienceBreakpoint(heroClass, 1),
Strength: classStats.InitStr,
Dexterity: classStats.InitDex,
Vitality: classStats.InitVit,
Energy: classStats.InitEne,
//TODO: proper formula for calculating health and mana
Health: classStats.InitVit * classStats.LifePerVit / 4,
MaxHealth: classStats.InitVit * classStats.LifePerVit / 4,
Mana: classStats.InitEne * classStats.ManaPerEne / 4,
MaxMana: classStats.InitEne * classStats.ManaPerEne / 4,
Stamina: classStats.InitStamina,
MaxStamina: classStats.InitStamina,
//TODO chance to hit, defense rating
}
//TODO: those are added only for demonstration purposes(to show that hp mana exp status bars and character stats panel get updated depending on current stats)
result.Health /= 2
result.Mana /= 3
result.Experience = result.NextLevelExp / 3
return &result
}

View File

@ -7,6 +7,7 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
@ -16,6 +17,8 @@ type Player struct {
mapEntity
composite *d2asset.Composite
Equipment d2inventory.CharacterEquipment
Stats d2hero.HeroStatsState
Class d2enum.Hero
Id string
direction int
name string
@ -31,7 +34,7 @@ type Player struct {
var baseWalkSpeed = 6.0
var baseRunSpeed = 9.0
func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero, equipment d2inventory.CharacterEquipment) *Player {
func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) *Player {
object := &d2datadict.ObjectLookupRecord{
Mode: d2enum.AnimationModePlayerTownNeutral.String(),
Base: "/data/global/chars",
@ -53,13 +56,18 @@ func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero
panic(err)
}
stats.NextLevelExp = d2datadict.GetExperienceBreakpoint(heroType, stats.Level)
stats.Stamina = stats.MaxStamina
result := &Player{
Id: id,
mapEntity: createMapEntity(x, y),
composite: composite,
Equipment: equipment,
Stats: stats,
direction: direction,
name: name,
Class: heroType,
nameLabel: d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic),
isRunToggled: true,
isInTown: true,

View File

@ -165,6 +165,7 @@ func (v *CharacterSelect) updateCharacterBoxes() {
// TODO: Generate or load the object from the actual player data...
v.characterImage[i] = d2mapentity.CreatePlayer("", "", 0, 0, 0,
v.gameStates[idx].HeroType,
*v.gameStates[idx].Stats,
d2inventory.HeroObjects[v.gameStates[idx].HeroType],
)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
@ -433,7 +434,7 @@ func (v SelectHeroClass) onExitButtonClicked() {
}
func (v SelectHeroClass) onOkButtonClicked() {
gameState := d2player.CreatePlayerState(v.heroNameTextbox.GetText(), v.selectedHero, v.hardcoreCheckbox.GetCheckState())
gameState := d2player.CreatePlayerState(v.heroNameTextbox.GetText(), v.selectedHero, *d2datadict.CharStats[v.selectedHero], v.hardcoreCheckbox.GetCheckState())
gameClient, _ := d2client.Create(d2clientconnectiontype.Local)
gameClient.Open(v.connectionHost, gameState.FilePath)
d2screen.SetNextScreen(CreateGame(gameClient))

View File

@ -28,29 +28,37 @@ type Panel interface {
// ID of missile to create when user right clicks.
var missileID = 59
var expBarWidth = 120.0
var staminaBarWidth = 102.0
var globeHeight = 80
var globeWidth = 80
type GameControls struct {
hero *d2mapentity.Player
mapEngine *d2mapengine.MapEngine
mapRenderer *d2maprenderer.MapRenderer
inventory *Inventory
heroStats *HeroStats
inputListener InputCallbackListener
FreeCam bool
lastMouseX int
lastMouseY int
hero *d2mapentity.Player
mapEngine *d2mapengine.MapEngine
mapRenderer *d2maprenderer.MapRenderer
inventory *Inventory
heroStatsPanel *HeroStatsPanel
inputListener InputCallbackListener
FreeCam bool
lastMouseX int
lastMouseY int
lastHealthPercent float64
lastManaPercent float64
// UI
globeSprite *d2ui.Sprite
mainPanel *d2ui.Sprite
menuButton *d2ui.Sprite
skillIcon *d2ui.Sprite
zoneChangeText *d2ui.Label
nameLabel *d2ui.Label
runButton d2ui.Button
isZoneTextShown bool
actionableRegions []ActionableRegion
globeSprite *d2ui.Sprite
hpManaStatusSprite *d2ui.Sprite
hpStatusBar d2render.Surface
manaStatusBar d2render.Surface
mainPanel *d2ui.Sprite
menuButton *d2ui.Sprite
skillIcon *d2ui.Sprite
zoneChangeText *d2ui.Label
nameLabel *d2ui.Label
runButton d2ui.Button
isZoneTextShown bool
actionableRegions []ActionableRegion
}
type ActionableType int
@ -76,9 +84,9 @@ func NewGameControls(hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine,
missileID = id
})
label := d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits)
label.Color = color.RGBA{R: 255, G: 88, B: 82, A: 255}
label.Alignment = d2ui.LabelAlignCenter
zoneLabel := d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits)
zoneLabel.Color = color.RGBA{R: 255, G: 88, B: 82, A: 255}
zoneLabel.Alignment = d2ui.LabelAlignCenter
nameLabel := d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic)
nameLabel.Alignment = d2ui.LabelAlignCenter
@ -91,9 +99,9 @@ func NewGameControls(hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine,
inputListener: inputListener,
mapRenderer: mapRenderer,
inventory: NewInventory(),
heroStats: NewHeroStats(),
heroStatsPanel: NewHeroStatsPanel(hero.Name(), hero.Class, hero.Stats),
nameLabel: &nameLabel,
zoneChangeText: &label,
zoneChangeText: &zoneLabel,
actionableRegions: []ActionableRegion{
{leftSkill, d2common.Rectangle{Left: 115, Top: 550, Width: 50, Height: 50}},
{leftSelec, d2common.Rectangle{Left: 206, Top: 563, Width: 30, Height: 30}},
@ -147,9 +155,9 @@ func (g *GameControls) OnKeyRepeat(event d2input.KeyEvent) bool {
func (g *GameControls) OnKeyDown(event d2input.KeyEvent) bool {
switch event.Key {
case d2input.KeyEscape:
if g.inventory.IsOpen() || g.heroStats.IsOpen() {
if g.inventory.IsOpen() || g.heroStatsPanel.IsOpen() {
g.inventory.Close()
g.heroStats.Close()
g.heroStatsPanel.Close()
g.updateLayout()
break
}
@ -157,7 +165,7 @@ func (g *GameControls) OnKeyDown(event d2input.KeyEvent) bool {
g.inventory.Toggle()
g.updateLayout()
case d2input.KeyC:
g.heroStats.Toggle()
g.heroStatsPanel.Toggle()
g.updateLayout()
case d2input.KeyR:
g.onToggleRunButton()
@ -265,6 +273,11 @@ func (g *GameControls) Load() {
animation, _ := d2asset.LoadAnimation(d2resource.GameGlobeOverlap, d2resource.PaletteSky)
g.globeSprite, _ = d2ui.LoadSprite(animation)
animation, _ = d2asset.LoadAnimation(d2resource.HealthManaIndicator, d2resource.PaletteSky)
g.hpManaStatusSprite, _ = d2ui.LoadSprite(animation)
g.hpStatusBar, _ = d2render.NewSurface(globeWidth, globeHeight, d2render.FilterNearest)
animation, _ = d2asset.LoadAnimation(d2resource.GamePanels, d2resource.PaletteSky)
g.mainPanel, _ = d2ui.LoadSprite(animation)
@ -277,7 +290,7 @@ func (g *GameControls) Load() {
g.loadUIButtons()
g.inventory.Load()
g.heroStats.Load()
g.heroStatsPanel.Load()
}
func (g *GameControls) loadUIButtons() {
@ -309,7 +322,7 @@ func (g *GameControls) updateLayout() {
// todo : add same logic when adding quest log and skill tree
isRightPanelOpen = g.inventory.isOpen || isRightPanelOpen
isLeftPanelOpen = g.heroStats.isOpen || isLeftPanelOpen
isLeftPanelOpen = g.heroStatsPanel.isOpen || isLeftPanelOpen
if isRightPanelOpen == isLeftPanelOpen {
g.mapRenderer.ViewportDefault()
@ -342,7 +355,7 @@ func (g *GameControls) Render(target d2render.Surface) {
}
g.inventory.Render(target)
g.heroStats.Render(target)
g.heroStatsPanel.Render(target)
width, height := target.GetSize()
offset := 0
@ -357,6 +370,24 @@ func (g *GameControls) Render(target d2render.Surface) {
g.globeSprite.SetCurrentFrame(0)
g.globeSprite.SetPosition(offset+28, height-5)
g.globeSprite.Render(target)
// Health status bar
healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth)
hpBarHeight := int(healthPercent * float64(globeHeight))
if g.lastHealthPercent != healthPercent {
g.hpStatusBar, _ = d2render.NewSurface(globeWidth, hpBarHeight, d2render.FilterNearest)
g.hpManaStatusSprite.SetCurrentFrame(0)
g.hpStatusBar.PushTranslation(0, hpBarHeight)
g.hpManaStatusSprite.Render(g.hpStatusBar)
g.hpStatusBar.Pop()
g.lastHealthPercent = healthPercent
}
target.PushTranslation(30, 508+(globeHeight-hpBarHeight))
target.Render(g.hpStatusBar)
target.Pop()
offset += w
// Left skill
@ -380,6 +411,19 @@ func (g *GameControls) Render(target d2render.Surface) {
g.mainPanel.Render(target)
offset += w
// Stamina status bar
target.PushTranslation(273, 572)
target.PushCompositeMode(d2render.CompositeModeLighter)
staminaPercent := float64(g.hero.Stats.Stamina) / float64(g.hero.Stats.MaxStamina)
target.DrawRect(int(staminaPercent*staminaBarWidth), 19, color.RGBA{R: 175, G: 136, B: 72, A: 200})
target.PopN(2)
// Experience status bar
target.PushTranslation(256, 561)
expPercent := float64(g.hero.Stats.Experience) / float64(g.hero.Stats.NextLevelExp)
target.DrawRect(int(expPercent*expBarWidth), 2, color.RGBA{R: 255, G: 255, B: 255, A: 255})
target.Pop()
// Center menu button
g.menuButton.SetCurrentFrame(0)
w, _ = g.mainPanel.GetCurrentFrameSize()
@ -401,7 +445,7 @@ func (g *GameControls) Render(target d2render.Surface) {
offset += w
// Right skill
g.skillIcon.SetCurrentFrame(10)
g.skillIcon.SetCurrentFrame(2)
w, _ = g.skillIcon.GetCurrentFrameSize()
g.skillIcon.SetPosition(offset, height)
g.skillIcon.Render(target)
@ -417,11 +461,30 @@ func (g *GameControls) Render(target d2render.Surface) {
g.globeSprite.SetCurrentFrame(1)
g.globeSprite.SetPosition(offset+8, height-8)
g.globeSprite.Render(target)
g.globeSprite.Render(target)
// Mana status bar
manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana)
manaBarHeight := int(manaPercent * float64(globeHeight))
if manaPercent != g.lastManaPercent {
g.manaStatusBar, _ = d2render.NewSurface(globeWidth, manaBarHeight, d2render.FilterNearest)
g.hpManaStatusSprite.SetCurrentFrame(1)
g.manaStatusBar.PushTranslation(0, manaBarHeight)
g.hpManaStatusSprite.Render(g.manaStatusBar)
g.manaStatusBar.Pop()
g.lastManaPercent = manaPercent
}
target.PushTranslation(offset+8, 508+(globeHeight-manaBarHeight))
target.Render(g.manaStatusBar)
target.Pop()
if g.isZoneTextShown {
g.zoneChangeText.SetPosition(width/2, height/4)
g.zoneChangeText.Render(target)
}
}
func (g *GameControls) SetZoneChangeText(text string) {

View File

@ -0,0 +1,283 @@
package d2player
import (
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
type PanelText struct {
X int
Y int
Height int
Text string
Font string
AlignCenter bool
}
type StatsPanelLabels struct {
Level d2ui.Label
Experience d2ui.Label
NextLevelExp d2ui.Label
Strength d2ui.Label
Dexterity d2ui.Label
Vitality d2ui.Label
Energy d2ui.Label
Health d2ui.Label
MaxHealth d2ui.Label
Mana d2ui.Label
MaxMana d2ui.Label
MaxStamina d2ui.Label
Stamina d2ui.Label
}
var StaticTextLabels = []PanelText{
{X: 110, Y: 100, Text: "Level", Font: d2resource.Font6, AlignCenter: true},
{X: 200, Y: 100, Text: "Experience", Font: d2resource.Font6, AlignCenter: true},
{X: 330, Y: 100, Text: "Next Level", Font: d2resource.Font6, AlignCenter: true},
{X: 100, Y: 150, Text: "Strength", Font: d2resource.Font6},
{X: 100, Y: 213, Text: "Dexterity", Font: d2resource.Font6},
{X: 100, Y: 300, Text: "Vitality", Font: d2resource.Font6},
{X: 100, Y: 360, Text: "Energy", Font: d2resource.Font6},
{X: 280, Y: 260, Text: "Defense", Font: d2resource.Font6},
{X: 280, Y: 300, Text: "Stamina", Font: d2resource.Font6, AlignCenter: true},
{X: 280, Y: 322, Text: "Life", Font: d2resource.Font6, AlignCenter: true},
{X: 280, Y: 360, Text: "Mana", Font: d2resource.Font6, AlignCenter: true},
// can't use "Fire\nResistance" because line spacing is too big and breaks the layout
{X: 310, Y: 395, Text: "Fire", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 402, Text: "Resistance", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 420, Text: "Cold", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 427, Text: "Resistance", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 445, Text: "Lightning", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 452, Text: "Resistance", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 468, Text: "Poison", Font: d2resource.Font6, AlignCenter: true},
{X: 310, Y: 477, Text: "Resistance", Font: d2resource.Font6, AlignCenter: true},
}
// stores all the labels that can change during gameplay(e.g. current level, current hp, mana, etc.)
var StatValueLabels = make([]d2ui.Label, 13)
type HeroStatsPanel struct {
frame *d2ui.Sprite
panel *d2ui.Sprite
heroState *d2hero.HeroStatsState
heroName string
heroClass d2enum.Hero
staticMenuImageCache *d2render.Surface
labels *StatsPanelLabels
originX int
originY int
isOpen bool
}
func NewHeroStatsPanel(heroName string, heroClass d2enum.Hero, heroState d2hero.HeroStatsState) *HeroStatsPanel {
originX := 0
originY := 0
return &HeroStatsPanel{
originX: originX,
originY: originY,
heroState: &heroState,
heroName: heroName,
heroClass: heroClass,
labels: &StatsPanelLabels{},
}
}
func (s *HeroStatsPanel) Load() {
animation, _ := d2asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky)
s.frame, _ = d2ui.LoadSprite(animation)
animation, _ = d2asset.LoadAnimation(d2resource.InventoryCharacterPanel, d2resource.PaletteSky)
s.panel, _ = d2ui.LoadSprite(animation)
s.initStatValueLabels()
}
func (s *HeroStatsPanel) IsOpen() bool {
return s.isOpen
}
func (s *HeroStatsPanel) Toggle() {
s.isOpen = !s.isOpen
}
func (s *HeroStatsPanel) Open() {
s.isOpen = true
}
func (s *HeroStatsPanel) Close() {
s.isOpen = false
}
func (s *HeroStatsPanel) Render(target d2render.Surface) {
if !s.isOpen {
return
}
if s.staticMenuImageCache == nil {
frameWidth, frameHeight := s.frame.GetFrameBounds()
framesCount := s.frame.GetFrameCount()
surface, err := d2render.NewSurface(frameWidth*framesCount, frameHeight*framesCount, d2render.FilterNearest)
if err != nil {
return
}
s.staticMenuImageCache = &surface
s.renderStaticMenu(*s.staticMenuImageCache)
}
target.Render(*s.staticMenuImageCache)
s.renderStatValues(target)
}
func (s *HeroStatsPanel) renderStaticMenu(target d2render.Surface) {
x, y := s.originX, s.originY
// Frame
// Top left
s.frame.SetCurrentFrame(0)
w, h := s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x += w
y += h
// Top right
s.frame.SetCurrentFrame(1)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, s.originY+h)
s.frame.Render(target)
x = s.originX
// Right
s.frame.SetCurrentFrame(2)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
y += h
// Bottom left
s.frame.SetCurrentFrame(3)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x += w
// Bottom right
s.frame.SetCurrentFrame(4)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x, y = s.originX, s.originY
y += 64
x += 80
// Panel
// Top left
s.panel.SetCurrentFrame(0)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
x += w
// Top right
s.panel.SetCurrentFrame(1)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
y += h
// Bottom right
s.panel.SetCurrentFrame(3)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
// Bottom left
s.panel.SetCurrentFrame(2)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x-w, y+h)
s.panel.Render(target)
var label d2ui.Label
// all static labels are not stored since we use them only once to generate the image cache
for _, textElement := range StaticTextLabels {
label = s.createTextLabel(textElement)
label.Render(target)
}
// hero name and class are part of the static image cache since they don't change after we enter the world
label = s.createTextLabel(PanelText{X: 165, Y: 72, Text: s.heroName, Font: d2resource.Font16, AlignCenter: true})
label.Render(target)
label = s.createTextLabel(PanelText{X: 330, Y: 72, Text: s.heroClass.String(), Font: d2resource.Font16, AlignCenter: true})
label.Render(target)
}
func (s *HeroStatsPanel) initStatValueLabels() {
s.labels.Level = s.createStatValueLabel(s.heroState.Level, 112, 110)
s.labels.Experience = s.createStatValueLabel(s.heroState.Experience, 200, 110)
s.labels.NextLevelExp = s.createStatValueLabel(s.heroState.NextLevelExp, 330, 110)
s.labels.Strength = s.createStatValueLabel(s.heroState.Strength, 175, 147)
s.labels.Dexterity = s.createStatValueLabel(s.heroState.Dexterity, 175, 207)
s.labels.Vitality = s.createStatValueLabel(s.heroState.Vitality, 175, 295)
s.labels.Energy = s.createStatValueLabel(s.heroState.Energy, 175, 355)
s.labels.MaxStamina = s.createStatValueLabel(s.heroState.MaxStamina, 330, 295)
s.labels.Stamina = s.createStatValueLabel(s.heroState.Stamina, 370, 295)
s.labels.MaxHealth = s.createStatValueLabel(s.heroState.MaxHealth, 330, 320)
s.labels.Health = s.createStatValueLabel(s.heroState.Health, 370, 320)
s.labels.MaxMana = s.createStatValueLabel(s.heroState.MaxMana, 330, 355)
s.labels.Mana = s.createStatValueLabel(s.heroState.Mana, 370, 355)
}
func (s *HeroStatsPanel) renderStatValues(target d2render.Surface) {
s.renderStatValueNum(s.labels.Level, s.heroState.Level, target)
s.renderStatValueNum(s.labels.Experience, s.heroState.Experience, target)
s.renderStatValueNum(s.labels.NextLevelExp, s.heroState.NextLevelExp, target)
s.renderStatValueNum(s.labels.Strength, s.heroState.Strength, target)
s.renderStatValueNum(s.labels.Dexterity, s.heroState.Dexterity, target)
s.renderStatValueNum(s.labels.Vitality, s.heroState.Vitality, target)
s.renderStatValueNum(s.labels.Energy, s.heroState.Energy, target)
s.renderStatValueNum(s.labels.MaxHealth, s.heroState.MaxHealth, target)
s.renderStatValueNum(s.labels.Health, s.heroState.Health, target)
s.renderStatValueNum(s.labels.MaxStamina, s.heroState.MaxStamina, target)
s.renderStatValueNum(s.labels.Stamina, s.heroState.Stamina, target)
s.renderStatValueNum(s.labels.MaxMana, s.heroState.MaxMana, target)
s.renderStatValueNum(s.labels.Mana, s.heroState.Mana, target)
}
func (s *HeroStatsPanel) renderStatValueNum(label d2ui.Label, value int, target d2render.Surface) {
label.SetText(strconv.Itoa(value))
label.Render(target)
}
func (s *HeroStatsPanel) createStatValueLabel(stat int, x int, y int) d2ui.Label {
text := strconv.Itoa(stat)
return s.createTextLabel(PanelText{X: x, Y: y, Text: text, Font: d2resource.Font16, AlignCenter: true})
}
func (s *HeroStatsPanel) createTextLabel(element PanelText) d2ui.Label {
label := d2ui.CreateLabel(element.Font, d2resource.PaletteStatic)
if element.AlignCenter {
label.Alignment = d2ui.LabelAlignCenter
}
label.SetText(element.Text)
label.SetPosition(element.X, element.Y)
return label
}

View File

@ -9,7 +9,9 @@ import (
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
)
@ -20,6 +22,7 @@ type PlayerState struct {
Act int `json:"act"`
FilePath string `json:"-"`
Equipment d2inventory.CharacterEquipment `json:"equipment"`
Stats *d2hero.HeroStatsState `json:"stats"`
X float64 `json:"x"`
Y float64 `json:"y"`
}
@ -42,7 +45,13 @@ func GetAllPlayerStates() []*PlayerState {
gameState := LoadPlayerState(path.Join(basePath, file.Name()))
if gameState == nil {
continue
// temporarily loading default class stats if the character was created before saving stats was introduced
// to be removed in the future
} else if gameState.Stats == nil {
gameState.Stats = d2hero.CreateHeroStatsState(gameState.HeroType, *d2datadict.CharStats[gameState.HeroType], 1, 0)
gameState.Save()
}
result = append(result, gameState)
}
return result
@ -70,11 +79,12 @@ func LoadPlayerState(path string) *PlayerState {
return result
}
func CreatePlayerState(heroName string, hero d2enum.Hero, hardcore bool) *PlayerState {
func CreatePlayerState(heroName string, hero d2enum.Hero, classStats d2datadict.CharStatsRecord, hardcore bool) *PlayerState {
result := &PlayerState{
HeroName: heroName,
HeroType: hero,
Act: 1,
Stats: d2hero.CreateHeroStatsState(hero, classStats, 1, 0),
Equipment: d2inventory.HeroObjects[hero],
FilePath: "",
}

View File

@ -1,124 +0,0 @@
package d2player
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
type HeroStats struct {
frame *d2ui.Sprite
panel *d2ui.Sprite
originX int
originY int
isOpen bool
}
func NewHeroStats() *HeroStats {
originX := 0
originY := 0
return &HeroStats{
originX: originX,
originY: originY,
}
}
func (s *HeroStats) Load() {
animation, _ := d2asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky)
s.frame, _ = d2ui.LoadSprite(animation)
animation, _ = d2asset.LoadAnimation(d2resource.InventoryCharacterPanel, d2resource.PaletteSky)
s.panel, _ = d2ui.LoadSprite(animation)
}
func (s *HeroStats) IsOpen() bool {
return s.isOpen
}
func (s *HeroStats) Toggle() {
s.isOpen = !s.isOpen
}
func (s *HeroStats) Open() {
s.isOpen = true
}
func (s *HeroStats) Close() {
s.isOpen = false
}
func (s *HeroStats) Render(target d2render.Surface) {
if !s.isOpen {
return
}
x, y := s.originX, s.originY
// Frame
// Top left
s.frame.SetCurrentFrame(0)
w, h := s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x += w
y += h
// Top right
s.frame.SetCurrentFrame(1)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, s.originY+h)
s.frame.Render(target)
x = s.originX
// Right
s.frame.SetCurrentFrame(2)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
y += h
// Bottom left
s.frame.SetCurrentFrame(3)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x += w
// Bottom right
s.frame.SetCurrentFrame(4)
w, h = s.frame.GetCurrentFrameSize()
s.frame.SetPosition(x, y+h)
s.frame.Render(target)
x, y = s.originX, s.originY
y += 64
x += 80
// Panel
// Top left
s.panel.SetCurrentFrame(0)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
x += w
// Top right
s.panel.SetCurrentFrame(1)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
y += h
// Bottom right
s.panel.SetCurrentFrame(3)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x, y+h)
s.panel.Render(target)
// Bottom left
s.panel.SetCurrentFrame(2)
w, h = s.panel.GetCurrentFrameSize()
s.panel.SetPosition(x-w, y+h)
s.panel.Render(target)
}

View File

@ -80,7 +80,7 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error {
log.Printf("Player id set to %s", serverInfo.PlayerId)
case d2netpackettype.AddPlayer:
player := packet.PacketData.(d2netpacket.AddPlayerPacket)
newPlayer := d2mapentity.CreatePlayer(player.Id, player.Name, player.X, player.Y, 0, player.HeroType, player.Equipment)
newPlayer := d2mapentity.CreatePlayer(player.Id, player.Name, player.X, player.Y, 0, player.HeroType, player.Stats, player.Equipment)
g.Players[newPlayer.Id] = newPlayer
g.MapEngine.AddEntity(newPlayer)
case d2netpackettype.MovePlayer:

View File

@ -2,6 +2,7 @@ package d2netpacket
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype"
)
@ -13,9 +14,10 @@ type AddPlayerPacket struct {
Y int `json:"y"`
HeroType d2enum.Hero `json:"hero"`
Equipment d2inventory.CharacterEquipment `json:"equipment"`
Stats d2hero.HeroStatsState `json:"heroStats"`
}
func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, equipment d2inventory.CharacterEquipment) NetPacket {
func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) NetPacket {
return NetPacket{
PacketType: d2netpackettype.AddPlayer,
PacketData: AddPlayerPacket{
@ -25,6 +27,7 @@ func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, equi
Y: y,
HeroType: heroType,
Equipment: equipment,
Stats: stats,
},
}
}

View File

@ -172,7 +172,7 @@ func OnClientConnected(client ClientConnection) {
playerState := client.GetPlayerState()
createPlayerPacket := d2netpacket.CreateAddPlayerPacket(client.GetUniqueId(), playerState.HeroName, int(sx*5)+3, int(sy*5)+3,
playerState.HeroType, playerState.Equipment)
playerState.HeroType, *playerState.Stats, playerState.Equipment)
for _, connection := range singletonServer.clientConnections {
connection.SendPacketToClient(createPlayerPacket)
if connection.GetUniqueId() == client.GetUniqueId() {
@ -181,7 +181,7 @@ func OnClientConnected(client ClientConnection) {
conPlayerState := connection.GetPlayerState()
client.SendPacketToClient(d2netpacket.CreateAddPlayerPacket(connection.GetUniqueId(), conPlayerState.HeroName,
int(conPlayerState.X*5)+3, int(conPlayerState.Y*5)+3, conPlayerState.HeroType, conPlayerState.Equipment))
int(conPlayerState.X*5)+3, int(conPlayerState.Y*5)+3, conPlayerState.HeroType, *conPlayerState.Stats, conPlayerState.Equipment))
}
}