package d2player import ( "fmt" "image/color" "strings" "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 ( act1 = iota + 1 act2 act3 act4 act5 ) const ( // for the dc6 frames questLogTopLeft = iota questLogTopRight questLogBottomLeft questLogBottomRight ) const ( questStatusCompleted = iota - 2 // quest completed questStatusCompleting // quest completed (need to play animation) questStatusNotStarted // quest not started yet questStatusInProgress // quest is in progress ) const ( normalActQuestsNumber = 6 act4QuestsNumber = 3 ) const ( questLogOffsetX, questLogOffsetY = 80, 64 ) const ( iconOffsetY = 88 questOffsetX, questOffsetY = 4, 4 q1SocketX, q1SocketY = 100, 95 q2SocketX, q2SocketY = 200, 95 q3SocketX, q3SocketY = 300, 95 q4SocketX, q4SocketY = 100, 190 q5SocketX, q5SocketY = 200, 190 q6SocketX, q6SocketY = 300, 190 ) const ( questLogCloseButtonX, questLogCloseButtonY = 358, 455 questLogDescrButtonX, questLogDescrButtonY = 308, 457 questNameLabelX, questNameLabelY = 240, 297 questDescrLabelX, questDescrLabelY = 90, 317 ) // toset const ( questTabY = 66 questTab1X = 85 questTab2X = 143 questTab3X = 201 questTab4X = 259 questTab5X = 317 ) const ( questLogTab1 = iota questLogTab2 questLogTab3 questLogTab4 questLogTab5 questLogNumTabs ) const ( questNone = 0 ) func (s *QuestLog) questTable(act, number int) struct { name string numberOfDescrs int status int frame int x int y int } { var quests = []struct { name string // name of quest in string table numberOfDescrs int // number of possible descriptions (not used yet) status int // status of quest (not used yet) frame int // frame of quest x, y int // position of quest }{ {"qstsa1q1", 5, 0, 0, q1SocketX, q1SocketY}, {"qstsa1q2", 0, 0, 1, q2SocketX, q2SocketY}, {"qstsa1q3", 0, 0, 2, q3SocketX, q3SocketY}, {"qstsa1q4", 0, 0, 3, q4SocketX, q4SocketY}, {"qstsa1q5", 0, 0, 4, q5SocketX, q5SocketY}, {"qstsa1q6", 0, 0, 5, q6SocketX, q6SocketY}, {"qstsa2q1", 0, 0, 6, q1SocketX, q1SocketY}, {"qstsa2q2", 0, 0, 7, q2SocketX, q2SocketY}, {"qstsa2q3", 0, 0, 8, q3SocketX, q3SocketY}, {"qstsa2q4", 0, 0, 9, q4SocketX, q4SocketY}, {"qstsa2q5", 0, 0, 10, q5SocketX, q5SocketY}, {"qstsa2q6", 0, 0, 11, q6SocketX, q6SocketY}, {"qstsa3q1", 0, 0, 12, q1SocketX, q1SocketY}, {"qstsa3q2", 0, 0, 13, q2SocketX, q2SocketY}, {"qstsa3q3", 0, 0, 14, q3SocketX, q3SocketY}, {"qstsa3q4", 0, 0, 15, q4SocketX, q4SocketY}, {"qstsa3q5", 0, 0, 16, q5SocketX, q5SocketY}, {"qstsa3q6", 0, 0, 17, q6SocketX, q6SocketY}, {"qstsa4q1", 0, 0, 18, q1SocketX, q1SocketY}, {"qstsa4q2", 0, 0, 19, q2SocketX, q2SocketY}, {"qstsa4q3", 0, 0, 20, q3SocketX, q3SocketY}, {"qstsa5q1", 0, 0, 21, q1SocketX, q1SocketY}, {"qstsa5q2", 0, 0, 22, q2SocketX, q2SocketY}, {"qstsa5q3", 0, 0, 23, q3SocketX, q3SocketY}, {"qstsa5q4", 0, 0, 24, q4SocketX, q4SocketY}, {"qstsa5q5", 0, 0, 25, q5SocketX, q5SocketY}, {"qstsa5q6", 0, 0, 26, q6SocketX, q6SocketY}, } key := s.cordsToQuestID(act, number) return quests[key] } // NewQuestLog creates a new quest log func NewQuestLog(asset *d2asset.AssetManager, ui *d2ui.UIManager, l d2util.LogLevel, act int) *QuestLog { originX := 0 originY := 0 qs := map[int]int{ 0: -2, 1: -2, 2: -1, 3: 0, 4: 1, 5: 2, 6: 3, 7: 0, 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, } ql := &QuestLog{ asset: asset, uiManager: ui, originX: originX, originY: originY, act: act, tab: [questLogNumTabs]*questLogTab{ {}, {}, {}, {}, {}, }, questStatus: qs, } 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 [questLogNumTabs]*questLogTab questName *d2ui.Label questDescr *d2ui.Label questsa1 *d2ui.WidgetGroup questsa2 *d2ui.WidgetGroup questsa3 *d2ui.WidgetGroup questsa4 *d2ui.WidgetGroup questsa5 *d2ui.WidgetGroup /* I think, It should looks like that: each quest has its own position in questStatus map which should come from save file. quests status values: - -2 - done - -1 - done, need to play animation - 0 - not started yet - and after that we have "in progress status" so for status (from 1 to n) we have appropriate quest descriptions and we'll have appropriate actions */ questStatus map[int]int originX int originY int isOpen bool *d2util.Logger } /* 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 ( questFrames = 27 notStartedFrame = 26 inProgresFrame = 27 ) const questDescriptionLenght = 30 type questLogTab struct { button *d2ui.Button invisibleButton *d2ui.Button } func (q *questLogTab) newTab(ui *d2ui.UIManager, tabType d2ui.ButtonType, x int) { q.button = ui.NewButton(tabType, "") q.invisibleButton = ui.NewButton(d2ui.ButtonTypeTabBlank, "") q.button.SetPosition(x, questTabY) q.invisibleButton.SetPosition(x, questTabY) } // Load the data for the hero status panel func (s *QuestLog) Load() { var err error s.questsa1 = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) s.questsa2 = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) s.questsa3 = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) s.questsa4 = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) s.questsa5 = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) frame := d2ui.NewUIFrame(s.asset, s.uiManager, 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.renderStaticMenu, 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] = rgbaColor(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] = rgbaColor(white) s.questDescr.SetPosition(questDescrLabelX, questDescrLabelY) s.panelGroup.AddWidget(s.questDescr) s.loadTabs() s.loadQuestIcons() s.panelGroup.SetVisible(false) } func (s *QuestLog) loadTabs() { s.tab[questLogTab1].newTab(s.uiManager, d2ui.ButtonTypeTab1, questTab1X) s.tab[questLogTab1].invisibleButton.OnActivated(func() { s.setTab(questLogTab1) }) s.panelGroup.AddWidget(s.tab[questLogTab1].button) s.panelGroup.AddWidget(s.tab[questLogTab1].invisibleButton) s.tab[questLogTab2].newTab(s.uiManager, d2ui.ButtonTypeTab2, questTab2X) s.tab[questLogTab2].invisibleButton.OnActivated(func() { s.setTab(questLogTab2) }) s.panelGroup.AddWidget(s.tab[questLogTab2].button) s.panelGroup.AddWidget(s.tab[questLogTab2].invisibleButton) s.tab[questLogTab3].newTab(s.uiManager, d2ui.ButtonTypeTab3, questTab3X) s.tab[questLogTab3].invisibleButton.OnActivated(func() { s.setTab(questLogTab3) }) s.panelGroup.AddWidget(s.tab[questLogTab3].button) s.panelGroup.AddWidget(s.tab[questLogTab3].invisibleButton) s.tab[questLogTab4].newTab(s.uiManager, d2ui.ButtonTypeTab4, questTab4X) s.tab[questLogTab4].invisibleButton.OnActivated(func() { s.setTab(questLogTab4) }) s.panelGroup.AddWidget(s.tab[questLogTab4].button) s.panelGroup.AddWidget(s.tab[questLogTab4].invisibleButton) s.tab[questLogTab5].newTab(s.uiManager, d2ui.ButtonTypeTab5, questTab5X) s.tab[questLogTab5].invisibleButton.OnActivated(func() { s.setTab(questLogTab5) }) s.panelGroup.AddWidget(s.tab[questLogTab5].button) s.panelGroup.AddWidget(s.tab[questLogTab5].invisibleButton) s.setTab(s.act - 1) } func (s *QuestLog) loadQuestIcons() { s.questsa1 = s.loadQuestIconsForAct(act1) s.questsa2 = s.loadQuestIconsForAct(act2) s.questsa3 = s.loadQuestIconsForAct(act3) s.questsa4 = s.loadQuestIconsForAct(act4) s.questsa5 = s.loadQuestIconsForAct(act5) } func (s *QuestLog) loadQuestIconsForAct(act int) *d2ui.WidgetGroup { wg := s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog) var questsInAct int if act == act4 { questsInAct = act4QuestsNumber } else { questsInAct = normalActQuestsNumber } var sockets []*d2ui.Sprite var buttons []*d2ui.Button var icon *d2ui.Sprite for n := 0; n < questsInAct; n++ { q := s.questTable(act, n) socket, err := s.uiManager.NewSprite(d2resource.QuestLogSocket, d2resource.PaletteSky) if err != nil { s.Error(err.Error()) } socket.SetPosition(q.x+questOffsetX, q.y+iconOffsetY+2*questOffsetY) sockets = append(sockets, socket) button := s.uiManager.NewButton(d2ui.ButtonTypeBlankQuestBtn, "") button.SetPosition(q.x+questOffsetX, q.y+questOffsetY) buttons = append(buttons, button) switch s.questStatus[s.cordsToQuestID(act, n)] { case questStatusCompleted: icon, err = s.uiManager.NewSprite(d2resource.QuestLogDone, d2resource.PaletteSky) if err != nil { s.Error(err.Error()) } err = icon.SetCurrentFrame(q.frame) if err != nil { s.Error(err.Error()) } case questStatusNotStarted: icon, err = s.uiManager.NewSprite(s.questIconsTable(act, n), d2resource.PaletteSky) if err != nil { s.Error(err.Error()) } err = icon.SetCurrentFrame(notStartedFrame) if err != nil { s.Error(err.Error()) } default: icon, err = s.uiManager.NewSprite(s.questIconsTable(act, n), d2resource.PaletteSky) if err != nil { s.Error(err.Error()) } err = icon.SetCurrentFrame(inProgresFrame) if err != nil { s.Error(err.Error()) } } icon.SetPosition(q.x+questOffsetX, q.y+questOffsetY+iconOffsetY) wg.AddWidget(icon) } for i := 0; i < questsInAct; i++ { currentQuest := i buttons[i].OnActivated(func() { var err error for j := 0; j < questsInAct; j++ { err = sockets[j].SetCurrentFrame(0) if err != nil { s.Error(err.Error()) } } if act-1 == s.selectedTab { err = sockets[currentQuest].SetCurrentFrame(1) if err != nil { s.Error(err.Error()) } } s.onQuestClicked(currentQuest + 1) }) } for _, s := range sockets { wg.AddWidget(s) } for _, b := range buttons { wg.AddWidget(b) } wg.SetVisible(false) return wg } func (s *QuestLog) setQuestLabel() { if s.selectedQuest == 0 { 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)] switch status { case questStatusCompleted, questStatusCompleting: s.questDescr.SetText( strings.Join( d2util.SplitIntoLinesWithMaxWidth( s.asset.TranslateString("qstsprevious"), 30), "\n"), ) case questStatusNotStarted: s.questDescr.SetText("") default: s.questDescr.SetText(strings.Join( d2util.SplitIntoLinesWithMaxWidth( s.asset.TranslateString( fmt.Sprintf("qstsa%dq%d%d", s.selectedTab+1, s.selectedQuest, status), ), 30), "\n"), ) } } func (s *QuestLog) setTab(tab int) { s.selectedTab = tab s.selectedQuest = questNone s.setQuestLabel() s.questsa1.SetVisible(tab == questLogTab1) s.questsa2.SetVisible(tab == questLogTab2) s.questsa3.SetVisible(tab == questLogTab3) s.questsa4.SetVisible(tab == questLogTab4) s.questsa5.SetVisible(tab == questLogTab5) for i := 0; i < questLogNumTabs; i++ { s.tab[i].button.SetEnabled(i == tab) } } 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() { // } // 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) } // Close closed the hero status panel func (s *QuestLog) Close() { s.isOpen = false s.panelGroup.SetVisible(false) s.questsa1.SetVisible(false) s.questsa2.SetVisible(false) s.questsa3.SetVisible(false) s.questsa4.SetVisible(false) s.questsa5.SetVisible(false) 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) { // } func (s *QuestLog) renderStaticMenu(target d2interface.Surface) { s.renderStaticPanelFrames(target) } // 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) } } // copy from character select func rgbaColor(rgba uint32) color.RGBA { result := color.RGBA{} a, b, g, r := 0, 1, 2, 3 byteWidth := 8 byteMask := 0xff for idx := 0; idx < 4; idx++ { shift := idx * byteWidth component := uint8(rgba>>shift) & uint8(byteMask) switch idx { case a: result.A = component case b: result.B = component case g: result.G = component case r: result.R = component } } return result } func (s *QuestLog) cordsToQuestID(act, number int) int { key := (act-1)*normalActQuestsNumber + number if act > act4 { key -= act4QuestsNumber } return key } func (s *QuestLog) questIDToCords(id int) (act, number int) { act = act1 for i := 0; i < normalActQuestsNumber; i++ { if id < normalActQuestsNumber { break } act++ id -= normalActQuestsNumber } number = id if act > act4 { number -= act4QuestsNumber } return act, number }