Skip to content

Commit 26c10ac

Browse files
Implement Recollections of Dusk
1 parent 15beefb commit 26c10ac

6 files changed

Lines changed: 227 additions & 40 deletions

File tree

server/internal/masterdata/sidestory.go

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,126 @@ package masterdata
22

33
import (
44
"log"
5+
"sort"
6+
7+
"lunar-tear/server/internal/model"
58
"lunar-tear/server/internal/utils"
69
)
710

11+
type SideStorySceneInfo struct {
12+
SceneId int32
13+
Type model.SideStorySceneIdType
14+
}
15+
16+
type SideStoryQuestInfo struct {
17+
SideStoryQuestId int32
18+
Scenes []SideStorySceneInfo // the 7 scenes, one per type
19+
Quests []int32 // ordered event quests (the chapter+difficulty sequence)
20+
}
21+
822
type SideStoryCatalog struct {
9-
FirstSceneByQuestId map[int32]int32
23+
QuestById map[int32]*SideStoryQuestInfo
24+
ChapterByEventQuestId map[int32]int32 // event quest id -> side story chapter id
25+
}
26+
27+
func (q *SideStoryQuestInfo) SceneIdByType(t model.SideStorySceneIdType) (int32, bool) {
28+
for _, s := range q.Scenes {
29+
if s.Type == t {
30+
return s.SceneId, true
31+
}
32+
}
33+
return 0, false
1034
}
1135

1236
func LoadSideStoryCatalog() *SideStoryCatalog {
1337
scenes, err := utils.ReadTable[EntityMSideStoryQuestScene]("m_side_story_quest_scene")
1438
if err != nil {
1539
log.Fatalf("load side story quest scene table: %v", err)
1640
}
41+
limitContents, err := utils.ReadTable[EntityMSideStoryQuestLimitContent]("m_side_story_quest_limit_content")
42+
if err != nil {
43+
log.Fatalf("load side story quest limit content table: %v", err)
44+
}
45+
seqGroups, err := utils.ReadTable[EntityMEventQuestSequenceGroup]("m_event_quest_sequence_group")
46+
if err != nil {
47+
log.Fatalf("load event quest sequence group table: %v", err)
48+
}
49+
sequences, err := utils.ReadTable[EntityMEventQuestSequence]("m_event_quest_sequence")
50+
if err != nil {
51+
log.Fatalf("load event quest sequence table: %v", err)
52+
}
53+
54+
seqRows := make(map[int32][]EntityMEventQuestSequence)
55+
for _, s := range sequences {
56+
seqRows[s.EventQuestSequenceId] = append(seqRows[s.EventQuestSequenceId], s)
57+
}
58+
orderedQuestIds := make(map[int32][]int32, len(seqRows))
59+
for seqId, rows := range seqRows {
60+
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
61+
ids := make([]int32, len(rows))
62+
for i, r := range rows {
63+
ids[i] = r.QuestId
64+
}
65+
orderedQuestIds[seqId] = ids
66+
}
67+
68+
// (chapterId, difficulty) -> sequenceId. Sequence group id == chapter id.
69+
type chapDiff struct{ chapter, difficulty int32 }
70+
sequenceByChapterDiff := make(map[chapDiff]int32, len(seqGroups))
71+
for _, g := range seqGroups {
72+
sequenceByChapterDiff[chapDiff{g.EventQuestSequenceGroupId, g.DifficultyType}] = g.EventQuestSequenceId
73+
}
74+
75+
// sideStoryQuestId -> limit content row. Limit content id == side story quest id.
76+
limitByQuest := make(map[int32]EntityMSideStoryQuestLimitContent, len(limitContents))
77+
for _, lc := range limitContents {
78+
limitByQuest[lc.SideStoryQuestLimitContentId] = lc
79+
}
80+
81+
// sideStoryQuestId -> scene rows
82+
scenesByQuest := make(map[int32][]EntityMSideStoryQuestScene)
83+
for _, sc := range scenes {
84+
scenesByQuest[sc.SideStoryQuestId] = append(scenesByQuest[sc.SideStoryQuestId], sc)
85+
}
86+
87+
questById := make(map[int32]*SideStoryQuestInfo, len(scenesByQuest))
88+
chapterByEventQuest := make(map[int32]int32)
89+
90+
for ssqId, rows := range scenesByQuest {
91+
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
92+
93+
var orderedQuests []int32
94+
var chapterId, difficulty int32
95+
if lc, ok := limitByQuest[ssqId]; ok {
96+
chapterId = lc.EventQuestChapterId
97+
difficulty = lc.DifficultyType
98+
if seqId, ok := sequenceByChapterDiff[chapDiff{chapterId, difficulty}]; ok {
99+
orderedQuests = orderedQuestIds[seqId]
100+
}
101+
}
102+
if chapterId != 0 {
103+
for _, questId := range orderedQuests {
104+
chapterByEventQuest[questId] = chapterId
105+
}
106+
}
17107

18-
firstScene := make(map[int32]int32, len(scenes)/7)
19-
for _, s := range scenes {
20-
if s.SortOrder == 1 {
21-
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
108+
info := &SideStoryQuestInfo{
109+
SideStoryQuestId: ssqId,
110+
Scenes: make([]SideStorySceneInfo, 0, len(rows)),
111+
Quests: orderedQuests,
22112
}
113+
for _, sc := range rows {
114+
info.Scenes = append(info.Scenes, SideStorySceneInfo{
115+
SceneId: sc.SideStoryQuestSceneId,
116+
Type: model.SideStorySceneIdType(sc.SortOrder),
117+
})
118+
}
119+
questById[ssqId] = info
23120
}
24121

25-
log.Printf("side story catalog loaded: %d quests", len(firstScene))
26-
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
122+
log.Printf("side story catalog loaded: %d quests, %d scenes", len(questById), len(scenes))
123+
return &SideStoryCatalog{
124+
QuestById: questById,
125+
ChapterByEventQuestId: chapterByEventQuest,
126+
}
27127
}

server/internal/model/quest.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ type SideStoryQuestStateType int32
164164

165165
const (
166166
SideStoryQuestStateUnknown SideStoryQuestStateType = 0
167-
SideStoryQuestStateActive SideStoryQuestStateType = 1
168-
SideStoryQuestStateCleared SideStoryQuestStateType = 2
167+
SideStoryQuestStateActive SideStoryQuestStateType = 2
168+
SideStoryQuestStateCleared SideStoryQuestStateType = 3
169+
)
170+
171+
type SideStorySceneIdType int32
172+
173+
// Values mirror SideStoryTypes.SceneIdTypes in the client (dump.cs).
174+
const (
175+
SideStorySceneInvalid SideStorySceneIdType = 0
176+
SideStorySceneIntroduction SideStorySceneIdType = 1
177+
SideStoryScenePlayGeneralQuest SideStorySceneIdType = 2
178+
SideStorySceneUnlockLastQuest SideStorySceneIdType = 3
179+
SideStoryScenePlayLastQuest SideStorySceneIdType = 4
180+
SideStorySceneOutroduction SideStorySceneIdType = 5
181+
SideStorySceneShowCostumeAcquisition SideStorySceneIdType = 6
182+
SideStoryScenePlayFreeQuest SideStorySceneIdType = 7
169183
)

server/internal/questflow/event_quest.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
4545
outcome := h.evaluateFinishOutcome(user, questId)
4646
if !isRetired {
4747
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
48+
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
4849
}
4950

5051
if isRetired && !isAnnihilated && quest.Stamina > 1 {
@@ -64,6 +65,18 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
6465
return outcome
6566
}
6667

68+
func (h *QuestHandler) recordSideStoryLimitContentStatus(user *store.UserState, questId int32, nowMillis int64) {
69+
chapterId, ok := h.SideStoryChapterByEventQuestId[questId]
70+
if !ok {
71+
return
72+
}
73+
st := user.QuestLimitContentStatus[questId]
74+
st.LimitContentQuestStatusType = 1
75+
st.EventQuestChapterId = chapterId
76+
st.LatestVersion = nowMillis
77+
user.QuestLimitContentStatus[questId] = st
78+
}
79+
6780
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
6881
h.HandleQuestRestart(user, questId, nowMillis)
6982

server/internal/questflow/handler.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,23 @@ type FinishOutcome struct {
2525

2626
type QuestHandler struct {
2727
*masterdata.QuestCatalog
28-
Config *masterdata.GameConfig
29-
Granter *store.PossessionGranter
28+
Config *masterdata.GameConfig
29+
Granter *store.PossessionGranter
30+
SideStoryChapterByEventQuestId map[int32]int32
3031
}
3132

32-
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler {
33+
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
3334
granter := BuildGranter(catalog)
34-
return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter}
35+
var sideStoryChapters map[int32]int32
36+
if sideStory != nil {
37+
sideStoryChapters = sideStory.ChapterByEventQuestId
38+
}
39+
return &QuestHandler{
40+
QuestCatalog: catalog,
41+
Config: config,
42+
Granter: granter,
43+
SideStoryChapterByEventQuestId: sideStoryChapters,
44+
}
3545
}
3646

3747
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {

server/internal/runtime/build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ func buildCatalogs() (*Catalogs, error) {
3333
if err != nil {
3434
return nil, fmt.Errorf("load quest catalog: %w", err)
3535
}
36-
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
36+
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
37+
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
3738

3839
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
3940
if err != nil {
@@ -136,7 +137,6 @@ func buildCatalogs() (*Catalogs, error) {
136137
}
137138
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
138139

139-
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
140140
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
141141

142142
return &Catalogs{

server/internal/service/quest_sidestory.go

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
pb "lunar-tear/server/gen/proto"
88
"lunar-tear/server/internal/gametime"
9+
"lunar-tear/server/internal/masterdata"
910
"lunar-tear/server/internal/model"
1011
"lunar-tear/server/internal/runtime"
1112
"lunar-tear/server/internal/store"
@@ -22,34 +23,89 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S
2223
return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder}
2324
}
2425

26+
func sideStoryClearedCount(info *masterdata.SideStoryQuestInfo, user *store.UserState) int {
27+
cleared := 0
28+
for _, questId := range info.Quests {
29+
if user.QuestLimitContentStatus[questId].LimitContentQuestStatusType == 1 {
30+
cleared++
31+
}
32+
}
33+
return cleared
34+
}
35+
36+
func sideStoryQuestCleared(info *masterdata.SideStoryQuestInfo, user *store.UserState) bool {
37+
return info != nil && len(info.Quests) > 0 && sideStoryClearedCount(info, user) == len(info.Quests)
38+
}
39+
40+
func sideStoryNextSceneAfterBattle(info *masterdata.SideStoryQuestInfo, user *store.UserState) (int32, bool) {
41+
cleared := sideStoryClearedCount(info, user)
42+
if cleared == 0 {
43+
return 0, false
44+
}
45+
total := len(info.Quests)
46+
var sceneType model.SideStorySceneIdType
47+
switch {
48+
case cleared >= total:
49+
sceneType = model.SideStorySceneOutroduction
50+
case cleared == total-1:
51+
sceneType = model.SideStorySceneUnlockLastQuest
52+
default:
53+
sceneType = model.SideStoryScenePlayLastQuest
54+
}
55+
return info.SceneIdByType(sceneType)
56+
}
57+
58+
func applySideStoryProgressState(progress *store.SideStoryQuestProgress, info *masterdata.SideStoryQuestInfo, user *store.UserState) {
59+
if sideStoryQuestCleared(info, user) {
60+
progress.SideStoryQuestStateType = model.SideStoryQuestStateCleared
61+
} else if progress.SideStoryQuestStateType == model.SideStoryQuestStateUnknown {
62+
progress.SideStoryQuestStateType = model.SideStoryQuestStateActive
63+
}
64+
}
65+
66+
func setSideStoryActive(user *store.UserState, questId, sceneId int32, nowMillis int64) {
67+
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
68+
CurrentSideStoryQuestId: questId,
69+
CurrentSideStoryQuestSceneId: sceneId,
70+
LatestVersion: nowMillis,
71+
}
72+
}
73+
74+
func setSideStoryScene(user *store.UserState, info *masterdata.SideStoryQuestInfo, questId, sceneId int32, nowMillis int64) {
75+
progress := user.SideStoryQuests[questId]
76+
progress.HeadSideStoryQuestSceneId = sceneId
77+
applySideStoryProgressState(&progress, info, user)
78+
progress.LatestVersion = nowMillis
79+
user.SideStoryQuests[questId] = progress
80+
setSideStoryActive(user, questId, sceneId, nowMillis)
81+
}
82+
2583
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
2684
log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId)
2785

2886
userId := CurrentUserId(ctx, s.users, s.sessions)
2987
nowMillis := gametime.NowMillis()
30-
firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId]
88+
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
3189

3290
s.users.UpdateUser(userId, func(user *store.UserState) {
91+
if info == nil || len(info.Quests) == 0 {
92+
log.Printf("[SideStoryQuestService] unknown sideStoryQuestId=%d, skipping", req.SideStoryQuestId)
93+
return
94+
}
95+
3396
existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
3497

35-
var sceneId int32
36-
if exists && existing.HeadSideStoryQuestSceneId > 0 {
37-
sceneId = existing.HeadSideStoryQuestSceneId
98+
var scene int32
99+
var ok bool
100+
if !exists || existing.HeadSideStoryQuestSceneId == 0 {
101+
scene, ok = info.SceneIdByType(model.SideStorySceneIntroduction)
38102
} else {
39-
sceneId = firstSceneId
103+
scene, ok = sideStoryNextSceneAfterBattle(info, user)
40104
}
41-
42-
user.SideStoryActiveProgress.CurrentSideStoryQuestId = req.SideStoryQuestId
43-
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = sceneId
44-
user.SideStoryActiveProgress.LatestVersion = nowMillis
45-
46-
if !exists {
47-
user.SideStoryQuests[req.SideStoryQuestId] = store.SideStoryQuestProgress{
48-
HeadSideStoryQuestSceneId: firstSceneId,
49-
SideStoryQuestStateType: model.SideStoryQuestStateActive,
50-
LatestVersion: nowMillis,
51-
}
105+
if !ok {
106+
return
52107
}
108+
setSideStoryScene(user, info, req.SideStoryQuestId, scene, nowMillis)
53109
})
54110

55111
return &pb.MoveSideStoryQuestResponse{}, nil
@@ -61,16 +117,10 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont
61117

62118
userId := CurrentUserId(ctx, s.users, s.sessions)
63119
nowMillis := gametime.NowMillis()
64-
s.users.UpdateUser(userId, func(user *store.UserState) {
65-
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId
66-
user.SideStoryActiveProgress.LatestVersion = nowMillis
120+
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
67121

68-
progress := user.SideStoryQuests[req.SideStoryQuestId]
69-
if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId {
70-
progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId
71-
}
72-
progress.LatestVersion = nowMillis
73-
user.SideStoryQuests[req.SideStoryQuestId] = progress
122+
s.users.UpdateUser(userId, func(user *store.UserState) {
123+
setSideStoryScene(user, info, req.SideStoryQuestId, req.SideStoryQuestSceneId, nowMillis)
74124
})
75125

76126
return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil

0 commit comments

Comments
 (0)