mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-06 11:34:15 -04:00
645 lines
15 KiB
Go
645 lines
15 KiB
Go
package d2player
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
|
)
|
|
|
|
const (
|
|
white = 0xffffffff
|
|
)
|
|
|
|
const ( // for the dc6 frames
|
|
questLogTopLeft = iota
|
|
questLogTopRight
|
|
questLogBottomLeft
|
|
questLogBottomRight
|
|
)
|
|
|
|
const (
|
|
questLogOffsetX, questLogOffsetY = 80, 64
|
|
)
|
|
|
|
const (
|
|
iconOffsetY = 88
|
|
questOffsetX, questOffsetY = 4, 4
|
|
socket1X = 100
|
|
socket2X = 200
|
|
socket3X = 300
|
|
socketUpY = 95
|
|
socketDownY = 190
|
|
)
|
|
|
|
const (
|
|
questLogCloseButtonX, questLogCloseButtonY = 358, 455
|
|
questLogDescrButtonX, questLogDescrButtonY = 308, 457
|
|
questNameLabelX, questNameLabelY = 240, 297
|
|
questDescrLabelX, questDescrLabelY = 90, 317
|
|
)
|
|
|
|
const (
|
|
questTabY = 66
|
|
questTabYOffset = 31
|
|
questTabBaseX = 86
|
|
questTabXOffset = 61
|
|
)
|
|
|
|
const questCompleteAnimationDuration = 3
|
|
|
|
func getPositionForSocket(number int) (x, y int) {
|
|
pos := []struct {
|
|
x int
|
|
y int
|
|
}{
|
|
{socket1X, socketUpY},
|
|
{socket2X, socketUpY},
|
|
{socket3X, socketUpY},
|
|
{socket1X, socketDownY},
|
|
{socket2X, socketDownY},
|
|
{socket3X, socketDownY},
|
|
}
|
|
|
|
return pos[number].x, pos[number].y
|
|
}
|
|
|
|
// NewQuestLog creates a new quest log
|
|
func NewQuestLog(asset *d2asset.AssetManager,
|
|
ui *d2ui.UIManager,
|
|
l d2util.LogLevel,
|
|
audioProvider d2interface.AudioProvider,
|
|
act int) *QuestLog {
|
|
originX := 0
|
|
originY := 0
|
|
|
|
//nolint:gomnd // this is only test
|
|
qs := map[int]int{
|
|
0: -2,
|
|
1: -2,
|
|
2: -2,
|
|
3: 0,
|
|
4: 1,
|
|
5: 4,
|
|
6: 3,
|
|
7: -1,
|
|
8: 0,
|
|
9: 0,
|
|
10: 0,
|
|
11: 0,
|
|
12: 0,
|
|
13: 0,
|
|
14: 0,
|
|
15: 0,
|
|
16: 0,
|
|
17: 0,
|
|
18: 0,
|
|
19: 0,
|
|
20: 0,
|
|
21: 0,
|
|
22: 0,
|
|
23: 0,
|
|
24: 0,
|
|
25: 0,
|
|
26: 0,
|
|
}
|
|
|
|
var quests [d2enum.ActsNumber]*questEntire
|
|
for i := 0; i < d2enum.ActsNumber; i++ {
|
|
quests[i] = &questEntire{WidgetGroup: ui.NewWidgetGroup(d2ui.RenderPriorityQuestLog)}
|
|
}
|
|
|
|
var tabs [d2enum.ActsNumber]questLogTab
|
|
for i := 0; i < d2enum.ActsNumber; i++ {
|
|
tabs[i] = questLogTab{}
|
|
}
|
|
|
|
// nolint:gomnd // this is only test, it also should come from save file
|
|
mpa := 2
|
|
|
|
ql := &QuestLog{
|
|
asset: asset,
|
|
uiManager: ui,
|
|
originX: originX,
|
|
originY: originY,
|
|
act: act,
|
|
tab: tabs,
|
|
quests: quests,
|
|
questStatus: qs,
|
|
maxPlayersAct: mpa,
|
|
audioProvider: audioProvider,
|
|
}
|
|
|
|
ql.Logger = d2util.NewLogger()
|
|
ql.Logger.SetLevel(l)
|
|
ql.Logger.SetPrefix(logPrefix)
|
|
|
|
return ql
|
|
}
|
|
|
|
// QuestLog represents the quest log
|
|
type QuestLog struct {
|
|
asset *d2asset.AssetManager
|
|
uiManager *d2ui.UIManager
|
|
panel *d2ui.Sprite
|
|
onCloseCb func()
|
|
panelGroup *d2ui.WidgetGroup
|
|
selectedTab int
|
|
selectedQuest int
|
|
act int
|
|
tab [d2enum.ActsNumber]questLogTab
|
|
audioProvider d2interface.AudioProvider
|
|
completeSound d2interface.SoundEffect
|
|
|
|
questName *d2ui.Label
|
|
questDescr *d2ui.Label
|
|
quests [d2enum.ActsNumber]*questEntire
|
|
questStatus map[int]int
|
|
maxPlayersAct int
|
|
|
|
originX int
|
|
originY int
|
|
isOpen bool
|
|
|
|
*d2util.Logger
|
|
}
|
|
|
|
type questEntire struct {
|
|
*d2ui.WidgetGroup
|
|
icons []*d2ui.Sprite
|
|
buttons []*d2ui.Button
|
|
sockets []*d2ui.Sprite
|
|
}
|
|
|
|
/* questIconTab returns path to quest animation using its
|
|
act and number. From d2resource:
|
|
QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"*/
|
|
func (s *QuestLog) questIconsTable(act, number int) string {
|
|
return fmt.Sprintf(d2resource.QuestLogAQuestAnimation, act, number+1)
|
|
}
|
|
|
|
const (
|
|
completedFrame = 24
|
|
inProgresFrame = 25
|
|
notStartedFrame = 26
|
|
)
|
|
|
|
const (
|
|
socketNormalFrame = 0
|
|
socketHighlightedFrame = 1
|
|
)
|
|
|
|
const questDescriptionLenght = 30
|
|
|
|
type questLogTab struct {
|
|
sprite *d2ui.Sprite
|
|
invisibleButton *d2ui.Button
|
|
}
|
|
|
|
// Load the data for the hero status panel
|
|
func (s *QuestLog) Load() {
|
|
var err error
|
|
|
|
s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog)
|
|
|
|
// quest completion sound.
|
|
s.completeSound, err = s.audioProvider.LoadSound(d2resource.QuestLogDoneSfx, false, false)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
frame := s.uiManager.NewUIFrame(d2ui.FrameLeft)
|
|
s.panelGroup.AddWidget(frame)
|
|
|
|
s.panel, err = s.uiManager.NewSprite(d2resource.QuestLogBg, d2resource.PaletteSky)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
w, h := frame.GetSize()
|
|
staticPanel := s.uiManager.NewCustomWidgetCached(s.renderStaticPanelFrames, w, h)
|
|
s.panelGroup.AddWidget(staticPanel)
|
|
|
|
closeButton := s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "")
|
|
closeButton.SetVisible(false)
|
|
closeButton.SetPosition(questLogCloseButtonX, questLogCloseButtonY)
|
|
closeButton.OnActivated(func() { s.Close() })
|
|
s.panelGroup.AddWidget(closeButton)
|
|
|
|
descrButton := s.uiManager.NewButton(d2ui.ButtonTypeQuestDescr, "")
|
|
descrButton.SetVisible(false)
|
|
descrButton.SetPosition(questLogDescrButtonX, questLogDescrButtonY)
|
|
descrButton.OnActivated(s.onDescrClicked)
|
|
s.panelGroup.AddWidget(descrButton)
|
|
|
|
s.questName = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
|
|
s.questName.Alignment = d2ui.HorizontalAlignCenter
|
|
s.questName.Color[0] = d2util.Color(white)
|
|
s.questName.SetPosition(questNameLabelX, questNameLabelY)
|
|
s.panelGroup.AddWidget(s.questName)
|
|
|
|
s.questDescr = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
|
|
s.questDescr.Alignment = d2ui.HorizontalAlignLeft
|
|
s.questDescr.Color[0] = d2util.Color(white)
|
|
s.questDescr.SetPosition(questDescrLabelX, questDescrLabelY)
|
|
s.panelGroup.AddWidget(s.questDescr)
|
|
|
|
s.loadTabs()
|
|
|
|
// creates quest boards for each act
|
|
for i := 0; i < d2enum.ActsNumber; i++ {
|
|
item, icons, buttons, sockets := s.loadQuestBoard(i + 1)
|
|
s.quests[i] = &questEntire{item, icons, buttons, sockets}
|
|
}
|
|
|
|
s.panelGroup.SetVisible(false)
|
|
}
|
|
|
|
// loadTabs loads quest log tabs
|
|
func (s *QuestLog) loadTabs() {
|
|
var err error
|
|
|
|
tabsResource := d2resource.WPTabs
|
|
|
|
// create tabs only for 'discovered' acts
|
|
for i := 0; i < s.maxPlayersAct; i++ {
|
|
currentValue := i
|
|
|
|
s.tab[i].sprite, err = s.uiManager.NewSprite(tabsResource, d2resource.PaletteSky)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
// nolint:gomnd // it's constant.
|
|
// each tab has two frames (active / inactive)
|
|
frame := 2 * currentValue
|
|
|
|
err := s.tab[i].sprite.SetCurrentFrame(frame)
|
|
if err != nil {
|
|
s.Errorf("Tabs sprite (%s) hasn't frame %d. %s", tabsResource, frame, err.Error())
|
|
}
|
|
|
|
s.tab[i].sprite.SetPosition(questTabBaseX+i*questTabXOffset, questTabY+questTabYOffset)
|
|
|
|
s.tab[i].invisibleButton = s.uiManager.NewButton(d2ui.ButtonTypeTabBlank, "")
|
|
s.tab[i].invisibleButton.SetPosition(questTabBaseX+i*questTabXOffset, questTabY)
|
|
s.tab[i].invisibleButton.OnActivated(func() { s.setTab(currentValue) })
|
|
|
|
s.panelGroup.AddWidget(s.tab[i].sprite)
|
|
s.panelGroup.AddWidget(s.tab[i].invisibleButton)
|
|
}
|
|
|
|
// sets tab to current player's act.
|
|
s.setTab(s.act - 1)
|
|
}
|
|
|
|
// loadQuestBoard creates quest fields (socket, button, icon) for specified act
|
|
func (s *QuestLog) loadQuestBoard(act int) (wg *d2ui.WidgetGroup, icons []*d2ui.Sprite, buttons []*d2ui.Button, sockets []*d2ui.Sprite) {
|
|
wg = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog)
|
|
|
|
// sets number of quests in act (for act 4 it's only 3, else 6)
|
|
var questsInAct int
|
|
if act == d2enum.Act4 {
|
|
questsInAct = d2enum.HalfQuestsNumber
|
|
} else {
|
|
questsInAct = d2enum.NormalActQuestsNumber
|
|
}
|
|
|
|
for n := 0; n < questsInAct; n++ {
|
|
cw := n
|
|
x, y := getPositionForSocket(n)
|
|
|
|
socket, err := s.uiManager.NewSprite(d2resource.QuestLogSocket, d2resource.PaletteSky)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
socket.SetPosition(x, y+iconOffsetY+questOffsetY)
|
|
sockets = append(sockets, socket)
|
|
|
|
icon, err := s.makeQuestIconForAct(act, n, x, y)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
icons = append(icons, icon)
|
|
|
|
button := s.uiManager.NewButton(d2ui.ButtonTypeBlankQuestBtn, "")
|
|
button.SetPosition(x+questOffsetX, y+questOffsetY)
|
|
button.SetEnabled(s.questStatus[s.cordsToQuestID(act, cw)] != d2enum.QuestStatusNotStarted)
|
|
buttons = append(buttons, button)
|
|
}
|
|
|
|
for i := 0; i < questsInAct; i++ {
|
|
currentQuest := i
|
|
|
|
// creates callback for quest button
|
|
buttons[i].OnActivated(func() {
|
|
var err error
|
|
|
|
// set normal (not-highlighted) frame for each quest socket
|
|
for j := 0; j < questsInAct; j++ {
|
|
err = sockets[j].SetCurrentFrame(socketNormalFrame)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
}
|
|
|
|
// highlights appropriate socket
|
|
err = sockets[currentQuest].SetCurrentFrame(socketHighlightedFrame)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
// sets quest labels
|
|
s.onQuestClicked(currentQuest + 1)
|
|
})
|
|
}
|
|
|
|
// adds sockets to widget group
|
|
for _, s := range sockets {
|
|
wg.AddWidget(s)
|
|
}
|
|
|
|
// adds buttons to widget group
|
|
for _, b := range buttons {
|
|
wg.AddWidget(b)
|
|
}
|
|
|
|
// adds icons to widget group
|
|
for _, i := range icons {
|
|
wg.AddWidget(i)
|
|
}
|
|
|
|
wg.SetVisible(false)
|
|
|
|
return wg, icons, buttons, sockets
|
|
}
|
|
|
|
func (s *QuestLog) makeQuestIconForAct(act, n, x, y int) (*d2ui.Sprite, error) {
|
|
iconResource := s.questIconsTable(act, n)
|
|
|
|
icon, err := s.uiManager.NewSprite(iconResource, d2resource.PaletteSky)
|
|
if err != nil {
|
|
s.Fatalf("during creating new quest icons for act %d (icon sprite %s doesn't exist). %s", act, iconResource, err.Error())
|
|
}
|
|
|
|
switch s.questStatus[s.cordsToQuestID(act, n)] {
|
|
case d2enum.QuestStatusCompleted:
|
|
err = icon.SetCurrentFrame(completedFrame)
|
|
case d2enum.QuestStatusCompleting:
|
|
// animation will be played after quest-log panel is opened (see s.playQuestAnimation)
|
|
err = icon.SetCurrentFrame(0)
|
|
case d2enum.QuestStatusNotStarted:
|
|
err = icon.SetCurrentFrame(notStartedFrame)
|
|
default:
|
|
err = icon.SetCurrentFrame(inProgresFrame)
|
|
}
|
|
|
|
icon.SetPosition(x+questOffsetX, y+questOffsetY+iconOffsetY)
|
|
|
|
return icon, err
|
|
}
|
|
|
|
// playQuestAnimations plays animations for quests (when status=questStatusCompleting)
|
|
func (s *QuestLog) playQuestAnimations() {
|
|
for j, i := range s.quests[s.selectedTab].icons {
|
|
questID := s.cordsToQuestID(s.selectedTab+1, j)
|
|
if s.questStatus[questID] == d2enum.QuestStatusCompleting {
|
|
s.completeSound.Play()
|
|
|
|
// quest should be highlighted and it's label should be displayed
|
|
s.quests[s.selectedTab].buttons[j].Activate()
|
|
|
|
i.SetPlayLength(questCompleteAnimationDuration)
|
|
i.PlayForward()
|
|
i.SetPlayLoop(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// stopPlayedAnimation stops currently played animations and sets quests in
|
|
// completing state to completed (should be used, when quest log is closing)
|
|
func (s *QuestLog) stopPlayedAnimations() {
|
|
// stops all played animations
|
|
for j, i := range s.quests[s.selectedTab].icons {
|
|
questID := s.cordsToQuestID(s.selectedTab+1, j)
|
|
if s.questStatus[questID] == d2enum.QuestStatusCompleting {
|
|
s.questStatus[questID] = d2enum.QuestStatusCompleted
|
|
|
|
err := i.SetCurrentFrame(completedFrame)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// setQuestLabel loads quest labels text (title and description)
|
|
func (s *QuestLog) setQuestLabel() {
|
|
if s.selectedQuest == d2enum.QuestNone {
|
|
s.questName.SetText("")
|
|
s.questDescr.SetText("")
|
|
|
|
return
|
|
}
|
|
|
|
s.questName.SetText(s.asset.TranslateString(fmt.Sprintf("qstsa%dq%d", s.selectedTab+1, s.selectedQuest)))
|
|
|
|
status := s.questStatus[s.cordsToQuestID(s.selectedTab+1, s.selectedQuest)-1]
|
|
switch status {
|
|
case d2enum.QuestStatusCompleted, d2enum.QuestStatusCompleting:
|
|
s.questDescr.SetText(
|
|
strings.Join(
|
|
d2util.SplitIntoLinesWithMaxWidth(
|
|
s.asset.TranslateString("qstsprevious"),
|
|
questDescriptionLenght),
|
|
"\n"),
|
|
)
|
|
case d2enum.QuestStatusNotStarted:
|
|
s.questDescr.SetText("")
|
|
default:
|
|
str := fmt.Sprintf("qstsa%dq%d%d", s.selectedTab+1, s.selectedQuest, status)
|
|
descr := s.asset.TranslateString(str)
|
|
|
|
// if description not found
|
|
if str == descr {
|
|
s.questDescr.SetText("")
|
|
} else {
|
|
s.questDescr.SetText(strings.Join(
|
|
d2util.SplitIntoLinesWithMaxWidth(
|
|
descr, questDescriptionLenght),
|
|
"\n"),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// switch all socket (in current tab) to normal state
|
|
func (s *QuestLog) clearHighlightment() {
|
|
for _, i := range s.quests[s.selectedTab].sockets {
|
|
err := i.SetCurrentFrame(socketNormalFrame)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *QuestLog) setTab(tab int) {
|
|
var mod int
|
|
|
|
// before we leafe current tab, we need to switch highlighted
|
|
// quest socket to normal frame
|
|
s.clearHighlightment()
|
|
|
|
s.selectedTab = tab
|
|
s.selectedQuest = d2enum.QuestNone
|
|
s.setQuestLabel()
|
|
s.playQuestAnimations()
|
|
|
|
// displays appropriate quests board
|
|
for i := 0; i < s.maxPlayersAct; i++ {
|
|
s.quests[i].SetVisible(tab == i)
|
|
}
|
|
|
|
// "highlights" appropriate tab
|
|
for i := 0; i < s.maxPlayersAct; i++ {
|
|
cv := i
|
|
|
|
// converts bool to 1/0
|
|
if cv == s.selectedTab {
|
|
mod = 0
|
|
} else {
|
|
mod = 1
|
|
}
|
|
|
|
// sets tab sprite to highlighted/non-highlighted
|
|
err := s.tab[cv].sprite.SetCurrentFrame(2*cv + mod)
|
|
if err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *QuestLog) onQuestClicked(number int) {
|
|
s.selectedQuest = number
|
|
s.setQuestLabel()
|
|
s.Infof("Quest number %d in tab %d clicked", number, s.selectedTab)
|
|
}
|
|
|
|
//
|
|
func (s *QuestLog) onDescrClicked() {
|
|
s.Info("Quest description button clicked")
|
|
}
|
|
|
|
// IsOpen returns true if the hero status panel is open
|
|
func (s *QuestLog) IsOpen() bool {
|
|
return s.isOpen
|
|
}
|
|
|
|
// Toggle toggles the visibility of the hero status panel
|
|
func (s *QuestLog) Toggle() {
|
|
if s.isOpen {
|
|
s.Close()
|
|
} else {
|
|
s.Open()
|
|
}
|
|
}
|
|
|
|
// Open opens the hero status panel
|
|
func (s *QuestLog) Open() {
|
|
s.isOpen = true
|
|
s.panelGroup.SetVisible(true)
|
|
s.setTab(s.selectedTab)
|
|
s.playQuestAnimations()
|
|
}
|
|
|
|
// Close closed the hero status panel
|
|
func (s *QuestLog) Close() {
|
|
s.isOpen = false
|
|
s.panelGroup.SetVisible(false)
|
|
|
|
for i := 0; i < s.maxPlayersAct; i++ {
|
|
s.quests[i].SetVisible(false)
|
|
}
|
|
|
|
s.stopPlayedAnimations()
|
|
|
|
s.onCloseCb()
|
|
}
|
|
|
|
// SetOnCloseCb the callback run on closing the HeroStatsPanel
|
|
func (s *QuestLog) SetOnCloseCb(cb func()) {
|
|
s.onCloseCb = cb
|
|
}
|
|
|
|
// Advance updates labels on the panel
|
|
func (s *QuestLog) Advance(elapsed float64) {
|
|
if !s.IsOpen() {
|
|
return
|
|
}
|
|
|
|
for j, i := range s.quests[s.selectedTab].icons {
|
|
questID := s.cordsToQuestID(s.selectedTab+1, j)
|
|
if s.questStatus[questID] == d2enum.QuestStatusCompleting {
|
|
if err := i.Advance(elapsed); err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
if i.GetCurrentFrame() == completedFrame {
|
|
s.questStatus[questID] = d2enum.QuestStatusCompleted
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// nolint:dupl // I think it is OK, to duplicate this function
|
|
func (s *QuestLog) renderStaticPanelFrames(target d2interface.Surface) {
|
|
frames := []int{
|
|
questLogTopLeft,
|
|
questLogTopRight,
|
|
questLogBottomRight,
|
|
questLogBottomLeft,
|
|
}
|
|
|
|
currentX := s.originX + questLogOffsetX
|
|
currentY := s.originY + questLogOffsetY
|
|
|
|
for _, frameIndex := range frames {
|
|
if err := s.panel.SetCurrentFrame(frameIndex); err != nil {
|
|
s.Error(err.Error())
|
|
}
|
|
|
|
w, h := s.panel.GetCurrentFrameSize()
|
|
|
|
switch frameIndex {
|
|
case questLogTopLeft:
|
|
s.panel.SetPosition(currentX, currentY+h)
|
|
currentX += w
|
|
case questLogTopRight:
|
|
s.panel.SetPosition(currentX, currentY+h)
|
|
currentY += h
|
|
case questLogBottomRight:
|
|
s.panel.SetPosition(currentX, currentY+h)
|
|
case questLogBottomLeft:
|
|
s.panel.SetPosition(currentX-w, currentY+h)
|
|
}
|
|
|
|
s.panel.Render(target)
|
|
}
|
|
}
|
|
|
|
func (s *QuestLog) cordsToQuestID(act, number int) int {
|
|
key := (act-1)*d2enum.NormalActQuestsNumber + number
|
|
if act > d2enum.Act4 {
|
|
key -= d2enum.HalfQuestsNumber
|
|
}
|
|
|
|
return key
|
|
}
|