Initial commit

This commit is contained in:
Ilya Groshev
2026-04-14 09:28:26 +03:00
commit 02f511f40c
161 changed files with 21541 additions and 0 deletions
@@ -0,0 +1,50 @@
package questflow
import (
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) HandleBigHuntQuestStart(user *store.UserState, questId, userDeckNumber int32, nowMillis int64) {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestStart", questId))
}
h.initQuestState(user, questId)
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
questState.UserDeckNumber = userDeckNumber
questState.QuestStateType = model.UserQuestStateTypeActive
questState.LatestStartDatetime = nowMillis
user.Quests[questId] = questState
}
func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
if !isRetired {
h.applyQuestVictory(user, questId, outcome, nowMillis)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
h.clearQuestMissions(user, questId, nowMillis)
return outcome
}
+89
View File
@@ -0,0 +1,89 @@
package questflow
import (
"fmt"
"log"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) HandleEventQuestStart(user *store.UserState, eventQuestChapterId, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestStart", questId))
}
h.initQuestState(user, questId)
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
questState.IsBattleOnly = isBattleOnly
questState.UserDeckNumber = userDeckNumber
questState.QuestStateType = model.UserQuestStateTypeActive
questState.LatestStartDatetime = nowMillis
user.Quests[questId] = questState
user.EventQuest.CurrentEventQuestChapterId = eventQuestChapterId
user.EventQuest.CurrentQuestId = questId
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
user.EventQuest.CurrentQuestSceneId = sceneIds[0]
user.EventQuest.HeadQuestSceneId = sceneIds[0]
}
}
func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestChapterId, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
if !isRetired {
h.applyQuestVictory(user, questId, outcome, nowMillis)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
user.EventQuest.CurrentQuestId = 0
user.EventQuest.CurrentQuestSceneId = 0
user.EventQuest.HeadQuestSceneId = 0
h.clearQuestMissions(user, questId, nowMillis)
return outcome
}
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
h.HandleQuestRestart(user, questId, nowMillis)
user.EventQuest.CurrentEventQuestChapterId = eventQuestChapterId
user.EventQuest.CurrentQuestId = questId
}
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
scene, ok := h.SceneById[questSceneId]
if !ok {
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
return
}
user.EventQuest.CurrentQuestSceneId = questSceneId
if h.isSceneAhead(questSceneId, user.EventQuest.HeadQuestSceneId) {
user.EventQuest.HeadQuestSceneId = questSceneId
}
h.applySceneGrants(user, questSceneId, nowMillis)
if scene.QuestResultType == model.QuestResultTypeHalfResult {
h.clearQuestMissions(user, scene.QuestId, nowMillis)
}
}
+86
View File
@@ -0,0 +1,86 @@
package questflow
import (
"fmt"
"log"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) HandleExtraQuestStart(user *store.UserState, questId, userDeckNumber int32, nowMillis int64) {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestStart", questId))
}
h.initQuestState(user, questId)
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
questState.UserDeckNumber = userDeckNumber
questState.QuestStateType = model.UserQuestStateTypeActive
questState.LatestStartDatetime = nowMillis
user.Quests[questId] = questState
user.ExtraQuest.CurrentQuestId = questId
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
user.ExtraQuest.CurrentQuestSceneId = sceneIds[0]
user.ExtraQuest.HeadQuestSceneId = sceneIds[0]
}
}
func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
if !isRetired {
h.applyQuestVictory(user, questId, outcome, nowMillis)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
user.ExtraQuest.CurrentQuestId = 0
user.ExtraQuest.CurrentQuestSceneId = 0
user.ExtraQuest.HeadQuestSceneId = 0
h.clearQuestMissions(user, questId, nowMillis)
return outcome
}
func (h *QuestHandler) HandleExtraQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
h.HandleQuestRestart(user, questId, nowMillis)
user.ExtraQuest.CurrentQuestId = questId
}
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
scene, ok := h.SceneById[questSceneId]
if !ok {
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
return
}
user.ExtraQuest.CurrentQuestSceneId = questSceneId
if h.isSceneAhead(questSceneId, user.ExtraQuest.HeadQuestSceneId) {
user.ExtraQuest.HeadQuestSceneId = questSceneId
}
h.applySceneGrants(user, questSceneId, nowMillis)
if scene.QuestResultType == model.QuestResultTypeHalfResult {
h.clearQuestMissions(user, scene.QuestId, nowMillis)
}
}
+68
View File
@@ -0,0 +1,68 @@
package questflow
import (
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
type RewardGrant struct {
PossessionType model.PossessionType
PossessionId int32
Count int32
}
type FinishOutcome struct {
DropRewards []RewardGrant
FirstClearRewards []RewardGrant
ReplayFlowFirstClearRewards []RewardGrant
MissionClearRewards []RewardGrant
MissionClearCompleteRewards []RewardGrant
BigWinClearedQuestMissionIds []int32
IsBigWin bool
}
type QuestHandler struct {
*masterdata.QuestCatalog
Config *masterdata.GameConfig
Granter *store.PossessionGranter
}
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler {
granter := BuildGranter(catalog)
return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter}
}
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
for id, cm := range catalog.CostumeById {
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
}
weaponById := make(map[int32]store.WeaponRef, len(catalog.WeaponById))
for id, wm := range catalog.WeaponById {
weaponById[id] = store.WeaponRef{
WeaponSkillGroupId: wm.WeaponSkillGroupId,
WeaponAbilityGroupId: wm.WeaponAbilityGroupId,
WeaponStoryReleaseConditionGroupId: wm.WeaponStoryReleaseConditionGroupId,
}
}
releaseConditions := make(map[int32][]store.WeaponStoryReleaseCond, len(catalog.ReleaseConditionsByGroupId))
for groupId, rows := range catalog.ReleaseConditionsByGroupId {
conds := make([]store.WeaponStoryReleaseCond, len(rows))
for i, r := range rows {
conds[i] = store.WeaponStoryReleaseCond{
StoryIndex: r.StoryIndex,
WeaponStoryReleaseConditionType: r.WeaponStoryReleaseConditionType,
ConditionValue: r.ConditionValue,
}
}
releaseConditions[groupId] = conds
}
return &store.PossessionGranter{
CostumeById: costumeById,
WeaponById: weaponById,
WeaponSkillSlots: catalog.WeaponSkillSlots,
WeaponAbilitySlots: catalog.WeaponAbilitySlots,
ReleaseConditions: releaseConditions,
}
}
+242
View File
@@ -0,0 +1,242 @@
package questflow
import (
"fmt"
"log"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) initQuestState(user *store.UserState, questId int32) {
quest := user.Quests[questId]
quest.QuestId = questId
user.Quests[questId] = quest
for _, missionId := range h.MissionIdsByQuestId[questId] {
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
mission := user.QuestMissions[key]
mission.QuestId = questId
mission.QuestMissionId = missionId
user.QuestMissions[key] = mission
}
}
func isMainQuestPlayable(quest masterdata.QuestRow) bool {
return !quest.IsRunInTheBackground && quest.IsCountedAsQuest
}
func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32, nowMillis int64) {
for _, missionId := range h.MissionIdsByQuestId[questId] {
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
mission := user.QuestMissions[key]
mission.IsClear = true
mission.ProgressValue = 1
mission.LatestClearDatetime = nowMillis
user.QuestMissions[key] = mission
}
}
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, false, nowMillis)
}
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, true, nowMillis)
}
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
}
h.initQuestState(user, questId)
if quest.Stamina > 0 {
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
}
questState := user.Quests[questId]
if questState.QuestStateType == model.UserQuestStateTypeCleared {
if isReplayFlow {
user.MainQuest.SavedCurrentQuestSceneId = user.MainQuest.CurrentQuestSceneId
user.MainQuest.SavedHeadQuestSceneId = user.MainQuest.HeadQuestSceneId
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
user.MainQuest.LatestVersion = nowMillis
questState.QuestStateType = model.UserQuestStateTypeActive
questState.LatestStartDatetime = nowMillis
questState.IsBattleOnly = isBattleOnly
questState.UserDeckNumber = userDeckNumber
user.Quests[questId] = questState
log.Printf("[HandleQuestStart] replay flow started for quest %d, saved scene=%d head=%d",
questId, user.MainQuest.SavedCurrentQuestSceneId, user.MainQuest.SavedHeadQuestSceneId)
}
return
}
questState.IsBattleOnly = isBattleOnly
questState.UserDeckNumber = userDeckNumber
if isMainQuestPlayable(quest) {
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
questState.QuestStateType = model.UserQuestStateTypeActive
questState.LatestStartDatetime = nowMillis
} else {
questState.QuestStateType = model.UserQuestStateTypeCleared
questState.ClearCount = 1
questState.DailyClearCount = 1
questState.LastClearDatetime = nowMillis
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
firstSceneId := sceneIds[0]
prevSceneId := user.MainQuest.CurrentQuestSceneId
user.MainQuest.CurrentQuestSceneId = firstSceneId
if h.isSceneAhead(firstSceneId, user.MainQuest.HeadQuestSceneId) {
user.MainQuest.HeadQuestSceneId = firstSceneId
}
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
lastSceneId := h.getChapterLastSceneId(questId)
user.MainQuest.IsReachedLastQuestScene = firstSceneId == lastSceneId
if routeId, ok := h.RouteIdByQuestId[questId]; ok {
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
user.MainQuest.MainQuestSeasonId = seasonId
}
}
log.Printf("[HandleQuestStart] background quest %d auto-cleared, scene %d -> %d", questId, prevSceneId, firstSceneId)
}
}
user.Quests[questId] = questState
}
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome FinishOutcome, nowMillis int64) {
questState := user.Quests[questId]
if !questState.IsRewardGranted {
h.applyQuestRewards(user, questId, nowMillis)
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)
questState.IsRewardGranted = true
}
for _, drop := range outcome.DropRewards {
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
}
for _, reward := range outcome.ReplayFlowFirstClearRewards {
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
}
questState.QuestStateType = model.UserQuestStateTypeCleared
questState.ClearCount++
questState.DailyClearCount++
questState.LastClearDatetime = nowMillis
user.Quests[questId] = questState
}
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
quest, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
}
outcome := h.evaluateFinishOutcome(user, questId)
if !isRetired {
h.applyQuestVictory(user, questId, outcome, nowMillis)
}
if isRetired && !isAnnihilated && quest.Stamina > 1 {
refund := quest.Stamina - 1
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
}
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
user.MainQuest.ProgressQuestSceneId = 0
user.MainQuest.ProgressHeadQuestSceneId = 0
user.MainQuest.ProgressQuestFlowType = 0
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
if wasReplay {
if user.MainQuest.SavedCurrentQuestSceneId > 0 {
user.MainQuest.CurrentQuestSceneId = user.MainQuest.SavedCurrentQuestSceneId
}
if user.MainQuest.SavedHeadQuestSceneId > 0 {
user.MainQuest.HeadQuestSceneId = user.MainQuest.SavedHeadQuestSceneId
}
user.MainQuest.SavedCurrentQuestSceneId = 0
user.MainQuest.SavedHeadQuestSceneId = 0
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
log.Printf("[HandleQuestFinish] replay flow ended for quest %d, restored scene=%d head=%d",
questId, user.MainQuest.CurrentQuestSceneId, user.MainQuest.HeadQuestSceneId)
}
h.clearQuestMissions(user, questId, nowMillis)
return outcome
}
func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount int32, nowMillis int64) FinishOutcome {
questDef, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
}
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
store.ConsumeStamina(user, 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)
for _, drop := range drops {
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
}
allDrops = append(allDrops, drops...)
if questDef.Gold != 0 {
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
}
h.applyExpRewards(user, questId, nowMillis)
}
questState := user.Quests[questId]
questState.ClearCount += skipCount
questState.DailyClearCount += skipCount
questState.LastClearDatetime = nowMillis
user.Quests[questId] = questState
log.Printf("[HandleQuestSkip] questId=%d skipCount=%d drops=%d gold=%d", questId, skipCount, len(allDrops), questDef.Gold*skipCount)
return FinishOutcome{DropRewards: allDrops}
}
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
questDef, ok := h.QuestById[questId]
if ok && isMainQuestPlayable(questDef) {
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
}
quest := user.Quests[questId]
quest.QuestId = questId
quest.QuestStateType = model.UserQuestStateTypeActive
quest.IsBattleOnly = false
quest.LatestStartDatetime = nowMillis
user.Quests[questId] = quest
for _, missionId := range h.MissionIdsByQuestId[questId] {
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
m := user.QuestMissions[key]
m.QuestId = questId
m.QuestMissionId = missionId
m.IsClear = false
m.ProgressValue = 0
m.LatestClearDatetime = 0
user.QuestMissions[key] = m
}
}
+394
View File
@@ -0,0 +1,394 @@
package questflow
import (
"fmt"
"log"
"lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) isQuestCleared(user *store.UserState, questId int32) bool {
quest, ok := user.Quests[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for isQuestCleared", questId))
}
return quest.QuestStateType == model.UserQuestStateTypeCleared
}
func appendMissionRewards(dst []RewardGrant, src []masterdata.QuestMissionRewardRow) []RewardGrant {
for _, r := range src {
dst = append(dst, RewardGrant{
PossessionType: r.PossessionType,
PossessionId: r.PossessionId,
Count: r.Count,
})
}
return dst
}
func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef masterdata.QuestRow) int32 {
rewardGroupId := questDef.QuestFirstClearRewardGroupId
for _, switchRow := range h.FirstClearRewardSwitchesByQuestId[questDef.QuestId] {
if h.isQuestCleared(user, switchRow.SwitchConditionClearQuestId) {
rewardGroupId = switchRow.QuestFirstClearRewardGroupId
break
}
}
return rewardGroupId
}
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32) FinishOutcome {
outcome := FinishOutcome{}
questState, ok := user.Quests[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
}
questDef, ok := h.QuestById[questId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
}
if !questState.IsRewardGranted {
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
PossessionType: reward.PossessionType,
PossessionId: reward.PossessionId,
Count: reward.Count,
})
}
}
if user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow) && questDef.QuestReplayFlowRewardGroupId > 0 {
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
PossessionType: reward.PossessionType,
PossessionId: reward.PossessionId,
Count: reward.Count,
})
}
}
pendingClearCount := 0
regularMissionCount := 0
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
missionDef, ok := h.MissionById[questMissionId]
if !ok || missionDef.QuestMissionConditionType == model.QuestMissionConditionTypeComplete {
continue
}
regularMissionCount++
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
mission := user.QuestMissions[key]
if !mission.IsClear {
pendingClearCount++
outcome.MissionClearRewards = appendMissionRewards(
outcome.MissionClearRewards,
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
)
}
}
priorClearCount := regularMissionCount - pendingClearCount
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
// always equals regularMissionCount. The two-variable form is kept to mirror the
// original game's intent where individual missions could fail their conditions.
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
if allRegularWillClear {
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
missionDef, ok := h.MissionById[questMissionId]
if !ok || missionDef.QuestMissionConditionType != model.QuestMissionConditionTypeComplete {
continue
}
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
if !user.QuestMissions[key].IsClear {
outcome.MissionClearCompleteRewards = appendMissionRewards(
outcome.MissionClearCompleteRewards,
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
)
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
}
}
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
}
outcome.DropRewards = h.computeDropRewards(questDef)
return outcome
}
func (h *QuestHandler) computeDropRewards(questDef masterdata.QuestRow) []RewardGrant {
if questDef.QuestPickupRewardGroupId == 0 {
return nil
}
var drops []RewardGrant
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
drops = append(drops, RewardGrant{
PossessionType: bdr.PossessionType,
PossessionId: bdr.PossessionId,
Count: bdr.Count,
})
}
}
return drops
}
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
questDef, ok := h.QuestById[questId]
if !ok {
return
}
oldLevel := user.Status.Level
user.Status.Exp += questDef.UserExp
user.Status.Level, user.Status.Exp = gameutil.LevelAndCap(user.Status.Exp, h.UserExpThresholds)
log.Printf("[applyExpRewards] questId=%d user: +%d exp -> total=%d level=%d", questId, questDef.UserExp, user.Status.Exp, user.Status.Level)
if user.Status.Level > oldLevel {
if maxStamina, ok := h.MaxStaminaByLevel[user.Status.Level]; ok {
store.ReplenishStamina(user, maxStamina*1000, nowMillis)
}
}
if h.RentalQuestIds[questId] {
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (rental deck)", questId)
return
}
deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId)
if deckCostumeUuids == nil {
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId)
return
}
if questDef.CharacterExp != 0 {
for id := range deckCharacterIds {
row := user.Characters[id]
row.Exp += questDef.CharacterExp
row.Level, row.Exp = gameutil.LevelAndCap(row.Exp, h.CharacterExpThresholds)
user.Characters[id] = row
log.Printf("[applyExpRewards] questId=%d character=%d: +%d exp -> total=%d level=%d", questId, id, questDef.CharacterExp, row.Exp, row.Level)
}
}
if questDef.CostumeExp != 0 {
for key := range deckCostumeUuids {
row := user.Costumes[key]
cm, ok := h.CostumeById[row.CostumeId]
if !ok {
continue
}
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
if row.Level >= maxLevel {
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): at max level %d, skipping", questId, row.CostumeId, key, row.Level)
continue
}
}
row.Exp += questDef.CostumeExp
if thresholds, ok := h.CostumeExpByRarity[cm.RarityType]; ok {
row.Level, row.Exp = gameutil.LevelAndCap(row.Exp, thresholds)
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
if row.Level > maxLevel && int(maxLevel) < len(thresholds) {
row.Level = maxLevel
row.Exp = thresholds[maxLevel]
}
}
}
user.Costumes[key] = row
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): +%d exp -> total=%d level=%d", questId, row.CostumeId, key, questDef.CostumeExp, row.Exp, row.Level)
}
}
}
func (h *QuestHandler) resolveDeckUnits(user *store.UserState, questId int32) (costumeUuids map[string]bool, characterIds map[int32]bool) {
dn := user.Quests[questId].UserDeckNumber
if dn == 0 {
return nil, nil
}
deck, ok := user.Decks[store.DeckKey{DeckType: model.DeckTypeQuest, UserDeckNumber: dn}]
if !ok {
return nil, nil
}
costumeUuids = make(map[string]bool)
characterIds = make(map[int32]bool)
for _, dcUuid := range []string{deck.UserDeckCharacterUuid01, deck.UserDeckCharacterUuid02, deck.UserDeckCharacterUuid03} {
if dcUuid == "" {
continue
}
dc, ok := user.DeckCharacters[dcUuid]
if !ok || dc.UserCostumeUuid == "" {
continue
}
costumeUuids[dc.UserCostumeUuid] = true
if costume, ok := user.Costumes[dc.UserCostumeUuid]; ok {
if cm, ok := h.CostumeById[costume.CostumeId]; ok {
characterIds[cm.CharacterId] = true
}
}
}
if len(costumeUuids) == 0 {
return nil, nil
}
return costumeUuids, characterIds
}
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
questDef, ok := h.QuestById[questId]
if !ok {
return
}
h.applyExpRewards(user, questId, nowMillis)
if questDef.Gold != 0 {
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
}
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
}
}
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
switch possType {
case model.PossessionTypeCompanion:
h.grantCompanion(user, possId, nowMillis)
case model.PossessionTypeParts:
h.grantParts(user, possId, nowMillis)
default:
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
}
}
func (h *QuestHandler) grantCompanion(user *store.UserState, companionId int32, nowMillis int64) {
for _, row := range user.Companions {
if row.CompanionId == companionId {
return
}
}
key := fmt.Sprintf("reward-companion-%d", companionId)
user.Companions[key] = store.CompanionState{
UserCompanionUuid: key,
CompanionId: companionId,
Level: 1,
HeadupDisplayViewId: 1,
AcquisitionDatetime: nowMillis,
}
}
func (h *QuestHandler) grantParts(user *store.UserState, partsId int32, nowMillis int64) {
for _, row := range user.Parts {
if row.PartsId == partsId {
return
}
}
var mainStatId int32
if partsDef, ok := h.PartsById[partsId]; ok {
mainStatId = h.DefaultPartsStatusMainByLotteryGroup[partsDef.PartsStatusMainLotteryGroupId]
if _, exists := user.PartsGroupNotes[partsDef.PartsGroupId]; !exists {
user.PartsGroupNotes[partsDef.PartsGroupId] = store.PartsGroupNoteState{
PartsGroupId: partsDef.PartsGroupId,
FirstAcquisitionDatetime: nowMillis,
LatestVersion: nowMillis,
}
}
}
key := fmt.Sprintf("reward-parts-%d", partsId)
user.Parts[key] = store.PartsState{
UserPartsUuid: key,
PartsId: partsId,
Level: 1,
PartsStatusMainId: mainStatId,
AcquisitionDatetime: nowMillis,
}
}
func (h *QuestHandler) grantWeaponStoryUnlock(user *store.UserState, weaponId, storyIndex int32, nowMillis int64) {
store.GrantWeaponStoryUnlock(user, weaponId, storyIndex, nowMillis)
}
var tutorialCompanionChoices = map[int32]int32{
1: 2, // bear + fire (Cat=1, Attr=2)
2: 1, // bear + wind (Cat=1, Attr=6)
3: 7, // doll + fire (Cat=3, Attr=2)
4: 10, // doll + wind (Cat=3, Attr=6)
}
func (h *QuestHandler) ApplyTutorialReward(user *store.UserState, tutorialType model.TutorialType, choiceId int32, nowMillis int64) []RewardGrant {
switch tutorialType {
case model.TutorialTypeCompanion:
return h.applyCompanionTutorialReward(user, choiceId, nowMillis)
default:
return nil
}
}
func (h *QuestHandler) applyCompanionTutorialReward(user *store.UserState, choiceId int32, nowMillis int64) []RewardGrant {
companionId, ok := tutorialCompanionChoices[choiceId]
if !ok {
log.Printf("[QuestHandler] unknown companion tutorial choiceId=%d", choiceId)
return nil
}
h.grantCompanion(user, companionId, nowMillis)
return []RewardGrant{{
PossessionType: model.PossessionTypeCompanion,
PossessionId: companionId,
Count: 1,
}}
}
func (h *QuestHandler) BattleDropRewards(questId int32) []masterdata.BattleDropInfo {
return h.BattleDropsByQuestId[questId]
}
func (h *QuestHandler) grantWeaponStoryUnlocksForQuestScene(user *store.UserState, questId int32, resultType model.QuestResultType, nowMillis int64) {
if resultType == model.QuestResultTypeHalfResult {
questDef, ok := h.QuestById[questId]
if !ok {
return
}
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
if reward.PossessionType != model.PossessionTypeWeapon {
continue
}
weaponId := reward.PossessionId
weapon, ok := h.WeaponById[weaponId]
if !ok || weapon.WeaponStoryReleaseConditionGroupId == 0 {
continue
}
groupId := weapon.WeaponStoryReleaseConditionGroupId
for _, cond := range h.ReleaseConditionsByGroupId[groupId] {
if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeAcquisition && cond.ConditionValue == 0 {
h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
}
}
}
return
}
if resultType == model.QuestResultTypeFullResult {
for groupId, conditions := range h.ReleaseConditionsByGroupId {
for _, cond := range conditions {
if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeQuestClear && cond.ConditionValue == questId {
for _, weaponId := range h.WeaponIdsByReleaseConditionGroupId[groupId] {
h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
}
break
}
}
}
}
}
+145
View File
@@ -0,0 +1,145 @@
package questflow
import (
"fmt"
"log"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (h *QuestHandler) applySceneGrants(user *store.UserState, questSceneId int32, nowMillis int64) {
grants, ok := h.SceneGrantsBySceneId[questSceneId]
if !ok {
return
}
for _, g := range grants {
h.applyRewardPossession(user, g.PossessionType, g.PossessionId, g.Count, nowMillis)
}
}
func (h *QuestHandler) isSceneAhead(newSceneId, currentHeadId int32) bool {
if currentHeadId == 0 {
return true
}
return h.SceneById[newSceneId].SortOrder > h.SceneById[currentHeadId].SortOrder
}
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
scene, ok := h.SceneById[questSceneId]
if !ok {
panic(fmt.Sprintf("unknown sceneId=%d for HandleMainFlowSceneProgress", questSceneId))
}
quest, ok := h.QuestById[scene.QuestId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleMainFlowSceneProgress", questSceneId))
}
user.MainQuest.CurrentQuestSceneId = questSceneId
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
user.MainQuest.HeadQuestSceneId = questSceneId
}
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
LatestVersion: nowMillis,
}
}
lastSceneId := h.getChapterLastSceneId(scene.QuestId)
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
routeId, ok := h.RouteIdByQuestId[quest.QuestId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleMainFlowSceneProgress setting currentMainQuestRouteId", quest.QuestId))
}
user.MainQuest.CurrentMainQuestRouteId = routeId
user.PortalCageStatus.IsCurrentProgress = false
user.PortalCageStatus.LatestVersion = nowMillis
h.applySceneGrants(user, questSceneId, nowMillis)
}
func (h *QuestHandler) advanceTutorialsForScene(user *store.UserState, sceneId int32) {
currentScene, ok := h.SceneById[sceneId]
if !ok {
log.Printf("[advanceTutorialsForScene] unknown sceneId=%d", sceneId)
return
}
for _, cond := range h.TutorialUnlockConditions {
condScene, ok := h.SceneById[cond.ConditionValue]
if !ok {
log.Printf("[advanceTutorialsForScene] unknown conditionValue=%d", cond.ConditionValue)
continue
}
if currentScene.SortOrder >= condScene.SortOrder {
if _, exists := user.Tutorials[cond.TutorialType]; !exists {
user.Tutorials[cond.TutorialType] = store.TutorialProgressState{
TutorialType: cond.TutorialType,
ProgressPhase: 99999,
}
}
}
}
}
func (h *QuestHandler) getLastMainFlowSceneId(questId int32) int32 {
sceneIds := h.SceneIdsByQuestId[questId]
if len(sceneIds) == 0 {
panic(fmt.Sprintf("no scenes found for questId=%d", questId))
}
return sceneIds[len(sceneIds)-1]
}
func (h *QuestHandler) getChapterLastSceneId(questId int32) int32 {
if id, ok := h.ChapterLastSceneByQuestId[questId]; ok {
return id
}
return h.getLastMainFlowSceneId(questId)
}
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
if user.MainQuest.ReplayFlowHeadQuestSceneId == 0 || h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
}
user.MainQuest.LatestVersion = nowMillis
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d replayHead=%d", questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId)
}
func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, questSceneId int32) {
scene, ok := h.SceneById[questSceneId]
if !ok {
panic(fmt.Sprintf("unknown sceneId=%d for HandleMainQuestSceneProgress", questSceneId))
}
quest, ok := h.QuestById[scene.QuestId]
if !ok {
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
}
if isMainQuestPlayable(quest) {
if scene.QuestResultType == model.QuestResultTypeHalfResult {
nowMillis := gametime.NowMillis()
h.clearQuestMissions(user, quest.QuestId, nowMillis)
}
user.MainQuest.ProgressQuestSceneId = questSceneId
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
}
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
} else {
user.MainQuest.CurrentQuestSceneId = questSceneId
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
user.MainQuest.HeadQuestSceneId = questSceneId
}
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
}
}