mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
403 lines
14 KiB
Go
403 lines
14 KiB
Go
package questflow
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"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.EntityMQuestMissionReward) []RewardGrant {
|
|
for _, r := range src {
|
|
dst = append(dst, RewardGrant{
|
|
PossessionType: model.PossessionType(r.PossessionType),
|
|
PossessionId: r.PossessionId,
|
|
Count: r.Count,
|
|
})
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef masterdata.EntityMQuest) 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: model.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: model.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 || model.QuestMissionConditionType(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 || model.QuestMissionConditionType(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.EntityMQuest) []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: model.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, model.PossessionType(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 := uuid.New().String()
|
|
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 := uuid.New().String()
|
|
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) bool {
|
|
return 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) []int32 {
|
|
var changedIds []int32
|
|
if resultType == model.QuestResultTypeHalfResult {
|
|
questDef, ok := h.QuestById[questId]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
|
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
|
if model.PossessionType(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 model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) == model.WeaponStoryReleaseConditionTypeAcquisition && cond.ConditionValue == 0 {
|
|
if h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) {
|
|
changedIds = append(changedIds, weaponId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return changedIds
|
|
}
|
|
if resultType == model.QuestResultTypeFullResult {
|
|
for groupId, conditions := range h.ReleaseConditionsByGroupId {
|
|
for _, cond := range conditions {
|
|
if model.WeaponStoryReleaseConditionType(cond.WeaponStoryReleaseConditionType) == model.WeaponStoryReleaseConditionTypeQuestClear && cond.ConditionValue == questId {
|
|
for _, weaponId := range h.WeaponIdsByReleaseConditionGroupId[groupId] {
|
|
if h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis) {
|
|
changedIds = append(changedIds, weaponId)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return changedIds
|
|
}
|