Skip to content

Commit 63df7d7

Browse files
Add auto-repeat quest and memoir auto-sell
1 parent c961fde commit 63df7d7

19 files changed

Lines changed: 413 additions & 34 deletions

server/internal/model/quest.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ package model
22

33
import "fmt"
44

5+
type QuestType int32
6+
7+
const (
8+
QuestTypeUnknown QuestType = 0
9+
QuestTypeMain QuestType = 1
10+
QuestTypeEvent QuestType = 2
11+
QuestTypeExtra QuestType = 3
12+
QuestTypeBigHunt QuestType = 4
13+
)
14+
515
type QuestFlowType int32
616

717
const (

server/internal/questflow/bighunt_quest.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
3535
}
3636

3737
target := h.targetForBigHunt(questId)
38-
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
39-
if !isRetired {
38+
var outcome FinishOutcome
39+
if !isRetired && !isAnnihilated {
40+
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
4041
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
4142
}
4243

server/internal/questflow/event_quest.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
4444
}
4545

4646
target := h.targetForEvent(eventQuestChapterId, questId)
47-
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
48-
if !isRetired {
47+
var outcome FinishOutcome
48+
if !isRetired && !isAnnihilated {
49+
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
4950
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
5051
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
5152
}

server/internal/questflow/extra_quest.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
4242
}
4343

4444
target := h.targetForExtra(questId)
45-
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
46-
if !isRetired {
45+
var outcome FinishOutcome
46+
if !isRetired && !isAnnihilated {
47+
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
4748
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
4849
}
4950

server/internal/questflow/handler.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type RewardGrant struct {
1313
PossessionType model.PossessionType
1414
PossessionId int32
1515
Count int32
16+
IsAutoSale bool
1617
}
1718

1819
type FinishOutcome struct {
@@ -36,7 +37,7 @@ type QuestHandler struct {
3637
}
3738

3839
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog, characterRebirth *masterdata.CharacterRebirthCatalog) *QuestHandler {
39-
granter := BuildGranter(catalog)
40+
granter := BuildGranter(catalog, config)
4041
var sideStoryChapters map[int32]int32
4142
if sideStory != nil {
4243
sideStoryChapters = sideStory.ChapterByEventQuestId
@@ -51,7 +52,7 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo
5152
}
5253
}
5354

54-
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
55+
func BuildGranter(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *store.PossessionGranter {
5556
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
5657
for id, cm := range catalog.CostumeById {
5758
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
@@ -111,6 +112,15 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
111112
}
112113
}
113114

115+
partsSellPriceL1 := make(map[int32]int32, len(catalog.SellPriceByRarity))
116+
for rarity, fn := range catalog.SellPriceByRarity {
117+
partsSellPriceL1[int32(rarity)] = fn.Evaluate(1)
118+
}
119+
var goldItemId int32
120+
if config != nil {
121+
goldItemId = config.ConsumableItemIdForGold
122+
}
123+
114124
return &store.PossessionGranter{
115125
CostumeById: costumeById,
116126
WeaponById: weaponById,
@@ -122,5 +132,7 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
122132
PartsVariantsByGroupRarity: partsVariants,
123133
PartsSubStatusPool: catalog.SubStatusPool,
124134
PartsSubStatusDefs: partsSubDefs,
135+
PartsSellPriceL1ByRarity: partsSellPriceL1,
136+
GoldConsumableItemId: goldItemId,
125137
}
126138
}

server/internal/questflow/quest.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,8 @@ func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, o
204204
}
205205
questState.IsRewardGranted = true
206206
}
207-
for _, drop := range outcome.DropRewards {
208-
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
209-
}
207+
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
208+
h.grantDropRewards(user, outcome.DropRewards, raritySet, rankSet, nowMillis)
210209
for _, reward := range outcome.ReplayFlowFirstClearRewards {
211210
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
212211
}
@@ -260,11 +259,12 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
260259

261260
h.initQuestState(user, questId)
262261

263-
outcome := h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
264262
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
265263
wasMenuReplay := user.MainQuest.SavedContext.Active
266264

267-
if !isRetired {
265+
var outcome FinishOutcome
266+
if !isRetired && !isAnnihilated {
267+
outcome = h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
268268
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
269269

270270
// A replay-flow finish must NOT move the MainFlow scene pointer: the
@@ -334,12 +334,11 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
334334
if user.ConsumableItems[skipTicketId] < 0 {
335335
user.ConsumableItems[skipTicketId] = 0
336336
}
337+
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
337338
var allDrops []RewardGrant
338339
for range skipCount {
339340
drops := h.computeDropRewards(questDef, target, nowMillis)
340-
for _, drop := range drops {
341-
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
342-
}
341+
h.grantDropRewards(user, drops, raritySet, rankSet, nowMillis)
343342
allDrops = append(allDrops, drops...)
344343

345344
if questDef.Gold != 0 {

server/internal/questflow/rewards.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package questflow
33
import (
44
"fmt"
55
"log"
6+
"strconv"
7+
"strings"
68

79
"lunar-tear/server/internal/campaign"
810
"lunar-tear/server/internal/gameutil"
@@ -128,6 +130,54 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
128130
return outcome
129131
}
130132

133+
var autoSaleRarityTiers = map[int32]bool{10: true, 20: true, 30: true, 40: true, 50: true}
134+
135+
// Rarity tiers (10..50) and ranks (1..5) are disjoint, so the delimited values
136+
// are classified by range — independent of the client's map key or delimiter.
137+
func parseAutoSaleRules(settings map[int32]store.AutoSaleSettingState) (raritySet, rankSet map[int32]bool) {
138+
raritySet = map[int32]bool{}
139+
rankSet = map[int32]bool{}
140+
for _, s := range settings {
141+
for _, n := range extractInts(s.PossessionAutoSaleItemValue) {
142+
switch {
143+
case autoSaleRarityTiers[n]:
144+
raritySet[n] = true
145+
case n >= 1 && n <= 5:
146+
rankSet[n] = true
147+
}
148+
}
149+
}
150+
return raritySet, rankSet
151+
}
152+
153+
func extractInts(s string) []int32 {
154+
fields := strings.FieldsFunc(s, func(r rune) bool { return r < '0' || r > '9' })
155+
out := make([]int32, 0, len(fields))
156+
for _, f := range fields {
157+
if v, err := strconv.Atoi(f); err == nil {
158+
out = append(out, int32(v))
159+
}
160+
}
161+
return out
162+
}
163+
164+
func (h *QuestHandler) grantDropRewards(user *store.UserState, drops []RewardGrant, raritySet, rankSet map[int32]bool, nowMillis int64) {
165+
for i := range drops {
166+
d := drops[i]
167+
if d.PossessionType == model.PossessionTypeParts || d.PossessionType == model.PossessionTypePartsEnhanced {
168+
chosenId, sold := h.Granter.GrantOrSellPartsDrop(user, d.PossessionId, raritySet, rankSet, nowMillis)
169+
if sold {
170+
// Sold parts have no inventory row, so the popup needs the rolled
171+
// variant id; kept parts read theirs from the parts table diff.
172+
drops[i].PossessionId = chosenId
173+
drops[i].IsAutoSale = true
174+
}
175+
continue
176+
}
177+
h.applyRewardPossession(user, d.PossessionType, d.PossessionId, d.Count, nowMillis)
178+
}
179+
}
180+
131181
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest, target campaign.QuestTarget, nowMillis int64) []RewardGrant {
132182
var drops []RewardGrant
133183
var dropRate campaign.DropRateMul
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package service
2+
3+
import (
4+
"log"
5+
6+
"lunar-tear/server/internal/model"
7+
"lunar-tear/server/internal/questflow"
8+
"lunar-tear/server/internal/store"
9+
)
10+
11+
func startAutoOrbit(user *store.UserState, questType model.QuestType, chapterId, questId, maxCount int32, nowMillis int64) {
12+
if maxCount <= 0 {
13+
if user.QuestAutoOrbit.MaxAutoOrbitCount > 0 {
14+
log.Printf("[autoOrbit] clear (start without max): prev questType=%d chapter=%d quest=%d cleared=%d/%d",
15+
user.QuestAutoOrbit.QuestType, user.QuestAutoOrbit.ChapterId, user.QuestAutoOrbit.QuestId,
16+
user.QuestAutoOrbit.ClearedAutoOrbitCount, user.QuestAutoOrbit.MaxAutoOrbitCount)
17+
}
18+
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
19+
return
20+
}
21+
s := user.QuestAutoOrbit
22+
if s.MaxAutoOrbitCount > 0 &&
23+
s.QuestType == int32(questType) && s.ChapterId == chapterId &&
24+
s.QuestId == questId && s.MaxAutoOrbitCount == maxCount {
25+
s.LatestVersion = nowMillis
26+
user.QuestAutoOrbit = s
27+
log.Printf("[autoOrbit] continue cleared=%d/%d", s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount)
28+
return
29+
}
30+
log.Printf("[autoOrbit] start questType=%d chapter=%d quest=%d max=%d", questType, chapterId, questId, maxCount)
31+
user.QuestAutoOrbit = store.QuestAutoOrbitState{
32+
QuestType: int32(questType),
33+
ChapterId: chapterId,
34+
QuestId: questId,
35+
MaxAutoOrbitCount: maxCount,
36+
LatestVersion: nowMillis,
37+
}
38+
}
39+
40+
func finishAutoOrbit(user *store.UserState, isAutoOrbit, isRetired, isAnnihilated bool, questType model.QuestType, chapterId, questId int32, nowMillis int64, drops []questflow.RewardGrant) (endedDrops []store.AutoOrbitDropEntry, loopEnded bool) {
41+
s := user.QuestAutoOrbit
42+
if s.MaxAutoOrbitCount <= 0 {
43+
return nil, false
44+
}
45+
if s.QuestType != int32(questType) || s.ChapterId != chapterId || s.QuestId != questId {
46+
log.Printf("[autoOrbit] finish for other quest, ignored: tracked={qt=%d ch=%d q=%d} got={qt=%d ch=%d q=%d}",
47+
s.QuestType, s.ChapterId, s.QuestId, int32(questType), chapterId, questId)
48+
return nil, false
49+
}
50+
if !isRetired && !isAnnihilated {
51+
added := 0
52+
for _, d := range drops {
53+
s.AccumulatedDrops = append(s.AccumulatedDrops, store.AutoOrbitDropEntry{
54+
PossessionType: int32(d.PossessionType),
55+
PossessionId: d.PossessionId,
56+
Count: d.Count,
57+
IsAutoSale: d.IsAutoSale,
58+
})
59+
added++
60+
}
61+
s.ClearedAutoOrbitCount++
62+
log.Printf("[autoOrbit] iter cleared=%d/%d +%d drops (total=%d)",
63+
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, added, len(s.AccumulatedDrops))
64+
}
65+
s.LastClearDatetime = nowMillis
66+
s.LatestVersion = nowMillis
67+
if !isAutoOrbit || isRetired || isAnnihilated || s.ClearedAutoOrbitCount >= s.MaxAutoOrbitCount {
68+
log.Printf("[autoOrbit] loop end: cleared=%d/%d total drops=%d (returned in response, accumulator kept)",
69+
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, len(s.AccumulatedDrops))
70+
user.QuestAutoOrbit = store.QuestAutoOrbitState{AccumulatedDrops: s.AccumulatedDrops}
71+
return s.AccumulatedDrops, true
72+
}
73+
user.QuestAutoOrbit = s
74+
return nil, false
75+
}
76+
77+
func consumeAutoOrbitRewards(user *store.UserState) []store.AutoOrbitDropEntry {
78+
drops := user.QuestAutoOrbit.AccumulatedDrops
79+
log.Printf("[autoOrbit] consume on FinishAutoOrbit: returning %d drops (loop status max=%d cleared=%d)",
80+
len(drops), user.QuestAutoOrbit.MaxAutoOrbitCount, user.QuestAutoOrbit.ClearedAutoOrbitCount)
81+
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
82+
return drops
83+
}

server/internal/service/quest_event.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ import (
66

77
pb "lunar-tear/server/gen/proto"
88
"lunar-tear/server/internal/gametime"
9+
"lunar-tear/server/internal/model"
910
"lunar-tear/server/internal/questflow"
1011
"lunar-tear/server/internal/store"
1112

1213
emptypb "google.golang.org/protobuf/types/known/emptypb"
1314
)
1415

1516
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
16-
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
17+
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v maxAutoOrbitCount=%d",
18+
req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.MaxAutoOrbitCount)
1719

1820
engine := s.holder.Get().QuestHandler
1921
userId := CurrentUserId(ctx, s.users, s.sessions)
2022
nowMillis := gametime.NowMillis()
2123
s.users.UpdateUser(userId, func(user *store.UserState) {
2224
engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
25+
startAutoOrbit(user, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, req.MaxAutoOrbitCount, nowMillis)
2326
})
2427

2528
drops := engine.BattleDropRewards(req.QuestId)
@@ -38,16 +41,25 @@ func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartE
3841
}
3942

4043
func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.FinishEventQuestRequest) (*pb.FinishEventQuestResponse, error) {
41-
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
44+
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v isAutoOrbit=%v",
45+
req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, req.IsAutoOrbit)
4246

4347
nowMillis := gametime.NowMillis()
4448
engine := s.holder.Get().QuestHandler
4549
userId := CurrentUserId(ctx, s.users, s.sessions)
4650
var outcome questflow.FinishOutcome
51+
var endedDrops []store.AutoOrbitDropEntry
52+
var loopEnded bool
4753
s.users.UpdateUser(userId, func(user *store.UserState) {
4854
outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
55+
endedDrops, loopEnded = finishAutoOrbit(user, req.IsAutoOrbit, req.IsRetired, req.IsAnnihilated, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, nowMillis, outcome.DropRewards)
4956
})
5057

58+
autoOrbitReward := emptyAutoOrbitReward()
59+
if loopEnded {
60+
autoOrbitReward.DropReward = autoOrbitDropsToProto(endedDrops)
61+
}
62+
5163
return &pb.FinishEventQuestResponse{
5264
DropReward: toProtoRewards(outcome.DropRewards),
5365
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
@@ -57,6 +69,7 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
5769
IsBigWin: outcome.IsBigWin,
5870
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
5971
UserStatusCampaignReward: []*pb.QuestReward{},
72+
AutoOrbitReward: autoOrbitReward,
6073
}, nil
6174
}
6275

0 commit comments

Comments
 (0)