diff --git a/Common/Hero.go b/Common/Hero.go new file mode 100644 index 00000000..d812bf6c --- /dev/null +++ b/Common/Hero.go @@ -0,0 +1,14 @@ +package Common + +type Hero int + +const ( + HeroNone Hero = 0 + HeroBarbarian Hero = 1 + HeroNecromancer Hero = 2 + HeroPaladin Hero = 3 + HeroAssassin Hero = 4 + HeroSorceress Hero = 5 + HeroAmazon Hero = 6 + HeroDruid Hero = 7 +) diff --git a/Common/Sprite.go b/Common/Sprite.go index f483ebbf..1dce9cb7 100644 --- a/Common/Sprite.go +++ b/Common/Sprite.go @@ -13,6 +13,8 @@ type Sprite struct { Directions uint32 FramesPerDirection uint32 Frames []*SpriteFrame + SpecialFrameTime int + StopOnLastFrame bool X, Y int Frame, Direction uint8 Blend bool @@ -50,6 +52,8 @@ func CreateSprite(data []byte, palette Palette) *Sprite { FramesPerDirection: binary.LittleEndian.Uint32(data[20:24]), Animate: false, LastFrameTime: time.Now(), + SpecialFrameTime: -1, + StopOnLastFrame: false, } dataPointer := uint32(24) totalFrames := result.Directions * result.FramesPerDirection @@ -140,17 +144,35 @@ func (v *Sprite) updateAnimation() { if !v.Animate { return } - tNow := time.Now() - if v.LastFrameTime.Add(time.Millisecond * 25).After(tNow) { - return + var timePerFrame time.Duration + + if v.SpecialFrameTime >= 0 { + timePerFrame = time.Duration(float64(time.Millisecond) * (float64(v.SpecialFrameTime) / float64(len(v.Frames)))) + } else { + timePerFrame = time.Duration(float64(time.Second) * (1.0 / float64(len(v.Frames)))) } - v.LastFrameTime = tNow - v.Frame++ - if v.Frame >= uint8(v.FramesPerDirection) { - v.Frame = 0 + for time.Now().Sub(v.LastFrameTime) >= timePerFrame { + v.LastFrameTime = v.LastFrameTime.Add(timePerFrame) + v.Frame++ + if v.Frame >= uint8(v.FramesPerDirection) { + if v.StopOnLastFrame { + v.Frame = uint8(v.FramesPerDirection) - 1 + } else { + v.Frame = 0 + } + } } } +func (v *Sprite) ResetAnimation() { + v.LastFrameTime = time.Now() + v.Frame = 0 +} + +func (v *Sprite) OnLastFrame() bool { + return v.Frame == uint8(v.FramesPerDirection-1) +} + // GetFrameSize returns the size of the specific frame func (v *Sprite) GetFrameSize(frame int) (width, height uint32) { for v.Frames[frame].Loaded == false { diff --git a/Scenes/CharacterSelect.go b/Scenes/CharacterSelect.go index 65d7621d..61df3129 100644 --- a/Scenes/CharacterSelect.go +++ b/Scenes/CharacterSelect.go @@ -25,7 +25,8 @@ type CharacterSelect struct { func CreateCharacterSelect( fileProvider Common.FileProvider, sceneProvider SceneProvider, - uiManager *UI.Manager, soundManager *Sound.Manager, + uiManager *UI.Manager, + soundManager *Sound.Manager, ) *CharacterSelect { result := &CharacterSelect{ uiManager: uiManager, @@ -46,6 +47,7 @@ func (v *CharacterSelect) Load() []func() { func() { v.newCharButton = UI.CreateButton(UI.ButtonTypeTall, v.fileProvider, "CREATE NEW\nCHARACTER") v.newCharButton.MoveTo(33, 468) + v.newCharButton.OnActivated(func() { v.onNewCharButtonClicked() }) v.uiManager.AddWidget(v.newCharButton) }, func() { @@ -75,12 +77,15 @@ func (v *CharacterSelect) Load() []func() { } } +func (v *CharacterSelect) onNewCharButtonClicked() { + v.sceneProvider.SetNextScene(CreateSelectHeroClass(v.fileProvider, v.sceneProvider, v.uiManager, v.soundManager)) +} + func (v *CharacterSelect) onExitButtonClicked() { v.sceneProvider.SetNextScene(CreateMainMenu(v.fileProvider, v.sceneProvider, v.uiManager, v.soundManager)) } func (v *CharacterSelect) Unload() { - } func (v *CharacterSelect) Render(screen *ebiten.Image) { @@ -88,5 +93,4 @@ func (v *CharacterSelect) Render(screen *ebiten.Image) { } func (v *CharacterSelect) Update(tickTime float64) { - } diff --git a/Scenes/SelectHeroClass.go b/Scenes/SelectHeroClass.go new file mode 100644 index 00000000..3de3c79c --- /dev/null +++ b/Scenes/SelectHeroClass.go @@ -0,0 +1,284 @@ +package Scenes + +import ( + "image" + + "github.com/essial/OpenDiablo2/Common" + "github.com/essial/OpenDiablo2/Palettes" + "github.com/essial/OpenDiablo2/ResourcePaths" + "github.com/essial/OpenDiablo2/Sound" + "github.com/essial/OpenDiablo2/UI" + "github.com/hajimehoshi/ebiten" +) + +type HeroStance int + +const ( + HeroStanceIdle HeroStance = 0 + HeroStanceIdleSelected HeroStance = 1 + HeroStanceApproaching HeroStance = 2 + HeroStanceSelected HeroStance = 3 + HeroStanceRetreating HeroStance = 4 +) + +type HeroRenderInfo struct { + Stance HeroStance + IdleSprite *Common.Sprite + IdleSelectedSprite *Common.Sprite + ForwardWalkSprite *Common.Sprite + ForwardWalkSpriteOverlay *Common.Sprite + SelectedSprite *Common.Sprite + SelectedSpriteOverlay *Common.Sprite + BackWalkSprite *Common.Sprite + BackWalkSpriteOverlay *Common.Sprite + SelectionBounds image.Rectangle +} + +type SelectHeroClass struct { + uiManager *UI.Manager + soundManager *Sound.Manager + fileProvider Common.FileProvider + sceneProvider SceneProvider + bgImage *Common.Sprite + campfire *Common.Sprite + headingLabel *UI.Label + heroRenderInfo map[Common.Hero]*HeroRenderInfo +} + +func CreateSelectHeroClass( + fileProvider Common.FileProvider, + sceneProvider SceneProvider, + uiManager *UI.Manager, soundManager *Sound.Manager, +) *SelectHeroClass { + result := &SelectHeroClass{ + uiManager: uiManager, + sceneProvider: sceneProvider, + fileProvider: fileProvider, + soundManager: soundManager, + heroRenderInfo: make(map[Common.Hero]*HeroRenderInfo), + } + return result +} + +func (v *SelectHeroClass) Load() []func() { + v.soundManager.PlayBGM(ResourcePaths.BGMTitle) + return []func(){ + func() { + v.bgImage = v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBackground, Palettes.Fechar) + v.bgImage.MoveTo(0, 0) + }, + func() { + v.headingLabel = UI.CreateLabel(v.fileProvider, ResourcePaths.Font30, Palettes.Units) + fontWidth, _ := v.headingLabel.GetSize() + v.headingLabel.MoveTo(400-int(fontWidth/2), 17) + v.headingLabel.SetText("Select Hero Class") + v.headingLabel.Alignment = UI.LabelAlignCenter + }, + func() { + v.campfire = v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectCampfire, Palettes.Fechar) + v.campfire.MoveTo(380, 335) + v.campfire.Animate = true + }, + func() { + v.heroRenderInfo[Common.HeroBarbarian] = &HeroRenderInfo{ + HeroStanceIdle, + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianUnselected, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianUnselectedH, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianForwardWalk, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianForwardWalkOverlay, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianSelected, Palettes.Fechar), + nil, + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelectBarbarianBackWalk, Palettes.Fechar), + nil, + image.Rectangle{Min: image.Point{364, 201}, Max: image.Point{90, 170}}, + } + v.heroRenderInfo[Common.HeroBarbarian].IdleSprite.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].IdleSprite.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].IdleSelectedSprite.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].IdleSelectedSprite.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSprite.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSprite.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSprite.SpecialFrameTime = 2500 + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSprite.StopOnLastFrame = true + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSpriteOverlay.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSpriteOverlay.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSpriteOverlay.SpecialFrameTime = 2500 + v.heroRenderInfo[Common.HeroBarbarian].ForwardWalkSpriteOverlay.StopOnLastFrame = true + v.heroRenderInfo[Common.HeroBarbarian].SelectedSprite.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].SelectedSprite.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].BackWalkSprite.MoveTo(400, 330) + v.heroRenderInfo[Common.HeroBarbarian].BackWalkSprite.Animate = true + v.heroRenderInfo[Common.HeroBarbarian].BackWalkSprite.SpecialFrameTime = 1000 + v.heroRenderInfo[Common.HeroBarbarian].BackWalkSprite.StopOnLastFrame = true + }, + func() { + v.heroRenderInfo[Common.HeroSorceress] = &HeroRenderInfo{ + HeroStanceIdle, + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressUnselected, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressUnselectedH, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressForwardWalk, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressForwardWalkOverlay, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressSelected, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressSelectedOverlay, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressBackWalk, Palettes.Fechar), + v.fileProvider.LoadSprite(ResourcePaths.CharacterSelecSorceressBackWalkOverlay, Palettes.Fechar), + image.Rectangle{Min: image.Point{580, 240}, Max: image.Point{65, 160}}, + } + v.heroRenderInfo[Common.HeroSorceress].IdleSprite.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].IdleSprite.Animate = true + v.heroRenderInfo[Common.HeroSorceress].IdleSelectedSprite.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].IdleSelectedSprite.Animate = true + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSprite.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSprite.Animate = true + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSprite.SpecialFrameTime = 2300 + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSprite.StopOnLastFrame = true + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSpriteOverlay.Blend = true + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSpriteOverlay.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSpriteOverlay.Animate = true + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSpriteOverlay.SpecialFrameTime = 2300 + v.heroRenderInfo[Common.HeroSorceress].ForwardWalkSpriteOverlay.StopOnLastFrame = true + v.heroRenderInfo[Common.HeroSorceress].SelectedSprite.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].SelectedSprite.Animate = true + v.heroRenderInfo[Common.HeroSorceress].SelectedSpriteOverlay.Blend = true + v.heroRenderInfo[Common.HeroSorceress].SelectedSpriteOverlay.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].SelectedSpriteOverlay.Animate = true + v.heroRenderInfo[Common.HeroSorceress].BackWalkSprite.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].BackWalkSprite.Animate = true + v.heroRenderInfo[Common.HeroSorceress].BackWalkSprite.SpecialFrameTime = 1200 + v.heroRenderInfo[Common.HeroSorceress].BackWalkSprite.StopOnLastFrame = true + v.heroRenderInfo[Common.HeroSorceress].BackWalkSpriteOverlay.Blend = true + v.heroRenderInfo[Common.HeroSorceress].BackWalkSpriteOverlay.MoveTo(626, 352) + v.heroRenderInfo[Common.HeroSorceress].BackWalkSpriteOverlay.Animate = true + v.heroRenderInfo[Common.HeroSorceress].BackWalkSpriteOverlay.SpecialFrameTime = 1200 + v.heroRenderInfo[Common.HeroSorceress].BackWalkSpriteOverlay.StopOnLastFrame = true + }, + } +} + +func (v *SelectHeroClass) Unload() { + +} + +func (v *SelectHeroClass) Render(screen *ebiten.Image) { + v.bgImage.DrawSegments(screen, 4, 3, 0) + v.headingLabel.Draw(screen) + for heroClass, heroInfo := range v.heroRenderInfo { + if heroInfo.Stance == HeroStanceIdle || heroInfo.Stance == HeroStanceIdleSelected { + v.renderHero(screen, heroClass) + } + } + for heroClass, heroInfo := range v.heroRenderInfo { + if heroInfo.Stance != HeroStanceIdle && heroInfo.Stance != HeroStanceIdleSelected { + v.renderHero(screen, heroClass) + } + } + v.campfire.Draw(screen) +} + +func (v *SelectHeroClass) Update(tickTime float64) { + canSelect := true + for _, info := range v.heroRenderInfo { + if info.Stance != HeroStanceIdle && info.Stance != HeroStanceIdleSelected && info.Stance != HeroStanceSelected { + canSelect = false + break + } + } + for heroType := range v.heroRenderInfo { + v.updateHeroSelectionHover(heroType, canSelect) + } +} + +func (v *SelectHeroClass) updateHeroSelectionHover(hero Common.Hero, canSelect bool) { + renderInfo := v.heroRenderInfo[hero] + switch renderInfo.Stance { + case HeroStanceApproaching: + if renderInfo.ForwardWalkSprite.OnLastFrame() { + renderInfo.Stance = HeroStanceSelected + renderInfo.SelectedSprite.ResetAnimation() + if renderInfo.SelectedSpriteOverlay != nil { + renderInfo.SelectedSpriteOverlay.ResetAnimation() + } + } + return + case HeroStanceRetreating: + if renderInfo.BackWalkSprite.OnLastFrame() { + renderInfo.Stance = HeroStanceIdle + renderInfo.IdleSprite.ResetAnimation() + } + return + } + if !canSelect { + return + } + if renderInfo.Stance == HeroStanceSelected { + return + } + mouseX := v.uiManager.CursorX + mouseY := v.uiManager.CursorY + b := renderInfo.SelectionBounds + mouseHover := (mouseX >= b.Min.X) && (mouseX <= b.Min.X+b.Max.X) && (mouseY >= b.Min.Y) && (mouseY <= b.Min.Y+b.Max.Y) + if mouseHover && v.uiManager.CursorButtonPressed(UI.CursorButtonLeft) { + // showEntryUi = true; + renderInfo.Stance = HeroStanceApproaching + renderInfo.ForwardWalkSprite.ResetAnimation() + if renderInfo.ForwardWalkSpriteOverlay != nil { + renderInfo.ForwardWalkSpriteOverlay.ResetAnimation() + } + for _, heroInfo := range v.heroRenderInfo { + if heroInfo.Stance != HeroStanceSelected { + continue + } + // PlayHeroDeselected(ri.Key); + heroInfo.Stance = HeroStanceRetreating + heroInfo.BackWalkSprite.ResetAnimation() + if heroInfo.BackWalkSpriteOverlay != nil { + heroInfo.BackWalkSpriteOverlay.ResetAnimation() + } + } + // selectedHero = hero; + // UpdateHeroText(); + // PlayHeroSelected(hero); + + return + } + + if mouseHover { + renderInfo.Stance = HeroStanceIdleSelected + } else { + renderInfo.Stance = HeroStanceIdle + } + + /* + if (selectedHero == null && mouseHover) + { + selectedHero = hero; + UpdateHeroText(); + } + */ + +} + +func (v *SelectHeroClass) renderHero(screen *ebiten.Image, hero Common.Hero) { + renderInfo := v.heroRenderInfo[hero] + switch renderInfo.Stance { + case HeroStanceIdle: + renderInfo.IdleSprite.Draw(screen) + case HeroStanceIdleSelected: + renderInfo.IdleSelectedSprite.Draw(screen) + case HeroStanceApproaching: + renderInfo.ForwardWalkSprite.Draw(screen) + if renderInfo.ForwardWalkSpriteOverlay != nil { + renderInfo.ForwardWalkSpriteOverlay.Draw(screen) + } + case HeroStanceSelected: + renderInfo.SelectedSprite.Draw(screen) + if renderInfo.SelectedSpriteOverlay != nil { + renderInfo.SelectedSpriteOverlay.Draw(screen) + } + case HeroStanceRetreating: + renderInfo.BackWalkSprite.Draw(screen) + if renderInfo.BackWalkSpriteOverlay != nil { + renderInfo.BackWalkSpriteOverlay.Draw(screen) + } + } +} diff --git a/UI/Button.go b/UI/Button.go index 63b5593c..f961c67a 100644 --- a/UI/Button.go +++ b/UI/Button.go @@ -40,24 +40,25 @@ const ( // ButtonLayout defines the type of buttons type ButtonLayout struct { - XSegments int //1 - YSegments int // 1 - ResourceName string - PaletteName Palettes.Palette + XSegments int //1 + YSegments int // 1 + ResourceName string // Font Name + PaletteName Palettes.Palette // Palette Toggleable bool // false BaseFrame int // 0 DisabledFrame int // -1 FontPath string // ResourcePaths.FontExocet10 ClickableRect *image.Rectangle // nil AllowFrameChange bool // true + TextOffset int // 0 } // ButtonLayouts define the type of buttons you can have var ButtonLayouts = map[ButtonType]ButtonLayout{ - ButtonTypeWide: {2, 1, ResourcePaths.WideButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontExocet10, nil, true}, - ButtonTypeShort: {1, 1, ResourcePaths.ShortButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontRediculous, nil, true}, - ButtonTypeMedium: {1, 1, ResourcePaths.MediumButtonBlank, Palettes.Units, false, 0, 0, ResourcePaths.FontExocet10, nil, true}, - ButtonTypeTall: {1, 1, ResourcePaths.TallButtonBlank, Palettes.Units, false, 0, 0, ResourcePaths.FontExocet10, nil, true}, + ButtonTypeWide: {2, 1, ResourcePaths.WideButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontExocet10, nil, true, 1}, + ButtonTypeShort: {1, 1, ResourcePaths.ShortButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontRediculous, nil, true, -1}, + ButtonTypeMedium: {1, 1, ResourcePaths.MediumButtonBlank, Palettes.Units, false, 0, 0, ResourcePaths.FontExocet10, nil, true, 0}, + ButtonTypeTall: {1, 1, ResourcePaths.TallButtonBlank, Palettes.Units, false, 0, 0, ResourcePaths.FontExocet10, nil, true, 5}, /* {eButtonType.Wide, new ButtonLayout { XSegments = 2, ResourceName = ResourcePaths.WideButtonBlank, PaletteName = Palettes.Units } }, {eButtonType.Narrow, new ButtonLayout { ResourceName = ResourcePaths.NarrowButtonBlank, PaletteName = Palettes.Units } }, @@ -124,11 +125,8 @@ func CreateButton(buttonType ButtonType, fileProvider Common.FileProvider, text result.normalImage, _ = ebiten.NewImage(int(result.width), int(result.height), ebiten.FilterNearest) _, fontHeight := font.GetTextMetrics(text) - textY := int((result.height/2)-(fontHeight/2)) + 6 - // Nasty size hack, please remove this - if buttonType == ButtonTypeShort { - textY -= 3 - } + textY := int((result.height/2)-(fontHeight/2)) + buttonLayout.TextOffset + buttonSprite.MoveTo(0, 0) buttonSprite.Blend = true buttonSprite.DrawSegments(result.normalImage, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.BaseFrame) diff --git a/UI/Font.go b/UI/Font.go index 42cbb91a..d5f5d6b2 100644 --- a/UI/Font.go +++ b/UI/Font.go @@ -61,7 +61,7 @@ func (v *Font) GetTextMetrics(text string) (width, height uint32) { curWidth := uint32(0) height = uint32(0) maxCharHeight := uint32(0) - for _, m := range v.metrics { + for _, m := range v.fontSprite.Frames { maxCharHeight = Common.Max(maxCharHeight, uint32(m.Height)) } for i := 0; i < len(text); i++ { @@ -100,7 +100,7 @@ func (v *Font) Draw(x, y int, text string, color color.Color, target *ebiten.Ima char := uint8(ch) metric := v.metrics[char] v.fontSprite.Frame = char - v.fontSprite.MoveTo(xPos, y+int(metric.Height)) + v.fontSprite.MoveTo(xPos, y+int(v.fontSprite.Frames[char].Height)) v.fontSprite.Draw(target) xPos += int(metric.Width) }