Add campaign bonuses; fix parts variant/sub-stat grants and menu-pick quest resume state
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled

This commit is contained in:
Ilya Groshev
2026-05-25 09:31:53 +03:00
parent 2d0c0d8ef0
commit dc7c1df4fd
21 changed files with 825 additions and 69 deletions
+7 -4
View File
@@ -17,7 +17,8 @@ func (h *QuestHandler) HandleBigHuntQuestStart(user *store.UserState, questId, u
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForBigHunt(questId), nowMillis)
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
@@ -33,13 +34,15 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
target := h.targetForBigHunt(questId)
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
if !isRetired {
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
if isRetired && !isAnnihilated && consumed > 1 {
refund := consumed - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
+56
View File
@@ -0,0 +1,56 @@
package questflow
import (
"lunar-tear/server/internal/campaign"
"lunar-tear/server/internal/model"
)
func (h *QuestHandler) targetForMain(questId int32) campaign.QuestTarget {
return campaign.QuestTarget{
QuestId: questId,
QuestType: campaign.QuestTypeMainQuest,
ChapterId: h.MainQuestChapterIdByQuestId[questId],
}
}
func (h *QuestHandler) targetForEvent(eventChapterId, questId int32) campaign.QuestTarget {
return campaign.QuestTarget{
QuestId: questId,
QuestType: campaign.QuestTypeEventQuest,
EventQuestType: h.EventQuestTypeByChapterId[eventChapterId],
ChapterId: eventChapterId,
}
}
func (h *QuestHandler) targetForExtra(questId int32) campaign.QuestTarget {
return campaign.QuestTarget{QuestId: questId, QuestType: campaign.QuestTypeExtraQuest}
}
func (h *QuestHandler) targetForBigHunt(questId int32) campaign.QuestTarget {
return campaign.QuestTarget{QuestId: questId, QuestType: campaign.QuestTypeBigHunt}
}
func (h *QuestHandler) campaignFilter(nowMillis int64) campaign.Filter {
return campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll}
}
func (h *QuestHandler) staminaWithCampaign(baseStamina int32, t campaign.QuestTarget, nowMillis int64) int32 {
if h.Campaigns == nil {
return baseStamina
}
return h.Campaigns.QuestStamina(t, h.campaignFilter(nowMillis)).Apply(baseStamina)
}
func (h *QuestHandler) appendBonusDrops(drops []RewardGrant, t campaign.QuestTarget, nowMillis int64) []RewardGrant {
if h.Campaigns == nil {
return drops
}
for _, bd := range h.Campaigns.QuestBonusDrops(t, h.campaignFilter(nowMillis)) {
drops = append(drops, RewardGrant{
PossessionType: model.PossessionType(bd.PossessionType),
PossessionId: bd.PossessionId,
Count: bd.Count,
})
}
return drops
}
+7 -4
View File
@@ -18,7 +18,8 @@ func (h *QuestHandler) HandleEventQuestStart(user *store.UserState, eventQuestCh
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForEvent(eventQuestChapterId, questId), nowMillis)
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
@@ -42,14 +43,16 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
target := h.targetForEvent(eventQuestChapterId, questId)
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
if !isRetired {
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
if isRetired && !isAnnihilated && consumed > 1 {
refund := consumed - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
+7 -4
View File
@@ -18,7 +18,8 @@ func (h *QuestHandler) HandleExtraQuestStart(user *store.UserState, questId, use
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForExtra(questId), nowMillis)
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
@@ -40,13 +41,15 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
target := h.targetForExtra(questId)
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
if !isRetired {
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
if isRetired && !isAnnihilated && consumed > 1 {
refund := consumed - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
+37 -1
View File
@@ -1,6 +1,9 @@
package questflow
import (
"sort"
"lunar-tear/server/internal/campaign"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
@@ -28,9 +31,10 @@ type QuestHandler struct {
Config *masterdata.GameConfig
Granter *store.PossessionGranter
SideStoryChapterByEventQuestId map[int32]int32
Campaigns *campaign.Catalog
}
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog) *QuestHandler {
granter := BuildGranter(catalog)
var sideStoryChapters map[int32]int32
if sideStory != nil {
@@ -41,6 +45,7 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo
Config: config,
Granter: granter,
SideStoryChapterByEventQuestId: sideStoryChapters,
Campaigns: campaigns,
}
}
@@ -70,12 +75,40 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
releaseConditions[groupId] = conds
}
partsById := make(map[int32]store.PartsRef, len(catalog.PartsById))
partsVariants := make(map[int32]map[int32][]int32)
for id, p := range catalog.PartsById {
partsById[id] = store.PartsRef{
PartsGroupId: p.PartsGroupId,
RarityType: p.RarityType,
PartsInitialLotteryId: p.PartsInitialLotteryId,
PartsStatusMainLotteryGroupId: p.PartsStatusMainLotteryGroupId,
PartsStatusSubLotteryGroupId: p.PartsStatusSubLotteryGroupId,
}
if partsVariants[p.PartsGroupId] == nil {
partsVariants[p.PartsGroupId] = map[int32][]int32{}
}
partsVariants[p.PartsGroupId][p.RarityType] = append(partsVariants[p.PartsGroupId][p.RarityType], p.PartsId)
}
for _, byRarity := range partsVariants {
for _, ids := range byRarity {
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
}
}
partsSubDefs := make(map[int32]store.PartsStatusSubDef, len(catalog.PartsStatusMainById))
for id, d := range catalog.PartsStatusMainById {
var fn func(int32) int32
if f, ok := catalog.FuncResolver.Resolve(d.StatusNumericalFunctionId); ok {
fn = f.Evaluate
}
partsSubDefs[id] = store.PartsStatusSubDef{
StatusKindType: d.StatusKindType,
StatusCalculationType: d.StatusCalculationType,
StatusChangeInitialValue: d.StatusChangeInitialValue,
StatusFunc: fn,
}
}
return &store.PossessionGranter{
CostumeById: costumeById,
WeaponById: weaponById,
@@ -84,5 +117,8 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
ReleaseConditions: releaseConditions,
PartsById: partsById,
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
PartsVariantsByGroupRarity: partsVariants,
PartsSubStatusPool: catalog.SubStatusPool,
PartsSubStatusDefs: partsSubDefs,
}
}
+10 -7
View File
@@ -61,7 +61,8 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
h.initQuestState(user, questId)
if quest.Stamina > 0 {
store.ConsumeStamina(user, quest.Stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForMain(questId), nowMillis)
store.ConsumeStamina(user, stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
}
questState := user.Quests[questId]
@@ -259,7 +260,7 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
h.initQuestState(user, questId)
outcome := h.evaluateFinishOutcome(user, questId)
outcome := h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
wasMenuReplay := user.MainQuest.SavedContext.Active
@@ -277,8 +278,9 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
}
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
consumed := h.staminaWithCampaign(quest.Stamina, h.targetForMain(questId), nowMillis)
if isRetired && !isAnnihilated && consumed > 1 {
refund := consumed - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
@@ -322,18 +324,19 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
}
target := h.targetForMain(questId)
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, skipCount, maxMillis, nowMillis)
perSkipStamina := h.staminaWithCampaign(questDef.Stamina, target, nowMillis)
store.ConsumeStamina(user, perSkipStamina*skipCount, maxMillis, nowMillis)
skipTicketId := h.Config.ConsumableItemIdForQuestSkipTicket
user.ConsumableItems[skipTicketId] -= skipCount
if user.ConsumableItems[skipTicketId] < 0 {
user.ConsumableItems[skipTicketId] = 0
}
var allDrops []RewardGrant
for range skipCount {
drops := h.computeDropRewards(questDef)
drops := h.computeDropRewards(questDef, target, nowMillis)
for _, drop := range drops {
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
}
+18 -14
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"lunar-tear/server/internal/campaign"
"lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
@@ -40,7 +41,7 @@ func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef m
return rewardGroupId
}
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32) FinishOutcome {
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32, target campaign.QuestTarget, nowMillis int64) FinishOutcome {
outcome := FinishOutcome{}
questState, ok := user.Quests[questId]
if !ok {
@@ -123,25 +124,28 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
}
}
outcome.DropRewards = h.computeDropRewards(questDef)
outcome.DropRewards = h.computeDropRewards(questDef, target, nowMillis)
return outcome
}
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest) []RewardGrant {
if questDef.QuestPickupRewardGroupId == 0 {
return nil
}
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest, target campaign.QuestTarget, nowMillis int64) []RewardGrant {
var drops []RewardGrant
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
drops = append(drops, RewardGrant{
PossessionType: model.PossessionType(bdr.PossessionType),
PossessionId: bdr.PossessionId,
Count: bdr.Count,
})
var dropRate campaign.DropRateMul
if h.Campaigns != nil {
dropRate = h.Campaigns.QuestDropRate(target, h.campaignFilter(nowMillis))
}
if questDef.QuestPickupRewardGroupId != 0 {
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
drops = append(drops, RewardGrant{
PossessionType: model.PossessionType(bdr.PossessionType),
PossessionId: bdr.PossessionId,
Count: dropRate.Apply(bdr.Count),
})
}
}
}
return drops
return h.appendBonusDrops(drops, target, nowMillis)
}
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {