Add auto-repeat quest and memoir auto-sell
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled

This commit is contained in:
Ilya Groshev
2026-05-28 10:48:26 +03:00
parent c961fde8ac
commit 63df7d7055
19 changed files with 413 additions and 34 deletions
@@ -0,0 +1,83 @@
package service
import (
"log"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store"
)
func startAutoOrbit(user *store.UserState, questType model.QuestType, chapterId, questId, maxCount int32, nowMillis int64) {
if maxCount <= 0 {
if user.QuestAutoOrbit.MaxAutoOrbitCount > 0 {
log.Printf("[autoOrbit] clear (start without max): prev questType=%d chapter=%d quest=%d cleared=%d/%d",
user.QuestAutoOrbit.QuestType, user.QuestAutoOrbit.ChapterId, user.QuestAutoOrbit.QuestId,
user.QuestAutoOrbit.ClearedAutoOrbitCount, user.QuestAutoOrbit.MaxAutoOrbitCount)
}
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
return
}
s := user.QuestAutoOrbit
if s.MaxAutoOrbitCount > 0 &&
s.QuestType == int32(questType) && s.ChapterId == chapterId &&
s.QuestId == questId && s.MaxAutoOrbitCount == maxCount {
s.LatestVersion = nowMillis
user.QuestAutoOrbit = s
log.Printf("[autoOrbit] continue cleared=%d/%d", s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount)
return
}
log.Printf("[autoOrbit] start questType=%d chapter=%d quest=%d max=%d", questType, chapterId, questId, maxCount)
user.QuestAutoOrbit = store.QuestAutoOrbitState{
QuestType: int32(questType),
ChapterId: chapterId,
QuestId: questId,
MaxAutoOrbitCount: maxCount,
LatestVersion: nowMillis,
}
}
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) {
s := user.QuestAutoOrbit
if s.MaxAutoOrbitCount <= 0 {
return nil, false
}
if s.QuestType != int32(questType) || s.ChapterId != chapterId || s.QuestId != questId {
log.Printf("[autoOrbit] finish for other quest, ignored: tracked={qt=%d ch=%d q=%d} got={qt=%d ch=%d q=%d}",
s.QuestType, s.ChapterId, s.QuestId, int32(questType), chapterId, questId)
return nil, false
}
if !isRetired && !isAnnihilated {
added := 0
for _, d := range drops {
s.AccumulatedDrops = append(s.AccumulatedDrops, store.AutoOrbitDropEntry{
PossessionType: int32(d.PossessionType),
PossessionId: d.PossessionId,
Count: d.Count,
IsAutoSale: d.IsAutoSale,
})
added++
}
s.ClearedAutoOrbitCount++
log.Printf("[autoOrbit] iter cleared=%d/%d +%d drops (total=%d)",
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, added, len(s.AccumulatedDrops))
}
s.LastClearDatetime = nowMillis
s.LatestVersion = nowMillis
if !isAutoOrbit || isRetired || isAnnihilated || s.ClearedAutoOrbitCount >= s.MaxAutoOrbitCount {
log.Printf("[autoOrbit] loop end: cleared=%d/%d total drops=%d (returned in response, accumulator kept)",
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, len(s.AccumulatedDrops))
user.QuestAutoOrbit = store.QuestAutoOrbitState{AccumulatedDrops: s.AccumulatedDrops}
return s.AccumulatedDrops, true
}
user.QuestAutoOrbit = s
return nil, false
}
func consumeAutoOrbitRewards(user *store.UserState) []store.AutoOrbitDropEntry {
drops := user.QuestAutoOrbit.AccumulatedDrops
log.Printf("[autoOrbit] consume on FinishAutoOrbit: returning %d drops (loop status max=%d cleared=%d)",
len(drops), user.QuestAutoOrbit.MaxAutoOrbitCount, user.QuestAutoOrbit.ClearedAutoOrbitCount)
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
return drops
}
+15 -2
View File
@@ -6,6 +6,7 @@ import (
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store"
@@ -13,13 +14,15 @@ import (
)
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v maxAutoOrbitCount=%d",
req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.MaxAutoOrbitCount)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
s.users.UpdateUser(userId, func(user *store.UserState) {
engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
startAutoOrbit(user, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, req.MaxAutoOrbitCount, nowMillis)
})
drops := engine.BattleDropRewards(req.QuestId)
@@ -38,16 +41,25 @@ func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartE
}
func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.FinishEventQuestRequest) (*pb.FinishEventQuestResponse, error) {
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v isAutoOrbit=%v",
req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, req.IsAutoOrbit)
nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome
var endedDrops []store.AutoOrbitDropEntry
var loopEnded bool
s.users.UpdateUser(userId, func(user *store.UserState) {
outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
endedDrops, loopEnded = finishAutoOrbit(user, req.IsAutoOrbit, req.IsRetired, req.IsAnnihilated, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, nowMillis, outcome.DropRewards)
})
autoOrbitReward := emptyAutoOrbitReward()
if loopEnded {
autoOrbitReward.DropReward = autoOrbitDropsToProto(endedDrops)
}
return &pb.FinishEventQuestResponse{
DropReward: toProtoRewards(outcome.DropRewards),
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
@@ -57,6 +69,7 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
IsBigWin: outcome.IsBigWin,
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
UserStatusCampaignReward: []*pb.QuestReward{},
AutoOrbitReward: autoOrbitReward,
}, nil
}
+55 -4
View File
@@ -65,7 +65,8 @@ func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, r
}
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
log.Printf("[QuestService] StartMainQuest: %+v", req)
log.Printf("[QuestService] StartMainQuest: questId=%d isMainFlow=%v isReplayFlow=%v isBattleOnly=%v maxAutoOrbitCount=%d",
req.QuestId, req.IsMainFlow, req.IsReplayFlow, req.IsBattleOnly, req.MaxAutoOrbitCount)
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions)
@@ -76,6 +77,7 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa
} else {
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.IsMainFlow, req.UserDeckNumber, nowMillis)
}
startAutoOrbit(user, model.QuestTypeMain, 0, req.QuestId, req.MaxAutoOrbitCount, nowMillis)
})
drops := engine.BattleDropRewards(req.QuestId)
@@ -93,6 +95,26 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa
}, nil
}
func emptyAutoOrbitReward() *pb.QuestAutoOrbitResult {
return &pb.QuestAutoOrbitResult{
DropReward: []*pb.QuestReward{},
UserStatusCampaignReward: []*pb.QuestReward{},
}
}
func autoOrbitDropsToProto(drops []store.AutoOrbitDropEntry) []*pb.QuestReward {
out := make([]*pb.QuestReward, len(drops))
for i, d := range drops {
out[i] = &pb.QuestReward{
PossessionType: d.PossessionType,
PossessionId: d.PossessionId,
Count: d.Count,
IsAutoSale: d.IsAutoSale,
}
}
return out
}
func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
if len(grants) == 0 {
return []*pb.QuestReward{}
@@ -103,23 +125,32 @@ func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
PossessionType: int32(g.PossessionType),
PossessionId: g.PossessionId,
Count: g.Count,
IsAutoSale: g.IsAutoSale,
}
}
return out
}
func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.FinishMainQuestRequest) (*pb.FinishMainQuestResponse, error) {
log.Printf("[QuestService] FinishMainQuest: questId=%d isMainFlow=%v isRetired=%v isAnnihilated=%v storySkipType=%d",
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
log.Printf("[QuestService] FinishMainQuest: questId=%d isMainFlow=%v isRetired=%v isAnnihilated=%v isAutoOrbit=%v storySkipType=%d",
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.IsAutoOrbit, req.StorySkipType)
nowMillis := gametime.NowMillis()
engine := s.holder.Get().QuestHandler
userId := CurrentUserId(ctx, s.users, s.sessions)
var outcome questflow.FinishOutcome
var endedDrops []store.AutoOrbitDropEntry
var loopEnded bool
s.users.UpdateUser(userId, func(user *store.UserState) {
outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
endedDrops, loopEnded = finishAutoOrbit(user, req.IsAutoOrbit, req.IsRetired, req.IsAnnihilated, model.QuestTypeMain, 0, req.QuestId, nowMillis, outcome.DropRewards)
})
autoOrbitReward := emptyAutoOrbitReward()
if loopEnded {
autoOrbitReward.DropReward = autoOrbitDropsToProto(endedDrops)
}
return &pb.FinishMainQuestResponse{
DropReward: toProtoRewards(outcome.DropRewards),
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
@@ -130,6 +161,7 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
ReplayFlowFirstClearReward: toProtoRewards(outcome.ReplayFlowFirstClearRewards),
UserStatusCampaignReward: []*pb.QuestReward{},
AutoOrbitReward: autoOrbitReward,
}, nil
}
@@ -162,7 +194,26 @@ func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.Resta
func (s *QuestServiceServer) FinishAutoOrbit(ctx context.Context, req *emptypb.Empty) (*pb.FinishAutoOrbitResponse, error) {
log.Printf("[QuestService] FinishAutoOrbit")
return &pb.FinishAutoOrbitResponse{}, nil
userId := CurrentUserId(ctx, s.users, s.sessions)
var drops []store.AutoOrbitDropEntry
s.users.UpdateUser(userId, func(user *store.UserState) {
drops = consumeAutoOrbitRewards(user)
})
pbDrops := make([]*pb.QuestReward, len(drops))
for i, d := range drops {
pbDrops[i] = &pb.QuestReward{
PossessionType: d.PossessionType,
PossessionId: d.PossessionId,
Count: d.Count,
}
}
return &pb.FinishAutoOrbitResponse{
AutoOrbitResult: []*pb.QuestReward{},
AutoOrbitReward: &pb.QuestAutoOrbitResult{
DropReward: pbDrops,
UserStatusCampaignReward: []*pb.QuestReward{},
},
}, nil
}
func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestRequest) (*pb.SkipQuestResponse, error) {