Implement Recollections of Dusk

This commit is contained in:
Ilya Groshev
2026-05-16 14:35:47 +03:00
parent 15beefb5b8
commit 26c10ac429
6 changed files with 229 additions and 42 deletions
+107 -7
View File
@@ -2,11 +2,35 @@ package masterdata
import ( import (
"log" "log"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils" "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 { 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 { func LoadSideStoryCatalog() *SideStoryCatalog {
@@ -14,14 +38,90 @@ func LoadSideStoryCatalog() *SideStoryCatalog {
if err != nil { if err != nil {
log.Fatalf("load side story quest scene table: %v", err) log.Fatalf("load side story quest scene table: %v", err)
} }
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)
}
firstScene := make(map[int32]int32, len(scenes)/7) seqRows := make(map[int32][]EntityMEventQuestSequence)
for _, s := range scenes { for _, s := range sequences {
if s.SortOrder == 1 { seqRows[s.EventQuestSequenceId] = append(seqRows[s.EventQuestSequenceId], s)
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId }
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
} }
} }
log.Printf("side story catalog loaded: %d quests", len(firstScene)) info := &SideStoryQuestInfo{
return &SideStoryCatalog{FirstSceneByQuestId: firstScene} 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,
}
} }
+16 -2
View File
@@ -164,6 +164,20 @@ type SideStoryQuestStateType int32
const ( const (
SideStoryQuestStateUnknown SideStoryQuestStateType = 0 SideStoryQuestStateUnknown SideStoryQuestStateType = 0
SideStoryQuestStateActive SideStoryQuestStateType = 1 SideStoryQuestStateActive SideStoryQuestStateType = 2
SideStoryQuestStateCleared 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
) )
+13
View File
@@ -45,6 +45,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
outcome := h.evaluateFinishOutcome(user, questId) outcome := h.evaluateFinishOutcome(user, questId)
if !isRetired { if !isRetired {
h.applyQuestVictory(user, questId, &outcome, nowMillis, false) h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
} }
if isRetired && !isAnnihilated && quest.Stamina > 1 { if isRetired && !isAnnihilated && quest.Stamina > 1 {
@@ -64,6 +65,18 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
return outcome 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) { func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
h.HandleQuestRestart(user, questId, nowMillis) h.HandleQuestRestart(user, questId, nowMillis)
+12 -2
View File
@@ -27,11 +27,21 @@ type QuestHandler struct {
*masterdata.QuestCatalog *masterdata.QuestCatalog
Config *masterdata.GameConfig Config *masterdata.GameConfig
Granter *store.PossessionGranter 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) 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 { func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
+2 -2
View File
@@ -33,7 +33,8 @@ func buildCatalogs() (*Catalogs, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("load quest catalog: %w", err) 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() gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
if err != nil { 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)) log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
bigHuntCatalog := masterdata.LoadBigHuntCatalog() bigHuntCatalog := masterdata.LoadBigHuntCatalog()
return &Catalogs{ return &Catalogs{
+75 -25
View File
@@ -6,6 +6,7 @@ import (
pb "lunar-tear/server/gen/proto" pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime" "lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model" "lunar-tear/server/internal/model"
"lunar-tear/server/internal/runtime" "lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store" "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} 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) { func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId) log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId)
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() 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) { 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] existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
var sceneId int32 var scene int32
if exists && existing.HeadSideStoryQuestSceneId > 0 { var ok bool
sceneId = existing.HeadSideStoryQuestSceneId if !exists || existing.HeadSideStoryQuestSceneId == 0 {
scene, ok = info.SceneIdByType(model.SideStorySceneIntroduction)
} else { } 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 return &pb.MoveSideStoryQuestResponse{}, nil
@@ -61,16 +117,10 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont
userId := CurrentUserId(ctx, s.users, s.sessions) userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis() nowMillis := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) { info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId
user.SideStoryActiveProgress.LatestVersion = nowMillis
progress := user.SideStoryQuests[req.SideStoryQuestId] s.users.UpdateUser(userId, func(user *store.UserState) {
if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId { setSideStoryScene(user, info, req.SideStoryQuestId, req.SideStoryQuestSceneId, nowMillis)
progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId
}
progress.LatestVersion = nowMillis
user.SideStoryQuests[req.SideStoryQuestId] = progress
}) })
return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil