Skip to content

Commit 2cf0c15

Browse files
Implement Fate Boards
1 parent 956dbfa commit 2cf0c15

16 files changed

Lines changed: 549 additions & 7 deletions

File tree

server/cmd/lunar-tear/grpc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,5 @@ func registerServices(
124124
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder))
125125
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder))
126126
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder))
127+
pb.RegisterLabyrinthServiceServer(srv, service.NewLabyrinthServiceServer(userStore, userStore, holder))
127128
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package masterdata
2+
3+
import (
4+
"log"
5+
"sort"
6+
7+
"lunar-tear/server/internal/utils"
8+
)
9+
10+
type LabyrinthChapter struct {
11+
EventQuestChapterId int32
12+
LatestSeasonNumber int32
13+
StageOrders []int32
14+
}
15+
16+
type LabyrinthStageTier struct {
17+
QuestMissionClearCount int32
18+
Rewards []RewardItem
19+
}
20+
21+
type LabyrinthSeasonMilestone struct {
22+
HeadQuestId int32
23+
HeadStageOrder int32
24+
Rewards []RewardItem
25+
}
26+
27+
type labyrinthStageKey struct {
28+
ChapterId int32
29+
StageOrder int32
30+
}
31+
32+
type LabyrinthCatalog struct {
33+
ChaptersByOrder []LabyrinthChapter
34+
ClearRewardsByStage map[labyrinthStageKey][]RewardItem
35+
AccumTiersByStage map[labyrinthStageKey][]LabyrinthStageTier
36+
SeasonMilestonesByChapter map[int32][]LabyrinthSeasonMilestone
37+
}
38+
39+
func (c *LabyrinthCatalog) StageClearReward(chapterId, stageOrder int32) []RewardItem {
40+
return c.ClearRewardsByStage[labyrinthStageKey{chapterId, stageOrder}]
41+
}
42+
43+
func (c *LabyrinthCatalog) CollectAccumulationRewards(chapterId, stageOrder, oldCount, targetCount int32) ([]RewardItem, int32) {
44+
var items []RewardItem
45+
highest := int32(0)
46+
for _, t := range c.AccumTiersByStage[labyrinthStageKey{chapterId, stageOrder}] {
47+
if t.QuestMissionClearCount > oldCount && t.QuestMissionClearCount <= targetCount {
48+
items = append(items, t.Rewards...)
49+
if t.QuestMissionClearCount > highest {
50+
highest = t.QuestMissionClearCount
51+
}
52+
}
53+
}
54+
return items, highest
55+
}
56+
57+
func (c *LabyrinthCatalog) SeasonMilestones(chapterId int32) []LabyrinthSeasonMilestone {
58+
return c.SeasonMilestonesByChapter[chapterId]
59+
}
60+
61+
func LoadLabyrinthCatalog() *LabyrinthCatalog {
62+
seasonRows, err := utils.ReadTable[EntityMEventQuestLabyrinthSeason]("m_event_quest_labyrinth_season")
63+
if err != nil {
64+
log.Printf("[labyrinth] m_event_quest_labyrinth_season unavailable, labyrinth disabled: %v", err)
65+
return &LabyrinthCatalog{}
66+
}
67+
stageRows, err := utils.ReadTable[EntityMEventQuestLabyrinthStage]("m_event_quest_labyrinth_stage")
68+
if err != nil {
69+
log.Printf("[labyrinth] m_event_quest_labyrinth_stage unavailable, labyrinth disabled: %v", err)
70+
return &LabyrinthCatalog{}
71+
}
72+
73+
// chapterId -> highest SeasonNumber
74+
latestSeason := make(map[int32]int32)
75+
for _, r := range seasonRows {
76+
if r.SeasonNumber > latestSeason[r.EventQuestChapterId] {
77+
latestSeason[r.EventQuestChapterId] = r.SeasonNumber
78+
}
79+
}
80+
// chapterId -> stage orders
81+
stagesByChapter := make(map[int32][]int32)
82+
for _, r := range stageRows {
83+
stagesByChapter[r.EventQuestChapterId] = append(stagesByChapter[r.EventQuestChapterId], r.StageOrder)
84+
}
85+
86+
chapters := make([]LabyrinthChapter, 0, len(latestSeason))
87+
for chapterId, season := range latestSeason {
88+
stages := stagesByChapter[chapterId]
89+
sort.Slice(stages, func(i, j int) bool { return stages[i] < stages[j] })
90+
chapters = append(chapters, LabyrinthChapter{
91+
EventQuestChapterId: chapterId,
92+
LatestSeasonNumber: season,
93+
StageOrders: stages,
94+
})
95+
}
96+
sort.Slice(chapters, func(i, j int) bool {
97+
return chapters[i].EventQuestChapterId < chapters[j].EventQuestChapterId
98+
})
99+
100+
clearRewards, accumTiers, seasonMilestones := loadLabyrinthRewards(seasonRows, stageRows)
101+
102+
log.Printf("labyrinth catalog loaded: %d chapters, %d stages with clear rewards, %d with accumulation rewards, %d chapters with season rewards",
103+
len(chapters), len(clearRewards), len(accumTiers), len(seasonMilestones))
104+
return &LabyrinthCatalog{
105+
ChaptersByOrder: chapters,
106+
ClearRewardsByStage: clearRewards,
107+
AccumTiersByStage: accumTiers,
108+
SeasonMilestonesByChapter: seasonMilestones,
109+
}
110+
}
111+
112+
func loadLabyrinthRewards(seasonRows []EntityMEventQuestLabyrinthSeason, stageRows []EntityMEventQuestLabyrinthStage) (
113+
clearRewards map[labyrinthStageKey][]RewardItem,
114+
accumTiers map[labyrinthStageKey][]LabyrinthStageTier,
115+
seasonMilestones map[int32][]LabyrinthSeasonMilestone,
116+
) {
117+
rewardGroupRows, err := utils.ReadTable[EntityMEventQuestLabyrinthRewardGroup]("m_event_quest_labyrinth_reward_group")
118+
if err != nil {
119+
log.Printf("[labyrinth] m_event_quest_labyrinth_reward_group unavailable, rewards disabled: %v", err)
120+
return nil, nil, nil
121+
}
122+
123+
// reward group id -> reward items
124+
itemsByRewardGroup := make(map[int32][]RewardItem)
125+
for _, r := range rewardGroupRows {
126+
itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId] = append(itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId], RewardItem{
127+
PossessionType: r.PossessionType,
128+
PossessionId: r.PossessionId,
129+
Count: r.Count,
130+
})
131+
}
132+
133+
// per-stage one-time clear reward
134+
clearRewards = make(map[labyrinthStageKey][]RewardItem)
135+
for _, r := range stageRows {
136+
if r.StageClearRewardGroupId == 0 {
137+
continue
138+
}
139+
if items := itemsByRewardGroup[r.StageClearRewardGroupId]; len(items) > 0 {
140+
clearRewards[labyrinthStageKey{r.EventQuestChapterId, r.StageOrder}] = items
141+
}
142+
}
143+
144+
if accumGroupRows, err := utils.ReadTable[EntityMEventQuestLabyrinthStageAccumulationRewardGroup]("m_event_quest_labyrinth_stage_accumulation_reward_group"); err != nil {
145+
log.Printf("[labyrinth] m_event_quest_labyrinth_stage_accumulation_reward_group unavailable, accumulation rewards disabled: %v", err)
146+
} else {
147+
// accumulation group id -> tiers (threshold + resolved reward items)
148+
tiersByGroup := make(map[int32][]LabyrinthStageTier)
149+
for _, r := range accumGroupRows {
150+
tiersByGroup[r.EventQuestLabyrinthStageAccumulationRewardGroupId] = append(tiersByGroup[r.EventQuestLabyrinthStageAccumulationRewardGroupId], LabyrinthStageTier{
151+
QuestMissionClearCount: r.QuestMissionClearCount,
152+
Rewards: itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
153+
})
154+
}
155+
accumTiers = make(map[labyrinthStageKey][]LabyrinthStageTier)
156+
for _, r := range stageRows {
157+
if r.StageAccumulationRewardGroupId == 0 {
158+
continue
159+
}
160+
tiers := tiersByGroup[r.StageAccumulationRewardGroupId]
161+
sort.Slice(tiers, func(i, j int) bool {
162+
return tiers[i].QuestMissionClearCount < tiers[j].QuestMissionClearCount
163+
})
164+
accumTiers[labyrinthStageKey{r.EventQuestChapterId, r.StageOrder}] = tiers
165+
}
166+
}
167+
168+
// per-chapter season-reward milestones
169+
if seasonRewardRows, err := utils.ReadTable[EntityMEventQuestLabyrinthSeasonRewardGroup]("m_event_quest_labyrinth_season_reward_group"); err != nil {
170+
log.Printf("[labyrinth] m_event_quest_labyrinth_season_reward_group unavailable, season rewards disabled: %v", err)
171+
} else {
172+
seasonMilestones = buildLabyrinthSeasonMilestones(seasonRows, seasonRewardRows, itemsByRewardGroup)
173+
}
174+
175+
return clearRewards, accumTiers, seasonMilestones
176+
}
177+
178+
func buildLabyrinthSeasonMilestones(
179+
seasonRows []EntityMEventQuestLabyrinthSeason,
180+
seasonRewardRows []EntityMEventQuestLabyrinthSeasonRewardGroup,
181+
itemsByRewardGroup map[int32][]RewardItem,
182+
) map[int32][]LabyrinthSeasonMilestone {
183+
// chapter -> SeasonRewardGroupId (all seasons of a chapter share one)
184+
groupByChapter := make(map[int32]int32)
185+
for _, r := range seasonRows {
186+
groupByChapter[r.EventQuestChapterId] = r.SeasonRewardGroupId
187+
}
188+
// SeasonRewardGroupId -> its rows, in table order
189+
rowsByGroup := make(map[int32][]EntityMEventQuestLabyrinthSeasonRewardGroup)
190+
for _, r := range seasonRewardRows {
191+
rowsByGroup[r.EventQuestLabyrinthSeasonRewardGroupId] = append(rowsByGroup[r.EventQuestLabyrinthSeasonRewardGroupId], r)
192+
}
193+
194+
milestones := make(map[int32][]LabyrinthSeasonMilestone)
195+
for chapterId, seasonGroupId := range groupByChapter {
196+
rows := rowsByGroup[seasonGroupId]
197+
if len(rows) == 0 {
198+
continue
199+
}
200+
// rank distinct reward-group ids ascending -> 1-based head stage order
201+
stageByRewardGroup := make(map[int32]int32)
202+
var distinct []int32
203+
for _, r := range rows {
204+
if _, seen := stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId]; !seen {
205+
stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId] = 0
206+
distinct = append(distinct, r.EventQuestLabyrinthRewardGroupId)
207+
}
208+
}
209+
sort.Slice(distinct, func(i, j int) bool { return distinct[i] < distinct[j] })
210+
for i, gid := range distinct {
211+
stageByRewardGroup[gid] = int32(i + 1)
212+
}
213+
214+
list := make([]LabyrinthSeasonMilestone, 0, len(rows))
215+
for _, r := range rows {
216+
list = append(list, LabyrinthSeasonMilestone{
217+
HeadQuestId: r.HeadQuestId,
218+
HeadStageOrder: stageByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
219+
Rewards: itemsByRewardGroup[r.EventQuestLabyrinthRewardGroupId],
220+
})
221+
}
222+
milestones[chapterId] = list
223+
}
224+
return milestones
225+
}

server/internal/model/quest.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ const (
1212
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
1313
)
1414

15-
// IsReplayQuestFlowType reports whether the flow type indicates an active
16-
// replay session — either same-route REPLAY_FLOW or cross-route
17-
// ANOTHER_ROUTE_REPLAY_FLOW. Mirrors the client's Story.IsReplayQuestFlowType
18-
// predicate (dump.cs:768202).
1915
func IsReplayQuestFlowType(t int32) bool {
2016
return t == int32(QuestFlowTypeReplayFlow) ||
2117
t == int32(QuestFlowTypeAnotherRouteReplayFlow)
@@ -170,7 +166,6 @@ const (
170166

171167
type SideStorySceneIdType int32
172168

173-
// Values mirror SideStoryTypes.SceneIdTypes in the client (dump.cs).
174169
const (
175170
SideStorySceneInvalid SideStorySceneIdType = 0
176171
SideStorySceneIntroduction SideStorySceneIdType = 1

server/internal/runtime/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ func buildCatalogs() (*Catalogs, error) {
141141

142142
towerCatalog := masterdata.LoadTowerCatalog()
143143

144+
labyrinthCatalog := masterdata.LoadLabyrinthCatalog()
145+
144146
return &Catalogs{
145147
GameConfig: gameConfig,
146148
Parts: partsCatalog,
@@ -167,6 +169,7 @@ func buildCatalogs() (*Catalogs, error) {
167169
SideStory: sideStoryCatalog,
168170
BigHunt: bigHuntCatalog,
169171
Tower: towerCatalog,
172+
Labyrinth: labyrinthCatalog,
170173
QuestHandler: questHandler,
171174
GachaHandler: gachaHandler,
172175
}, nil

server/internal/runtime/holder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type Catalogs struct {
5151
SideStory *masterdata.SideStoryCatalog
5252
BigHunt *masterdata.BigHuntCatalog
5353
Tower *masterdata.TowerCatalog
54+
Labyrinth *masterdata.LabyrinthCatalog
5455

5556
// Catalog-derived handlers must rebuild on every reload because they
5657
// embed/cache pointers to specific catalog instances.

0 commit comments

Comments
 (0)