diff --git a/server/internal/masterdata/sidestory.go b/server/internal/masterdata/sidestory.go index 8485731..b9ab75a 100644 --- a/server/internal/masterdata/sidestory.go +++ b/server/internal/masterdata/sidestory.go @@ -2,11 +2,35 @@ package masterdata import ( "log" + "sort" + + "lunar-tear/server/internal/model" "lunar-tear/server/internal/utils" ) +type SideStorySceneInfo struct { + SceneId int32 + Type model.SideStorySceneIdType +} + +type SideStoryQuestInfo struct { + SideStoryQuestId int32 + Scenes []SideStorySceneInfo // the 7 scenes, one per type + Quests []int32 // ordered event quests (the chapter+difficulty sequence) +} + type SideStoryCatalog struct { - FirstSceneByQuestId map[int32]int32 + QuestById map[int32]*SideStoryQuestInfo + ChapterByEventQuestId map[int32]int32 // event quest id -> side story chapter id +} + +func (q *SideStoryQuestInfo) SceneIdByType(t model.SideStorySceneIdType) (int32, bool) { + for _, s := range q.Scenes { + if s.Type == t { + return s.SceneId, true + } + } + return 0, false } func LoadSideStoryCatalog() *SideStoryCatalog { @@ -14,14 +38,90 @@ func LoadSideStoryCatalog() *SideStoryCatalog { if err != nil { log.Fatalf("load side story quest scene table: %v", err) } - - firstScene := make(map[int32]int32, len(scenes)/7) - for _, s := range scenes { - if s.SortOrder == 1 { - firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId - } + limitContents, err := utils.ReadTable[EntityMSideStoryQuestLimitContent]("m_side_story_quest_limit_content") + if err != nil { + log.Fatalf("load side story quest limit content table: %v", err) + } + seqGroups, err := utils.ReadTable[EntityMEventQuestSequenceGroup]("m_event_quest_sequence_group") + if err != nil { + log.Fatalf("load event quest sequence group table: %v", err) + } + sequences, err := utils.ReadTable[EntityMEventQuestSequence]("m_event_quest_sequence") + if err != nil { + log.Fatalf("load event quest sequence table: %v", err) } - log.Printf("side story catalog loaded: %d quests", len(firstScene)) - return &SideStoryCatalog{FirstSceneByQuestId: firstScene} + seqRows := make(map[int32][]EntityMEventQuestSequence) + for _, s := range sequences { + seqRows[s.EventQuestSequenceId] = append(seqRows[s.EventQuestSequenceId], s) + } + orderedQuestIds := make(map[int32][]int32, len(seqRows)) + for seqId, rows := range seqRows { + sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder }) + ids := make([]int32, len(rows)) + for i, r := range rows { + ids[i] = r.QuestId + } + orderedQuestIds[seqId] = ids + } + + // (chapterId, difficulty) -> sequenceId. Sequence group id == chapter id. + type chapDiff struct{ chapter, difficulty int32 } + sequenceByChapterDiff := make(map[chapDiff]int32, len(seqGroups)) + for _, g := range seqGroups { + sequenceByChapterDiff[chapDiff{g.EventQuestSequenceGroupId, g.DifficultyType}] = g.EventQuestSequenceId + } + + // sideStoryQuestId -> limit content row. Limit content id == side story quest id. + limitByQuest := make(map[int32]EntityMSideStoryQuestLimitContent, len(limitContents)) + for _, lc := range limitContents { + limitByQuest[lc.SideStoryQuestLimitContentId] = lc + } + + // sideStoryQuestId -> scene rows + scenesByQuest := make(map[int32][]EntityMSideStoryQuestScene) + for _, sc := range scenes { + scenesByQuest[sc.SideStoryQuestId] = append(scenesByQuest[sc.SideStoryQuestId], sc) + } + + questById := make(map[int32]*SideStoryQuestInfo, len(scenesByQuest)) + chapterByEventQuest := make(map[int32]int32) + + for ssqId, rows := range scenesByQuest { + sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder }) + + var orderedQuests []int32 + var chapterId, difficulty int32 + if lc, ok := limitByQuest[ssqId]; ok { + chapterId = lc.EventQuestChapterId + difficulty = lc.DifficultyType + if seqId, ok := sequenceByChapterDiff[chapDiff{chapterId, difficulty}]; ok { + orderedQuests = orderedQuestIds[seqId] + } + } + if chapterId != 0 { + for _, questId := range orderedQuests { + chapterByEventQuest[questId] = chapterId + } + } + + info := &SideStoryQuestInfo{ + SideStoryQuestId: ssqId, + Scenes: make([]SideStorySceneInfo, 0, len(rows)), + Quests: orderedQuests, + } + for _, sc := range rows { + info.Scenes = append(info.Scenes, SideStorySceneInfo{ + SceneId: sc.SideStoryQuestSceneId, + Type: model.SideStorySceneIdType(sc.SortOrder), + }) + } + questById[ssqId] = info + } + + log.Printf("side story catalog loaded: %d quests, %d scenes", len(questById), len(scenes)) + return &SideStoryCatalog{ + QuestById: questById, + ChapterByEventQuestId: chapterByEventQuest, + } } diff --git a/server/internal/model/quest.go b/server/internal/model/quest.go index 9e15727..02c1f48 100644 --- a/server/internal/model/quest.go +++ b/server/internal/model/quest.go @@ -164,6 +164,20 @@ type SideStoryQuestStateType int32 const ( SideStoryQuestStateUnknown SideStoryQuestStateType = 0 - SideStoryQuestStateActive SideStoryQuestStateType = 1 - SideStoryQuestStateCleared SideStoryQuestStateType = 2 + SideStoryQuestStateActive SideStoryQuestStateType = 2 + SideStoryQuestStateCleared SideStoryQuestStateType = 3 +) + +type SideStorySceneIdType int32 + +// Values mirror SideStoryTypes.SceneIdTypes in the client (dump.cs). +const ( + SideStorySceneInvalid SideStorySceneIdType = 0 + SideStorySceneIntroduction SideStorySceneIdType = 1 + SideStoryScenePlayGeneralQuest SideStorySceneIdType = 2 + SideStorySceneUnlockLastQuest SideStorySceneIdType = 3 + SideStoryScenePlayLastQuest SideStorySceneIdType = 4 + SideStorySceneOutroduction SideStorySceneIdType = 5 + SideStorySceneShowCostumeAcquisition SideStorySceneIdType = 6 + SideStoryScenePlayFreeQuest SideStorySceneIdType = 7 ) diff --git a/server/internal/questflow/event_quest.go b/server/internal/questflow/event_quest.go index 2fad8ec..b738097 100644 --- a/server/internal/questflow/event_quest.go +++ b/server/internal/questflow/event_quest.go @@ -45,6 +45,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC outcome := h.evaluateFinishOutcome(user, questId) if !isRetired { h.applyQuestVictory(user, questId, &outcome, nowMillis, false) + h.recordSideStoryLimitContentStatus(user, questId, nowMillis) } if isRetired && !isAnnihilated && quest.Stamina > 1 { @@ -64,6 +65,18 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC return outcome } +func (h *QuestHandler) recordSideStoryLimitContentStatus(user *store.UserState, questId int32, nowMillis int64) { + chapterId, ok := h.SideStoryChapterByEventQuestId[questId] + if !ok { + return + } + st := user.QuestLimitContentStatus[questId] + st.LimitContentQuestStatusType = 1 + st.EventQuestChapterId = chapterId + st.LatestVersion = nowMillis + user.QuestLimitContentStatus[questId] = st +} + func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) { h.HandleQuestRestart(user, questId, nowMillis) diff --git a/server/internal/questflow/handler.go b/server/internal/questflow/handler.go index 390f7d7..1e31ab9 100644 --- a/server/internal/questflow/handler.go +++ b/server/internal/questflow/handler.go @@ -25,13 +25,23 @@ type FinishOutcome struct { type QuestHandler struct { *masterdata.QuestCatalog - Config *masterdata.GameConfig - Granter *store.PossessionGranter + Config *masterdata.GameConfig + Granter *store.PossessionGranter + SideStoryChapterByEventQuestId map[int32]int32 } -func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler { +func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler { granter := BuildGranter(catalog) - return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter} + var sideStoryChapters map[int32]int32 + if sideStory != nil { + sideStoryChapters = sideStory.ChapterByEventQuestId + } + return &QuestHandler{ + QuestCatalog: catalog, + Config: config, + Granter: granter, + SideStoryChapterByEventQuestId: sideStoryChapters, + } } func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter { diff --git a/server/internal/runtime/build.go b/server/internal/runtime/build.go index 8b3ead4..4032637 100644 --- a/server/internal/runtime/build.go +++ b/server/internal/runtime/build.go @@ -33,7 +33,8 @@ func buildCatalogs() (*Catalogs, error) { if err != nil { return nil, fmt.Errorf("load quest catalog: %w", err) } - questHandler := questflow.NewQuestHandler(questCatalog, gameConfig) + sideStoryCatalog := masterdata.LoadSideStoryCatalog() + questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog) gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() if err != nil { @@ -136,7 +137,6 @@ func buildCatalogs() (*Catalogs, error) { } log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory)) - sideStoryCatalog := masterdata.LoadSideStoryCatalog() bigHuntCatalog := masterdata.LoadBigHuntCatalog() return &Catalogs{ diff --git a/server/internal/service/quest_sidestory.go b/server/internal/service/quest_sidestory.go index d1b6c2b..c24d62c 100644 --- a/server/internal/service/quest_sidestory.go +++ b/server/internal/service/quest_sidestory.go @@ -6,6 +6,7 @@ import ( pb "lunar-tear/server/gen/proto" "lunar-tear/server/internal/gametime" + "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/model" "lunar-tear/server/internal/runtime" "lunar-tear/server/internal/store" @@ -22,34 +23,89 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder} } +func sideStoryClearedCount(info *masterdata.SideStoryQuestInfo, user *store.UserState) int { + cleared := 0 + for _, questId := range info.Quests { + if user.QuestLimitContentStatus[questId].LimitContentQuestStatusType == 1 { + cleared++ + } + } + return cleared +} + +func sideStoryQuestCleared(info *masterdata.SideStoryQuestInfo, user *store.UserState) bool { + return info != nil && len(info.Quests) > 0 && sideStoryClearedCount(info, user) == len(info.Quests) +} + +func sideStoryNextSceneAfterBattle(info *masterdata.SideStoryQuestInfo, user *store.UserState) (int32, bool) { + cleared := sideStoryClearedCount(info, user) + if cleared == 0 { + return 0, false + } + total := len(info.Quests) + var sceneType model.SideStorySceneIdType + switch { + case cleared >= total: + sceneType = model.SideStorySceneOutroduction + case cleared == total-1: + sceneType = model.SideStorySceneUnlockLastQuest + default: + sceneType = model.SideStoryScenePlayLastQuest + } + return info.SceneIdByType(sceneType) +} + +func applySideStoryProgressState(progress *store.SideStoryQuestProgress, info *masterdata.SideStoryQuestInfo, user *store.UserState) { + if sideStoryQuestCleared(info, user) { + progress.SideStoryQuestStateType = model.SideStoryQuestStateCleared + } else if progress.SideStoryQuestStateType == model.SideStoryQuestStateUnknown { + progress.SideStoryQuestStateType = model.SideStoryQuestStateActive + } +} + +func setSideStoryActive(user *store.UserState, questId, sceneId int32, nowMillis int64) { + user.SideStoryActiveProgress = store.SideStoryActiveProgress{ + CurrentSideStoryQuestId: questId, + CurrentSideStoryQuestSceneId: sceneId, + LatestVersion: nowMillis, + } +} + +func setSideStoryScene(user *store.UserState, info *masterdata.SideStoryQuestInfo, questId, sceneId int32, nowMillis int64) { + progress := user.SideStoryQuests[questId] + progress.HeadSideStoryQuestSceneId = sceneId + applySideStoryProgressState(&progress, info, user) + progress.LatestVersion = nowMillis + user.SideStoryQuests[questId] = progress + setSideStoryActive(user, questId, sceneId, nowMillis) +} + func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) { log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId) userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId] + info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId] s.users.UpdateUser(userId, func(user *store.UserState) { + if info == nil || len(info.Quests) == 0 { + log.Printf("[SideStoryQuestService] unknown sideStoryQuestId=%d, skipping", req.SideStoryQuestId) + return + } + existing, exists := user.SideStoryQuests[req.SideStoryQuestId] - var sceneId int32 - if exists && existing.HeadSideStoryQuestSceneId > 0 { - sceneId = existing.HeadSideStoryQuestSceneId + var scene int32 + var ok bool + if !exists || existing.HeadSideStoryQuestSceneId == 0 { + scene, ok = info.SceneIdByType(model.SideStorySceneIntroduction) } else { - sceneId = firstSceneId + scene, ok = sideStoryNextSceneAfterBattle(info, user) } - - user.SideStoryActiveProgress.CurrentSideStoryQuestId = req.SideStoryQuestId - user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = sceneId - user.SideStoryActiveProgress.LatestVersion = nowMillis - - if !exists { - user.SideStoryQuests[req.SideStoryQuestId] = store.SideStoryQuestProgress{ - HeadSideStoryQuestSceneId: firstSceneId, - SideStoryQuestStateType: model.SideStoryQuestStateActive, - LatestVersion: nowMillis, - } + if !ok { + return } + setSideStoryScene(user, info, req.SideStoryQuestId, scene, nowMillis) }) return &pb.MoveSideStoryQuestResponse{}, nil @@ -61,16 +117,10 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont userId := CurrentUserId(ctx, s.users, s.sessions) nowMillis := gametime.NowMillis() - s.users.UpdateUser(userId, func(user *store.UserState) { - user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId - user.SideStoryActiveProgress.LatestVersion = nowMillis + info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId] - progress := user.SideStoryQuests[req.SideStoryQuestId] - if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId { - progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId - } - progress.LatestVersion = nowMillis - user.SideStoryQuests[req.SideStoryQuestId] = progress + s.users.UpdateUser(userId, func(user *store.UserState) { + setSideStoryScene(user, info, req.SideStoryQuestId, req.SideStoryQuestSceneId, nowMillis) }) return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil