mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Compare commits
4 Commits
2d0c0d8ef0
..
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 63df7d7055 | |||
| c961fde8ac | |||
| 72b2bd1ec5 | |||
| dc7c1df4fd |
@@ -0,0 +1,170 @@
|
||||
package campaign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type Catalog struct {
|
||||
enhance []enhanceRow
|
||||
quest []questRow
|
||||
}
|
||||
|
||||
type enhanceRow struct {
|
||||
effectType EnhanceCampaignEffectType
|
||||
effectValue int32
|
||||
targets []enhanceMatch
|
||||
startMillis int64
|
||||
endMillis int64
|
||||
userStatus TargetUserStatusType
|
||||
}
|
||||
|
||||
type enhanceMatch struct {
|
||||
t EnhanceCampaignTargetType
|
||||
v int32
|
||||
}
|
||||
|
||||
type questRow struct {
|
||||
effectType QuestCampaignEffectType
|
||||
effectValue int32
|
||||
bonusItems []BonusDrop
|
||||
targets []questMatch
|
||||
startMillis int64
|
||||
endMillis int64
|
||||
userStatus TargetUserStatusType
|
||||
}
|
||||
|
||||
type questMatch struct {
|
||||
t QuestCampaignTargetType
|
||||
v int32
|
||||
}
|
||||
|
||||
func Load() (*Catalog, error) {
|
||||
enhance, err := loadEnhanceRows()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load enhance campaigns: %w", err)
|
||||
}
|
||||
quest, err := loadQuestRows()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest campaigns: %w", err)
|
||||
}
|
||||
return &Catalog{enhance: enhance, quest: quest}, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) EnhanceCount() int { return len(c.enhance) }
|
||||
func (c *Catalog) QuestCount() int { return len(c.quest) }
|
||||
|
||||
func loadEnhanceRows() ([]enhanceRow, error) {
|
||||
campaigns, err := utils.ReadTable[masterdata.EntityMEnhanceCampaign]("m_enhance_campaign")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets, err := utils.ReadTable[masterdata.EntityMEnhanceCampaignTargetGroup]("m_enhance_campaign_target_group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
byGroup := make(map[int32][]enhanceMatch, len(targets))
|
||||
for _, t := range targets {
|
||||
byGroup[t.EnhanceCampaignTargetGroupId] = append(byGroup[t.EnhanceCampaignTargetGroupId], enhanceMatch{
|
||||
t: EnhanceCampaignTargetType(t.EnhanceCampaignTargetType),
|
||||
v: t.EnhanceCampaignTargetValue,
|
||||
})
|
||||
}
|
||||
|
||||
rows := make([]enhanceRow, 0, len(campaigns))
|
||||
for _, c := range campaigns {
|
||||
grp := byGroup[c.EnhanceCampaignTargetGroupId]
|
||||
if len(grp) == 0 {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, enhanceRow{
|
||||
effectType: EnhanceCampaignEffectType(c.EnhanceCampaignEffectType),
|
||||
effectValue: c.EnhanceCampaignEffectValue / 10,
|
||||
targets: grp,
|
||||
startMillis: c.StartDatetime,
|
||||
endMillis: c.EndDatetime,
|
||||
userStatus: TargetUserStatusType(c.TargetUserStatusType),
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func loadQuestRows() ([]questRow, error) {
|
||||
campaigns, err := utils.ReadTable[masterdata.EntityMQuestCampaign]("m_quest_campaign")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets, err := utils.ReadTable[masterdata.EntityMQuestCampaignTargetGroup]("m_quest_campaign_target_group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
effects, err := utils.ReadTable[masterdata.EntityMQuestCampaignEffectGroup]("m_quest_campaign_effect_group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemGroups, err := utils.ReadTable[masterdata.EntityMQuestCampaignTargetItemGroup]("m_quest_campaign_target_item_group")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetsByGroup := make(map[int32][]questMatch, len(targets))
|
||||
for _, t := range targets {
|
||||
targetsByGroup[t.QuestCampaignTargetGroupId] = append(targetsByGroup[t.QuestCampaignTargetGroupId], questMatch{
|
||||
t: QuestCampaignTargetType(t.QuestCampaignTargetType),
|
||||
v: t.QuestCampaignTargetValue,
|
||||
})
|
||||
}
|
||||
|
||||
bonusByGroup := make(map[int32][]BonusDrop, len(itemGroups))
|
||||
for _, ig := range itemGroups {
|
||||
bonusByGroup[ig.QuestCampaignTargetItemGroupId] = append(bonusByGroup[ig.QuestCampaignTargetItemGroupId], BonusDrop{
|
||||
PossessionType: ig.PossessionType,
|
||||
PossessionId: ig.PossessionId,
|
||||
Count: ig.Count,
|
||||
})
|
||||
}
|
||||
|
||||
effectByGroup := make(map[int32]masterdata.EntityMQuestCampaignEffectGroup, len(effects))
|
||||
for _, e := range effects {
|
||||
effectByGroup[e.QuestCampaignEffectGroupId] = e
|
||||
}
|
||||
|
||||
rows := make([]questRow, 0, len(campaigns))
|
||||
for _, c := range campaigns {
|
||||
grp := targetsByGroup[c.QuestCampaignTargetGroupId]
|
||||
if len(grp) == 0 {
|
||||
continue
|
||||
}
|
||||
eff, ok := effectByGroup[c.QuestCampaignEffectGroupId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, questRow{
|
||||
effectType: QuestCampaignEffectType(eff.QuestCampaignEffectType),
|
||||
effectValue: eff.QuestCampaignEffectValue,
|
||||
bonusItems: bonusByGroup[eff.QuestCampaignTargetItemGroupId],
|
||||
targets: grp,
|
||||
startMillis: c.StartDatetime,
|
||||
endMillis: c.EndDatetime,
|
||||
userStatus: TargetUserStatusType(c.TargetUserStatusType),
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r enhanceRow) isActive(f Filter) bool {
|
||||
if f.NowMillis < r.startMillis || f.NowMillis > r.endMillis {
|
||||
return false
|
||||
}
|
||||
return r.userStatus == TargetUserStatusAll || r.userStatus == f.UserStatus
|
||||
}
|
||||
|
||||
func (r questRow) isActive(f Filter) bool {
|
||||
if f.NowMillis < r.startMillis || f.NowMillis > r.endMillis {
|
||||
return false
|
||||
}
|
||||
return r.userStatus == TargetUserStatusAll || r.userStatus == f.UserStatus
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package campaign
|
||||
|
||||
func (c *Catalog) PartsRateBonus(t PartsTarget, f Filter) RateBonus {
|
||||
var out RateBonus
|
||||
for _, r := range c.enhance {
|
||||
if !r.isActive(f) {
|
||||
continue
|
||||
}
|
||||
if !matchesParts(r.targets, t) {
|
||||
continue
|
||||
}
|
||||
out = applyEnhanceEffect(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Catalog) CostumeExpBonus(t CostumeTarget, f Filter) ExpBonus {
|
||||
var sum int32
|
||||
for _, r := range c.enhance {
|
||||
if !r.isActive(f) || r.effectType != EnhanceEffectAdditionalPerm {
|
||||
continue
|
||||
}
|
||||
if matchesCostume(r.targets, t) {
|
||||
sum += r.effectValue
|
||||
}
|
||||
}
|
||||
return ExpBonus{bonusPermil: sum}
|
||||
}
|
||||
|
||||
func (c *Catalog) WeaponExpBonus(t WeaponTarget, f Filter) ExpBonus {
|
||||
var sum int32
|
||||
for _, r := range c.enhance {
|
||||
if !r.isActive(f) || r.effectType != EnhanceEffectAdditionalPerm {
|
||||
continue
|
||||
}
|
||||
if matchesWeapon(r.targets, t) {
|
||||
sum += r.effectValue
|
||||
}
|
||||
}
|
||||
return ExpBonus{bonusPermil: sum}
|
||||
}
|
||||
|
||||
func applyEnhanceEffect(b RateBonus, r enhanceRow) RateBonus {
|
||||
switch r.effectType {
|
||||
case EnhanceEffectProbability:
|
||||
b.override = r.effectValue
|
||||
case EnhanceEffectAdditionalPerm:
|
||||
b.bonusPermil += r.effectValue
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func matchesParts(targets []enhanceMatch, t PartsTarget) bool {
|
||||
for _, m := range targets {
|
||||
switch m.t {
|
||||
case EnhanceTargetPartsAll:
|
||||
return true
|
||||
case EnhanceTargetPartsSeriesId:
|
||||
if m.v == t.PartsGroupId {
|
||||
return true
|
||||
}
|
||||
case EnhanceTargetPartsId:
|
||||
if m.v == t.PartsId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesCostume(targets []enhanceMatch, t CostumeTarget) bool {
|
||||
for _, m := range targets {
|
||||
switch m.t {
|
||||
case EnhanceTargetCostumeAll:
|
||||
return true
|
||||
case EnhanceTargetCostumeCharacterId:
|
||||
if m.v == t.CharacterId {
|
||||
return true
|
||||
}
|
||||
case EnhanceTargetCostumeSkillfulWeapon:
|
||||
if m.v == t.SkillfulWeaponType {
|
||||
return true
|
||||
}
|
||||
case EnhanceTargetCostumeId:
|
||||
if m.v == t.CostumeId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesWeapon(targets []enhanceMatch, t WeaponTarget) bool {
|
||||
for _, m := range targets {
|
||||
switch m.t {
|
||||
case EnhanceTargetWeaponAll:
|
||||
return true
|
||||
case EnhanceTargetWeaponTypeId:
|
||||
if m.v == t.WeaponType {
|
||||
return true
|
||||
}
|
||||
case EnhanceTargetWeaponAttributeTypeId:
|
||||
if m.v == t.AttributeType {
|
||||
return true
|
||||
}
|
||||
case EnhanceTargetWeaponId:
|
||||
if m.v == t.WeaponId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package campaign
|
||||
|
||||
type RateBonus struct {
|
||||
override int32
|
||||
bonusPermil int32
|
||||
}
|
||||
|
||||
func (b RateBonus) Apply(basePermil int32) int32 {
|
||||
base := basePermil
|
||||
if b.override > 0 {
|
||||
base = b.override
|
||||
}
|
||||
return clampPermil(int32(int64(base) + int64(b.bonusPermil)))
|
||||
}
|
||||
|
||||
type ExpBonus struct {
|
||||
bonusPermil int32
|
||||
}
|
||||
|
||||
func (b ExpBonus) Apply(base int32) int32 {
|
||||
return int32(int64(base) * int64(1000+b.bonusPermil) / 1000)
|
||||
}
|
||||
|
||||
type StaminaMul struct {
|
||||
permil int32
|
||||
}
|
||||
|
||||
func (m StaminaMul) Apply(base int32) int32 {
|
||||
if m.permil == 1000 {
|
||||
return base
|
||||
}
|
||||
return int32(int64(base) * int64(m.permil) / 1000)
|
||||
}
|
||||
|
||||
type DropRateMul struct {
|
||||
bonusPermil int32
|
||||
}
|
||||
|
||||
func (m DropRateMul) Apply(base int32) int32 {
|
||||
return int32((int64(base)*int64(1000+m.bonusPermil) + 999) / 1000)
|
||||
}
|
||||
|
||||
type BonusDrop struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
func clampPermil(v int32) int32 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 1000 {
|
||||
return 1000
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package campaign
|
||||
|
||||
func (c *Catalog) QuestStamina(t QuestTarget, f Filter) StaminaMul {
|
||||
return questPermilMin(c.quest, QuestEffectStaminaConsume, t, f)
|
||||
}
|
||||
|
||||
func (c *Catalog) QuestDropRate(t QuestTarget, f Filter) DropRateMul {
|
||||
var best int32
|
||||
for _, r := range c.quest {
|
||||
if !r.isActive(f) || r.effectType != QuestEffectDropRate {
|
||||
continue
|
||||
}
|
||||
if !matchesQuest(r.targets, t) {
|
||||
continue
|
||||
}
|
||||
if r.effectValue > best {
|
||||
best = r.effectValue
|
||||
}
|
||||
}
|
||||
return DropRateMul{bonusPermil: best}
|
||||
}
|
||||
|
||||
func (c *Catalog) QuestBonusDrops(t QuestTarget, f Filter) []BonusDrop {
|
||||
var out []BonusDrop
|
||||
for _, r := range c.quest {
|
||||
if !r.isActive(f) || r.effectType != QuestEffectDropItemAdd {
|
||||
continue
|
||||
}
|
||||
if !matchesQuest(r.targets, t) {
|
||||
continue
|
||||
}
|
||||
out = append(out, r.bonusItems...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func questPermilMin(rows []questRow, want QuestCampaignEffectType, t QuestTarget, f Filter) StaminaMul {
|
||||
min := int32(1000)
|
||||
for _, r := range rows {
|
||||
if !r.isActive(f) || r.effectType != want {
|
||||
continue
|
||||
}
|
||||
if !matchesQuest(r.targets, t) {
|
||||
continue
|
||||
}
|
||||
if r.effectValue < min {
|
||||
min = r.effectValue
|
||||
}
|
||||
}
|
||||
return StaminaMul{permil: min}
|
||||
}
|
||||
|
||||
func matchesQuest(targets []questMatch, t QuestTarget) bool {
|
||||
for _, m := range targets {
|
||||
switch m.t {
|
||||
case QuestTargetWholeQuest:
|
||||
return true
|
||||
case QuestTargetQuestType:
|
||||
if int32(t.QuestType) == m.v {
|
||||
return true
|
||||
}
|
||||
case QuestTargetEventQuestType:
|
||||
if t.QuestType == QuestTypeEventQuest && t.EventQuestType == m.v {
|
||||
return true
|
||||
}
|
||||
case QuestTargetMainQuestChapterId:
|
||||
if t.QuestType == QuestTypeMainQuest && t.ChapterId == m.v {
|
||||
return true
|
||||
}
|
||||
case QuestTargetMainQuestQuestId:
|
||||
if t.QuestType == QuestTypeMainQuest && t.QuestId == m.v {
|
||||
return true
|
||||
}
|
||||
case QuestTargetSubQuestChapterId:
|
||||
if t.QuestType == QuestTypeEventQuest && t.ChapterId == m.v {
|
||||
return true
|
||||
}
|
||||
case QuestTargetSubQuestQuestId:
|
||||
if t.QuestType == QuestTypeEventQuest && t.QuestId == m.v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package campaign
|
||||
|
||||
import "lunar-tear/server/internal/model"
|
||||
|
||||
type EnhanceCampaignEffectType int32
|
||||
|
||||
const (
|
||||
EnhanceEffectUnknown EnhanceCampaignEffectType = 0
|
||||
EnhanceEffectProbability EnhanceCampaignEffectType = 1
|
||||
EnhanceEffectAdditionalPerm EnhanceCampaignEffectType = 2
|
||||
)
|
||||
|
||||
type EnhanceCampaignTargetType int32
|
||||
|
||||
const (
|
||||
EnhanceTargetUnknown EnhanceCampaignTargetType = 0
|
||||
EnhanceTargetCostumeAll EnhanceCampaignTargetType = 1
|
||||
EnhanceTargetWeaponAll EnhanceCampaignTargetType = 2
|
||||
EnhanceTargetPartsAll EnhanceCampaignTargetType = 3
|
||||
EnhanceTargetCostumeCharacterId EnhanceCampaignTargetType = 11
|
||||
EnhanceTargetCostumeSkillfulWeapon EnhanceCampaignTargetType = 12
|
||||
EnhanceTargetCostumeId EnhanceCampaignTargetType = 13
|
||||
EnhanceTargetWeaponTypeId EnhanceCampaignTargetType = 21
|
||||
EnhanceTargetWeaponAttributeTypeId EnhanceCampaignTargetType = 22
|
||||
EnhanceTargetWeaponId EnhanceCampaignTargetType = 23
|
||||
EnhanceTargetPartsSeriesId EnhanceCampaignTargetType = 31
|
||||
EnhanceTargetPartsId EnhanceCampaignTargetType = 32
|
||||
)
|
||||
|
||||
type QuestCampaignEffectType int32
|
||||
|
||||
const (
|
||||
QuestEffectUnknown QuestCampaignEffectType = 0
|
||||
QuestEffectDropRate QuestCampaignEffectType = 1
|
||||
QuestEffectDropCount QuestCampaignEffectType = 2
|
||||
QuestEffectStaminaConsume QuestCampaignEffectType = 3
|
||||
QuestEffectClearRewardGold QuestCampaignEffectType = 4
|
||||
QuestEffectDropItemAdd QuestCampaignEffectType = 5
|
||||
)
|
||||
|
||||
type QuestCampaignTargetType int32
|
||||
|
||||
const (
|
||||
QuestTargetUnknown QuestCampaignTargetType = 0
|
||||
QuestTargetWholeQuest QuestCampaignTargetType = 1
|
||||
QuestTargetQuestType QuestCampaignTargetType = 2
|
||||
QuestTargetEventQuestType QuestCampaignTargetType = 3
|
||||
QuestTargetMainQuestChapterId QuestCampaignTargetType = 4
|
||||
QuestTargetMainQuestQuestId QuestCampaignTargetType = 5
|
||||
QuestTargetSubQuestChapterId QuestCampaignTargetType = 6
|
||||
QuestTargetSubQuestQuestId QuestCampaignTargetType = 7
|
||||
)
|
||||
|
||||
type QuestType int32
|
||||
|
||||
const (
|
||||
QuestTypeUnknown QuestType = 0
|
||||
QuestTypeMainQuest QuestType = 1
|
||||
QuestTypeEventQuest QuestType = 2
|
||||
QuestTypeExtraQuest QuestType = 3
|
||||
QuestTypeBigHunt QuestType = 4
|
||||
)
|
||||
|
||||
type TargetUserStatusType int32
|
||||
|
||||
const (
|
||||
TargetUserStatusUnknown TargetUserStatusType = 0
|
||||
TargetUserStatusAll TargetUserStatusType = 1
|
||||
TargetUserStatusComeback TargetUserStatusType = 2
|
||||
TargetUserStatusBeginner TargetUserStatusType = 3
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
NowMillis int64
|
||||
UserStatus TargetUserStatusType
|
||||
}
|
||||
|
||||
type PartsTarget struct {
|
||||
PartsId int32
|
||||
PartsGroupId int32
|
||||
Rarity model.RarityType
|
||||
}
|
||||
|
||||
type CostumeTarget struct {
|
||||
CostumeId int32
|
||||
CharacterId int32
|
||||
SkillfulWeaponType int32
|
||||
}
|
||||
|
||||
type WeaponTarget struct {
|
||||
WeaponId int32
|
||||
WeaponType int32
|
||||
AttributeType int32
|
||||
}
|
||||
|
||||
type QuestTarget struct {
|
||||
QuestId int32
|
||||
QuestType QuestType
|
||||
EventQuestType int32
|
||||
ChapterId int32
|
||||
}
|
||||
@@ -17,3 +17,16 @@ func LevelAndCap(exp int32, thresholds []int32) (level, capped int32) {
|
||||
}
|
||||
return level, exp
|
||||
}
|
||||
|
||||
// ApplyExpWithMaxLevel runs LevelAndCap and then clamps the resulting
|
||||
// level to the per-instance maxLevel (e.g. limit break + awaken for
|
||||
// weapons, limit break + rebirth for costumes). A maxLevel <= 0 means
|
||||
// "no per-instance cap" and the result is identical to LevelAndCap.
|
||||
func ApplyExpWithMaxLevel(exp int32, thresholds []int32, maxLevel int32) (level, capped int32) {
|
||||
level, capped = LevelAndCap(exp, thresholds)
|
||||
if maxLevel > 0 && level > maxLevel && int(maxLevel) < len(thresholds) {
|
||||
level = maxLevel
|
||||
capped = thresholds[maxLevel]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,25 @@ type CharacterRebirthCatalog struct {
|
||||
MaterialsByGroupId map[int32][]EntityMCharacterRebirthMaterialGroup
|
||||
}
|
||||
|
||||
func (c *CharacterRebirthCatalog) CostumeLevelLimitUp(characterId, rebirthCount int32) int32 {
|
||||
if c == nil || rebirthCount <= 0 {
|
||||
return 0
|
||||
}
|
||||
stepGroupId, ok := c.StepGroupByCharacterId[characterId]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
var total int32
|
||||
for i := range rebirthCount {
|
||||
step, ok := c.StepByGroupAndCount[StepKey{GroupId: stepGroupId, BeforeRebirthCount: i}]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
total += step.CostumeLevelLimitUp
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) {
|
||||
rebirthRows, err := utils.ReadTable[EntityMCharacterRebirth]("m_character_rebirth")
|
||||
if err != nil {
|
||||
|
||||
@@ -158,5 +158,9 @@ func buildPartsStatusMain() (map[int32]PartsStatusMainDef, map[int32][]int32) {
|
||||
id++
|
||||
}
|
||||
}
|
||||
// Newer parts groups (PartsGroupId 401-490) use PartsStatusSubLotteryGroupId
|
||||
// 11/12 for rarities 10/20 instead of 1/2. Same stat pools — alias them.
|
||||
pool[11] = pool[1]
|
||||
pool[12] = pool[2]
|
||||
return defs, pool
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ type QuestCatalog struct {
|
||||
RoutesBySeason map[int32][]int32
|
||||
RouteCompletionQuestId map[int32]int32
|
||||
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||
MainQuestChapterIdByQuestId map[int32]int32
|
||||
EventQuestTypeByChapterId map[int32]int32
|
||||
|
||||
UserExpThresholds []int32
|
||||
CharacterExpThresholds []int32
|
||||
@@ -382,12 +384,23 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
||||
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
||||
}
|
||||
routeIdByQuestId := make(map[int32]int32)
|
||||
mainQuestChapterIdByQuestId := make(map[int32]int32)
|
||||
for _, sequence := range sequences {
|
||||
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
||||
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
|
||||
mainQuestChapterIdByQuestId[sequence.QuestId] = chapter.MainQuestChapterId
|
||||
}
|
||||
}
|
||||
|
||||
eventChapters, err := utils.ReadTable[EntityMEventQuestChapter]("m_event_quest_chapter")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load event quest chapter table: %w", err)
|
||||
}
|
||||
eventQuestTypeByChapterId := make(map[int32]int32, len(eventChapters))
|
||||
for _, ec := range eventChapters {
|
||||
eventQuestTypeByChapterId[ec.EventQuestChapterId] = ec.EventQuestType
|
||||
}
|
||||
|
||||
sortedChapters := make([]EntityMMainQuestChapter, len(chapters))
|
||||
copy(sortedChapters, chapters)
|
||||
sort.Slice(sortedChapters, func(i, j int) bool {
|
||||
@@ -589,6 +602,8 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
||||
RoutesBySeason: routesBySeason,
|
||||
RouteCompletionQuestId: routeCompletionQuestId,
|
||||
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||
MainQuestChapterIdByQuestId: mainQuestChapterIdByQuestId,
|
||||
EventQuestTypeByChapterId: eventQuestTypeByChapterId,
|
||||
|
||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||
|
||||
@@ -2,6 +2,16 @@ package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type QuestType int32
|
||||
|
||||
const (
|
||||
QuestTypeUnknown QuestType = 0
|
||||
QuestTypeMain QuestType = 1
|
||||
QuestTypeEvent QuestType = 2
|
||||
QuestTypeExtra QuestType = 3
|
||||
QuestTypeBigHunt QuestType = 4
|
||||
)
|
||||
|
||||
type QuestFlowType int32
|
||||
|
||||
const (
|
||||
|
||||
@@ -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,16 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
target := h.targetForBigHunt(questId)
|
||||
var outcome FinishOutcome
|
||||
if !isRetired && !isAnnihilated {
|
||||
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,17 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
target := h.targetForEvent(eventQuestChapterId, questId)
|
||||
var outcome FinishOutcome
|
||||
if !isRetired && !isAnnihilated {
|
||||
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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,16 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
target := h.targetForExtra(questId)
|
||||
var outcome FinishOutcome
|
||||
if !isRetired && !isAnnihilated {
|
||||
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -10,6 +13,7 @@ type RewardGrant struct {
|
||||
PossessionType model.PossessionType
|
||||
PossessionId int32
|
||||
Count int32
|
||||
IsAutoSale bool
|
||||
}
|
||||
|
||||
type FinishOutcome struct {
|
||||
@@ -28,10 +32,12 @@ type QuestHandler struct {
|
||||
Config *masterdata.GameConfig
|
||||
Granter *store.PossessionGranter
|
||||
SideStoryChapterByEventQuestId map[int32]int32
|
||||
Campaigns *campaign.Catalog
|
||||
CharacterRebirth *masterdata.CharacterRebirthCatalog
|
||||
}
|
||||
|
||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
|
||||
granter := BuildGranter(catalog)
|
||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog, characterRebirth *masterdata.CharacterRebirthCatalog) *QuestHandler {
|
||||
granter := BuildGranter(catalog, config)
|
||||
var sideStoryChapters map[int32]int32
|
||||
if sideStory != nil {
|
||||
sideStoryChapters = sideStory.ChapterByEventQuestId
|
||||
@@ -41,10 +47,12 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo
|
||||
Config: config,
|
||||
Granter: granter,
|
||||
SideStoryChapterByEventQuestId: sideStoryChapters,
|
||||
Campaigns: campaigns,
|
||||
CharacterRebirth: characterRebirth,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
||||
func BuildGranter(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *store.PossessionGranter {
|
||||
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
|
||||
for id, cm := range catalog.CostumeById {
|
||||
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
|
||||
@@ -70,12 +78,49 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
partsSellPriceL1 := make(map[int32]int32, len(catalog.SellPriceByRarity))
|
||||
for rarity, fn := range catalog.SellPriceByRarity {
|
||||
partsSellPriceL1[int32(rarity)] = fn.Evaluate(1)
|
||||
}
|
||||
var goldItemId int32
|
||||
if config != nil {
|
||||
goldItemId = config.ConsumableItemIdForGold
|
||||
}
|
||||
|
||||
return &store.PossessionGranter{
|
||||
CostumeById: costumeById,
|
||||
WeaponById: weaponById,
|
||||
@@ -84,5 +129,10 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
||||
ReleaseConditions: releaseConditions,
|
||||
PartsById: partsById,
|
||||
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
|
||||
PartsVariantsByGroupRarity: partsVariants,
|
||||
PartsSubStatusPool: catalog.SubStatusPool,
|
||||
PartsSubStatusDefs: partsSubDefs,
|
||||
PartsSellPriceL1ByRarity: partsSellPriceL1,
|
||||
GoldConsumableItemId: goldItemId,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -203,9 +204,8 @@ func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, o
|
||||
}
|
||||
questState.IsRewardGranted = true
|
||||
}
|
||||
for _, drop := range outcome.DropRewards {
|
||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
||||
}
|
||||
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
|
||||
h.grantDropRewards(user, outcome.DropRewards, raritySet, rankSet, nowMillis)
|
||||
for _, reward := range outcome.ReplayFlowFirstClearRewards {
|
||||
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
|
||||
}
|
||||
@@ -259,11 +259,12 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
||||
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||
|
||||
if !isRetired {
|
||||
var outcome FinishOutcome
|
||||
if !isRetired && !isAnnihilated {
|
||||
outcome = h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
|
||||
|
||||
// A replay-flow finish must NOT move the MainFlow scene pointer: the
|
||||
@@ -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,21 +324,21 @@ 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
|
||||
}
|
||||
|
||||
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
|
||||
var allDrops []RewardGrant
|
||||
for range skipCount {
|
||||
drops := h.computeDropRewards(questDef)
|
||||
for _, drop := range drops {
|
||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
||||
}
|
||||
drops := h.computeDropRewards(questDef, target, nowMillis)
|
||||
h.grantDropRewards(user, drops, raritySet, rankSet, nowMillis)
|
||||
allDrops = append(allDrops, drops...)
|
||||
|
||||
if questDef.Gold != 0 {
|
||||
|
||||
@@ -3,7 +3,10 @@ package questflow
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
@@ -40,7 +43,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 +126,76 @@ 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
|
||||
var autoSaleRarityTiers = map[int32]bool{10: true, 20: true, 30: true, 40: true, 50: true}
|
||||
|
||||
// Rarity tiers (10..50) and ranks (1..5) are disjoint, so the delimited values
|
||||
// are classified by range — independent of the client's map key or delimiter.
|
||||
func parseAutoSaleRules(settings map[int32]store.AutoSaleSettingState) (raritySet, rankSet map[int32]bool) {
|
||||
raritySet = map[int32]bool{}
|
||||
rankSet = map[int32]bool{}
|
||||
for _, s := range settings {
|
||||
for _, n := range extractInts(s.PossessionAutoSaleItemValue) {
|
||||
switch {
|
||||
case autoSaleRarityTiers[n]:
|
||||
raritySet[n] = true
|
||||
case n >= 1 && n <= 5:
|
||||
rankSet[n] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return raritySet, rankSet
|
||||
}
|
||||
|
||||
func extractInts(s string) []int32 {
|
||||
fields := strings.FieldsFunc(s, func(r rune) bool { return r < '0' || r > '9' })
|
||||
out := make([]int32, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
if v, err := strconv.Atoi(f); err == nil {
|
||||
out = append(out, int32(v))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *QuestHandler) grantDropRewards(user *store.UserState, drops []RewardGrant, raritySet, rankSet map[int32]bool, nowMillis int64) {
|
||||
for i := range drops {
|
||||
d := drops[i]
|
||||
if d.PossessionType == model.PossessionTypeParts || d.PossessionType == model.PossessionTypePartsEnhanced {
|
||||
chosenId, sold := h.Granter.GrantOrSellPartsDrop(user, d.PossessionId, raritySet, rankSet, nowMillis)
|
||||
if sold {
|
||||
// Sold parts have no inventory row, so the popup needs the rolled
|
||||
// variant id; kept parts read theirs from the parts table diff.
|
||||
drops[i].PossessionId = chosenId
|
||||
drops[i].IsAutoSale = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
h.applyRewardPossession(user, d.PossessionType, d.PossessionId, d.Count, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest, target campaign.QuestTarget, nowMillis int64) []RewardGrant {
|
||||
var drops []RewardGrant
|
||||
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: bdr.Count,
|
||||
Count: dropRate.Apply(bdr.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
return drops
|
||||
}
|
||||
return h.appendBonusDrops(drops, target, nowMillis)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
@@ -193,8 +247,10 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var maxLevel int32
|
||||
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
||||
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
|
||||
maxLevel = maxLevelFunc.Evaluate(row.LimitBreakCount) +
|
||||
h.CharacterRebirth.CostumeLevelLimitUp(cm.CharacterId, user.CharacterRebirths[cm.CharacterId].RebirthCount)
|
||||
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
|
||||
@@ -202,14 +258,7 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
|
||||
}
|
||||
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]
|
||||
}
|
||||
}
|
||||
row.Level, row.Exp = gameutil.ApplyExpWithMaxLevel(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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gacha"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/masterdata/memorydb"
|
||||
@@ -35,7 +36,17 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
return nil, fmt.Errorf("load quest catalog: %w", err)
|
||||
}
|
||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
|
||||
campaignCatalog, err := campaign.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load campaign catalog: %w", err)
|
||||
}
|
||||
log.Printf("campaign catalog loaded: %d enhance, %d quest", campaignCatalog.EnhanceCount(), campaignCatalog.QuestCount())
|
||||
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
|
||||
}
|
||||
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog, campaignCatalog, characterRebirthCatalog)
|
||||
userdata.SetQuestHandler(questHandler)
|
||||
|
||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||
@@ -127,12 +138,6 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
}
|
||||
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
||||
|
||||
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
|
||||
}
|
||||
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
||||
|
||||
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load companion catalog: %w", err)
|
||||
@@ -172,6 +177,7 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
BigHunt: bigHuntCatalog,
|
||||
Tower: towerCatalog,
|
||||
Labyrinth: labyrinthCatalog,
|
||||
Campaign: campaignCatalog,
|
||||
QuestHandler: questHandler,
|
||||
GachaHandler: gachaHandler,
|
||||
}, nil
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gacha"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/masterdata/memorydb"
|
||||
@@ -52,23 +53,17 @@ type Catalogs struct {
|
||||
BigHunt *masterdata.BigHuntCatalog
|
||||
Tower *masterdata.TowerCatalog
|
||||
Labyrinth *masterdata.LabyrinthCatalog
|
||||
Campaign *campaign.Catalog
|
||||
|
||||
// Catalog-derived handlers must rebuild on every reload because they
|
||||
// embed/cache pointers to specific catalog instances.
|
||||
QuestHandler *questflow.QuestHandler
|
||||
GachaHandler *gacha.GachaHandler
|
||||
}
|
||||
|
||||
// Holder owns the current *Catalogs and the bin.e path. Concurrent readers
|
||||
// call Get(); the single-writer Reload() rebuilds and atomically publishes.
|
||||
type Holder struct {
|
||||
binPath string
|
||||
cur atomic.Pointer[Catalogs]
|
||||
}
|
||||
|
||||
// NewHolder reads the binary at binPath, builds the initial catalogs, and
|
||||
// returns a ready-to-use Holder. Subsequent calls to Reload() re-read the
|
||||
// same path.
|
||||
func NewHolder(binPath string) (*Holder, error) {
|
||||
h := &Holder{binPath: binPath}
|
||||
if err := h.Reload(); err != nil {
|
||||
@@ -77,9 +72,6 @@ func NewHolder(binPath string) (*Holder, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Reload re-reads the bin.e from disk, rebuilds every catalog and handler,
|
||||
// atomically publishes the new snapshot, and bumps the bin.e mtime so client
|
||||
// caches invalidate (see service/data.go GetLatestMasterDataVersion).
|
||||
func (h *Holder) Reload() error {
|
||||
if err := memorydb.Init(h.binPath); err != nil {
|
||||
return fmt.Errorf("memorydb.Init: %w", err)
|
||||
@@ -91,16 +83,11 @@ func (h *Holder) Reload() error {
|
||||
h.cur.Store(c)
|
||||
now := time.Now()
|
||||
if err := os.Chtimes(h.binPath, now, now); err != nil {
|
||||
// Non-fatal: the catalogs swapped fine in-memory; clients may take
|
||||
// longer to invalidate their cached download but server-side state is
|
||||
// already coherent.
|
||||
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the current snapshot. Safe for concurrent callers; the returned
|
||||
// pointer is stable for the duration of the caller's use.
|
||||
func (h *Holder) Get() *Catalogs {
|
||||
return h.cur.Load()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
@@ -50,6 +51,12 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
||||
return
|
||||
}
|
||||
|
||||
expBonus := cat.Campaign.CostumeExpBonus(campaign.CostumeTarget{
|
||||
CostumeId: costume.CostumeId,
|
||||
CharacterId: cm.CharacterId,
|
||||
SkillfulWeaponType: cm.SkillfulWeaponType,
|
||||
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||
|
||||
totalExp := int32(0)
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
@@ -71,7 +78,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
||||
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += expPerUnit * count
|
||||
totalExp += expBonus.Apply(expPerUnit * count)
|
||||
}
|
||||
|
||||
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||
@@ -83,7 +90,12 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
||||
costume.Exp += totalExp
|
||||
|
||||
if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok {
|
||||
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
|
||||
var maxLevel int32
|
||||
if maxLevelFunc, hasMax := catalog.MaxLevelByRarity[cm.RarityType]; hasMax {
|
||||
maxLevel = maxLevelFunc.Evaluate(costume.LimitBreakCount) +
|
||||
cat.CharacterRebirth.CostumeLevelLimitUp(cm.CharacterId, user.CharacterRebirths[cm.CharacterId].RebirthCount)
|
||||
}
|
||||
costume.Level, costume.Exp = gameutil.ApplyExpWithMaxLevel(costume.Exp, thresholds, maxLevel)
|
||||
}
|
||||
|
||||
costume.LatestVersion = nowMillis
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"math/rand"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/runtime"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
@@ -180,17 +182,23 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
||||
successRate = r
|
||||
}
|
||||
}
|
||||
baseRate := successRate
|
||||
successRate = cat.Campaign.PartsRateBonus(campaign.PartsTarget{
|
||||
PartsId: part.PartsId,
|
||||
PartsGroupId: partDef.PartsGroupId,
|
||||
Rarity: model.RarityType(partDef.RarityType),
|
||||
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll}).Apply(baseRate)
|
||||
|
||||
if rand.Intn(1000) < int(successRate) {
|
||||
part.Level++
|
||||
isSuccess = true
|
||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level-1, part.Level, successRate, goldCost)
|
||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰ base=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level-1, part.Level, successRate, baseRate, goldCost)
|
||||
|
||||
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
||||
} else {
|
||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level, successRate, goldCost)
|
||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰ base=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level, successRate, baseRate, goldCost)
|
||||
}
|
||||
|
||||
part.LatestVersion = nowMillis
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/campaign"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
@@ -91,6 +92,12 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
||||
return
|
||||
}
|
||||
|
||||
expBonus := cat.Campaign.WeaponExpBonus(campaign.WeaponTarget{
|
||||
WeaponId: weapon.WeaponId,
|
||||
WeaponType: wm.WeaponType,
|
||||
AttributeType: wm.AttributeType,
|
||||
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||
|
||||
totalExp := int32(0)
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
@@ -112,7 +119,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
||||
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += expPerUnit * count
|
||||
totalExp += expBonus.Apply(expPerUnit * count)
|
||||
}
|
||||
|
||||
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||
@@ -124,16 +131,11 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
||||
weapon.Exp += totalExp
|
||||
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||
var maxLevel int32
|
||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||
if weapon.Level > cap {
|
||||
weapon.Level = cap
|
||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||
weapon.Exp = thresholds[cap]
|
||||
}
|
||||
}
|
||||
maxLevel = awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||
}
|
||||
weapon.Level, weapon.Exp = gameutil.ApplyExpWithMaxLevel(weapon.Exp, thresholds, maxLevel)
|
||||
}
|
||||
|
||||
note := user.WeaponNotes[weapon.WeaponId]
|
||||
@@ -702,6 +704,12 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
||||
return
|
||||
}
|
||||
|
||||
expBonus := cat.Campaign.WeaponExpBonus(campaign.WeaponTarget{
|
||||
WeaponId: weapon.WeaponId,
|
||||
WeaponType: wm.WeaponType,
|
||||
AttributeType: wm.AttributeType,
|
||||
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||
|
||||
totalExp := int32(0)
|
||||
consumedCount := int32(0)
|
||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||
@@ -722,7 +730,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
||||
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
||||
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += baseExp
|
||||
totalExp += expBonus.Apply(baseExp)
|
||||
|
||||
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||
for itemId, count := range medals {
|
||||
@@ -746,16 +754,11 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
||||
weapon.Exp += totalExp
|
||||
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||
var maxLevel int32
|
||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||
if weapon.Level > cap {
|
||||
weapon.Level = cap
|
||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||
weapon.Exp = thresholds[cap]
|
||||
}
|
||||
}
|
||||
maxLevel = awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||
}
|
||||
weapon.Level, weapon.Exp = gameutil.ApplyExpWithMaxLevel(weapon.Exp, thresholds, maxLevel)
|
||||
}
|
||||
|
||||
note := user.WeaponNotes[weapon.WeaponId]
|
||||
|
||||
@@ -85,6 +85,7 @@ func CloneUserState(u UserState) UserState {
|
||||
out.CostumeLotteryEffectPending = maps.Clone(u.CostumeLotteryEffectPending)
|
||||
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
|
||||
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
|
||||
out.QuestAutoOrbit.AccumulatedDrops = append([]AutoOrbitDropEntry(nil), u.QuestAutoOrbit.AccumulatedDrops...)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sort"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -102,7 +103,19 @@ type WeaponStoryReleaseCond struct {
|
||||
|
||||
type PartsRef struct {
|
||||
PartsGroupId int32
|
||||
RarityType int32
|
||||
PartsInitialLotteryId int32
|
||||
PartsStatusMainLotteryGroupId int32
|
||||
PartsStatusSubLotteryGroupId int32
|
||||
}
|
||||
|
||||
// PartsStatusSubDef carries the per-lottery-id sub-status shape needed at
|
||||
// grant time. Held here so the store package does not import masterdata.
|
||||
type PartsStatusSubDef struct {
|
||||
StatusKindType int32
|
||||
StatusCalculationType int32
|
||||
StatusChangeInitialValue int32
|
||||
StatusFunc func(level int32) int32
|
||||
}
|
||||
|
||||
type PossessionGranter struct {
|
||||
@@ -114,6 +127,12 @@ type PossessionGranter struct {
|
||||
|
||||
PartsById map[int32]PartsRef
|
||||
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
||||
PartsVariantsByGroupRarity map[int32]map[int32][]int32
|
||||
PartsSubStatusPool map[int32][]int32
|
||||
PartsSubStatusDefs map[int32]PartsStatusSubDef
|
||||
|
||||
PartsSellPriceL1ByRarity map[int32]int32
|
||||
GoldConsumableItemId int32
|
||||
|
||||
LastChangedStoryWeaponIds []int32
|
||||
}
|
||||
@@ -184,26 +203,108 @@ func (g *PossessionGranter) GrantCompanion(user *UserState, companionId int32, n
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) GrantParts(user *UserState, partsId int32, nowMillis int64) {
|
||||
var mainStatId int32
|
||||
if ref, ok := g.PartsById[partsId]; ok {
|
||||
mainStatId = g.DefaultPartsStatusMainByLotteryGroup[ref.PartsStatusMainLotteryGroupId]
|
||||
if _, exists := user.PartsGroupNotes[ref.PartsGroupId]; !exists {
|
||||
user.PartsGroupNotes[ref.PartsGroupId] = PartsGroupNoteState{
|
||||
PartsGroupId: ref.PartsGroupId,
|
||||
FirstAcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
func (g *PossessionGranter) GrantParts(user *UserState, requestedPartsId int32, nowMillis int64) {
|
||||
chosenPartsId, chosenRef, ok := g.rollPartsVariant(requestedPartsId)
|
||||
if !ok {
|
||||
g.grantBareParts(user, requestedPartsId, nowMillis)
|
||||
return
|
||||
}
|
||||
g.createParts(user, chosenPartsId, chosenRef, nowMillis)
|
||||
}
|
||||
|
||||
// The rolled variant sets both rarity and rank, so the auto-sale decision can
|
||||
// only happen after the roll. Returns the rolled variant id and whether it sold.
|
||||
func (g *PossessionGranter) GrantOrSellPartsDrop(user *UserState, requestedPartsId int32, raritySet, rankSet map[int32]bool, nowMillis int64) (int32, bool) {
|
||||
chosenPartsId, chosenRef, ok := g.rollPartsVariant(requestedPartsId)
|
||||
if !ok {
|
||||
g.grantBareParts(user, requestedPartsId, nowMillis)
|
||||
return requestedPartsId, false
|
||||
}
|
||||
rarity := chosenRef.RarityType
|
||||
rank := chosenRef.PartsInitialLotteryId
|
||||
if price, ok := g.PartsSellPriceL1ByRarity[rarity]; ok && raritySet[rarity] && rankSet[rank] {
|
||||
user.ConsumableItems[g.GoldConsumableItemId] += price
|
||||
log.Printf("[GrantParts] auto-sold chosen=%d rarity=%d rank=%d -> %d gold", chosenPartsId, rarity, rank, price)
|
||||
return chosenPartsId, true
|
||||
}
|
||||
g.createParts(user, chosenPartsId, chosenRef, nowMillis)
|
||||
return chosenPartsId, false
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) grantBareParts(user *UserState, partsId int32, nowMillis int64) {
|
||||
key := uuid.New().String()
|
||||
user.Parts[key] = PartsState{
|
||||
UserPartsUuid: key,
|
||||
PartsId: partsId,
|
||||
Level: 1,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
log.Printf("[GrantParts] unknown partsId=%d, granted as-is with no variant roll", partsId)
|
||||
}
|
||||
|
||||
// rollPartsVariant picks one of a parts group's 5 variants at random; the five
|
||||
// carry distinct PartsInitialLotteryId 1..5, which is the part's rank.
|
||||
func (g *PossessionGranter) rollPartsVariant(requestedPartsId int32) (int32, PartsRef, bool) {
|
||||
ref, refOk := g.PartsById[requestedPartsId]
|
||||
if !refOk {
|
||||
return requestedPartsId, PartsRef{}, false
|
||||
}
|
||||
chosenPartsId := requestedPartsId
|
||||
chosenRef := ref
|
||||
if variants := g.PartsVariantsByGroupRarity[ref.PartsGroupId][ref.RarityType]; len(variants) == 5 {
|
||||
chosenPartsId = variants[rand.Intn(len(variants))]
|
||||
chosenRef = g.PartsById[chosenPartsId]
|
||||
} else {
|
||||
log.Printf("[GrantParts] no 5-variant set for group=%d rarity=%d (have %d), granting requested=%d", ref.PartsGroupId, ref.RarityType, len(variants), requestedPartsId)
|
||||
}
|
||||
return chosenPartsId, chosenRef, true
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) createParts(user *UserState, chosenPartsId int32, chosenRef PartsRef, nowMillis int64) {
|
||||
mainStatId := g.DefaultPartsStatusMainByLotteryGroup[chosenRef.PartsStatusMainLotteryGroupId]
|
||||
if _, exists := user.PartsGroupNotes[chosenRef.PartsGroupId]; !exists {
|
||||
user.PartsGroupNotes[chosenRef.PartsGroupId] = PartsGroupNoteState{
|
||||
PartsGroupId: chosenRef.PartsGroupId,
|
||||
FirstAcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
key := uuid.New().String()
|
||||
user.Parts[key] = PartsState{
|
||||
UserPartsUuid: key,
|
||||
PartsId: chosenPartsId,
|
||||
Level: 1,
|
||||
PartsStatusMainId: mainStatId,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
|
||||
initialCount := chosenRef.PartsInitialLotteryId
|
||||
pool := g.PartsSubStatusPool[chosenRef.PartsStatusSubLotteryGroupId]
|
||||
if initialCount > 1 && len(pool) > 0 {
|
||||
for i := int32(0); i < initialCount-1; i++ {
|
||||
pickId := pool[rand.Intn(len(pool))]
|
||||
def, ok := g.PartsSubStatusDefs[pickId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
val := def.StatusChangeInitialValue
|
||||
if def.StatusFunc != nil {
|
||||
val = def.StatusFunc(1)
|
||||
}
|
||||
user.PartsStatusSubs[PartsStatusSubKey{UserPartsUuid: key, StatusIndex: i + 1}] = PartsStatusSubState{
|
||||
UserPartsUuid: key,
|
||||
StatusIndex: i + 1,
|
||||
PartsStatusSubLotteryId: pickId,
|
||||
Level: 1,
|
||||
StatusKindType: def.StatusKindType,
|
||||
StatusCalculationType: def.StatusCalculationType,
|
||||
StatusChangeValue: val,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[GrantParts] chosen=%d group=%d rarity=%d preUnlockedSubs=%d", chosenPartsId, chosenRef.PartsGroupId, chosenRef.RarityType, initialCount-1)
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
@@ -210,6 +211,17 @@ func load1to1(db *sql.DB, uid int64, u *store.UserState) {
|
||||
Scan(&u.GuerrillaFreeOpen.StartDatetime, &u.GuerrillaFreeOpen.OpenMinutes,
|
||||
&u.GuerrillaFreeOpen.DailyOpenedCount, &u.GuerrillaFreeOpen.LatestVersion)
|
||||
|
||||
var accumulatedDropsJSON string
|
||||
_ = db.QueryRow(`SELECT quest_type, chapter_id, quest_id, max_auto_orbit_count, cleared_auto_orbit_count, last_clear_datetime, latest_version, accumulated_drops_json
|
||||
FROM user_quest_auto_orbit WHERE user_id=?`, uid).
|
||||
Scan(&u.QuestAutoOrbit.QuestType, &u.QuestAutoOrbit.ChapterId, &u.QuestAutoOrbit.QuestId,
|
||||
&u.QuestAutoOrbit.MaxAutoOrbitCount, &u.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||
&u.QuestAutoOrbit.LastClearDatetime, &u.QuestAutoOrbit.LatestVersion,
|
||||
&accumulatedDropsJSON)
|
||||
if accumulatedDropsJSON != "" && accumulatedDropsJSON != "[]" {
|
||||
_ = json.Unmarshal([]byte(accumulatedDropsJSON), &u.QuestAutoOrbit.AccumulatedDrops)
|
||||
}
|
||||
|
||||
var isTicket int
|
||||
_ = db.QueryRow(`SELECT is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version
|
||||
FROM user_explore WHERE user_id=?`, uid).
|
||||
|
||||
@@ -2,12 +2,24 @@ package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func marshalAutoOrbitDrops(drops []store.AutoOrbitDropEntry) string {
|
||||
if len(drops) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
b, err := json.Marshal(drops)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
@@ -109,6 +121,13 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||
uid, u.GuerrillaFreeOpen.StartDatetime, u.GuerrillaFreeOpen.OpenMinutes, u.GuerrillaFreeOpen.DailyOpenedCount, u.GuerrillaFreeOpen.LatestVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec(`INSERT INTO user_quest_auto_orbit (user_id, quest_type, chapter_id, quest_id, max_auto_orbit_count, cleared_auto_orbit_count, last_clear_datetime, latest_version, accumulated_drops_json) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||
uid, u.QuestAutoOrbit.QuestType, u.QuestAutoOrbit.ChapterId, u.QuestAutoOrbit.QuestId,
|
||||
u.QuestAutoOrbit.MaxAutoOrbitCount, u.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||
u.QuestAutoOrbit.LastClearDatetime, u.QuestAutoOrbit.LatestVersion,
|
||||
marshalAutoOrbitDrops(u.QuestAutoOrbit.AccumulatedDrops)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exec(`INSERT INTO user_explore (user_id, is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
||||
uid, boolToInt(u.Explore.IsUseExploreTicket), u.Explore.PlayingExploreId, u.Explore.LatestPlayDatetime, u.Explore.LatestVersion); err != nil {
|
||||
return err
|
||||
@@ -674,6 +693,15 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !before.QuestAutoOrbit.Equal(after.QuestAutoOrbit) {
|
||||
if err := exec(`UPDATE user_quest_auto_orbit SET quest_type=?, chapter_id=?, quest_id=?, max_auto_orbit_count=?, cleared_auto_orbit_count=?, last_clear_datetime=?, latest_version=?, accumulated_drops_json=? WHERE user_id=?`,
|
||||
after.QuestAutoOrbit.QuestType, after.QuestAutoOrbit.ChapterId, after.QuestAutoOrbit.QuestId,
|
||||
after.QuestAutoOrbit.MaxAutoOrbitCount, after.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||
after.QuestAutoOrbit.LastClearDatetime, after.QuestAutoOrbit.LatestVersion,
|
||||
marshalAutoOrbitDrops(after.QuestAutoOrbit.AccumulatedDrops), uid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if before.Explore != after.Explore {
|
||||
if err := exec(`UPDATE user_explore SET is_use_explore_ticket=?, playing_explore_id=?, latest_play_datetime=?, latest_version=? WHERE user_id=?`,
|
||||
boolToInt(after.Explore.IsUseExploreTicket), after.Explore.PlayingExploreId, after.Explore.LatestPlayDatetime, after.Explore.LatestVersion, uid); err != nil {
|
||||
|
||||
@@ -95,6 +95,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
||||
"user_viewed_movies",
|
||||
"user_navi_cutin_played",
|
||||
"user_auto_sale_settings",
|
||||
"user_quest_auto_orbit",
|
||||
"user_explore_scores",
|
||||
"user_tutorials",
|
||||
"user_premium_items",
|
||||
|
||||
@@ -119,6 +119,7 @@ type UserState struct {
|
||||
CostumeLotteryEffectPending map[string]CostumeLotteryEffectPendingState // key: userCostumeUuid
|
||||
AutoSaleSettings map[int32]AutoSaleSettingState
|
||||
CharacterRebirths map[int32]CharacterRebirthState
|
||||
QuestAutoOrbit QuestAutoOrbitState
|
||||
}
|
||||
|
||||
func (u *UserState) EnsureMaps() {
|
||||
@@ -331,6 +332,45 @@ type GuerrillaFreeOpenState struct {
|
||||
LatestVersion int64
|
||||
}
|
||||
|
||||
type AutoOrbitDropEntry struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
IsAutoSale bool
|
||||
}
|
||||
|
||||
type QuestAutoOrbitState struct {
|
||||
QuestType int32
|
||||
ChapterId int32
|
||||
QuestId int32
|
||||
MaxAutoOrbitCount int32
|
||||
ClearedAutoOrbitCount int32
|
||||
LastClearDatetime int64
|
||||
LatestVersion int64
|
||||
AccumulatedDrops []AutoOrbitDropEntry
|
||||
}
|
||||
|
||||
func (s QuestAutoOrbitState) Equal(other QuestAutoOrbitState) bool {
|
||||
if s.QuestType != other.QuestType ||
|
||||
s.ChapterId != other.ChapterId ||
|
||||
s.QuestId != other.QuestId ||
|
||||
s.MaxAutoOrbitCount != other.MaxAutoOrbitCount ||
|
||||
s.ClearedAutoOrbitCount != other.ClearedAutoOrbitCount ||
|
||||
s.LastClearDatetime != other.LastClearDatetime ||
|
||||
s.LatestVersion != other.LatestVersion {
|
||||
return false
|
||||
}
|
||||
if len(s.AccumulatedDrops) != len(other.AccumulatedDrops) {
|
||||
return false
|
||||
}
|
||||
for i := range s.AccumulatedDrops {
|
||||
if s.AccumulatedDrops[i] != other.AccumulatedDrops[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type PortalCageStatusState struct {
|
||||
IsCurrentProgress bool
|
||||
DropItemStartDatetime int64
|
||||
|
||||
@@ -268,6 +268,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
||||
if !mapsEqualStruct(before.TowerAccumulationRewards, after.TowerAccumulationRewards) {
|
||||
add("IUserEventQuestTowerAccumulationReward")
|
||||
}
|
||||
if !before.QuestAutoOrbit.Equal(after.QuestAutoOrbit) {
|
||||
add("IUserQuestAutoOrbit")
|
||||
}
|
||||
if !mapsEqualStruct(before.LabyrinthStages, after.LabyrinthStages) {
|
||||
add("IUserEventQuestLabyrinthStage")
|
||||
}
|
||||
@@ -476,6 +479,8 @@ func keyFieldsForTable(table string) []string {
|
||||
return []string{"userId", "bigHuntWeeklyVersion"}
|
||||
case "IUserDeckTypeNote":
|
||||
return []string{"userId", "deckType"}
|
||||
case "IUserQuestAutoOrbit":
|
||||
return []string{"userId"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package userdata
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
@@ -13,13 +14,30 @@ func sortedQuestRecords(user store.UserState) []map[string]any {
|
||||
ids = append(ids, int(id))
|
||||
}
|
||||
sort.Ints(ids)
|
||||
|
||||
var replayQuestId int32
|
||||
if user.MainQuest.SavedContext.Active && questHandler != nil {
|
||||
if scene, ok := questHandler.SceneById[user.MainQuest.ProgressQuestSceneId]; ok {
|
||||
replayQuestId = scene.QuestId
|
||||
}
|
||||
}
|
||||
|
||||
records := make([]map[string]any, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
row := user.Quests[int32(id)]
|
||||
stateType := row.QuestStateType
|
||||
if replayQuestId != 0 {
|
||||
switch {
|
||||
case int32(id) == replayQuestId:
|
||||
stateType = model.UserQuestStateTypeActive
|
||||
case stateType == model.UserQuestStateTypeActive:
|
||||
stateType = model.UserQuestStateTypeCleared
|
||||
}
|
||||
}
|
||||
records = append(records, map[string]any{
|
||||
"userId": user.UserId,
|
||||
"questId": row.QuestId,
|
||||
"questStateType": row.QuestStateType,
|
||||
"questStateType": stateType,
|
||||
"isBattleOnly": row.IsBattleOnly,
|
||||
"latestStartDatetime": row.LatestStartDatetime,
|
||||
"clearCount": row.ClearCount,
|
||||
@@ -250,10 +268,26 @@ func init() {
|
||||
s, _ := utils.EncodeJSONMaps(records...)
|
||||
return s
|
||||
})
|
||||
register("IUserQuestAutoOrbit", func(user store.UserState) string {
|
||||
s := user.QuestAutoOrbit
|
||||
if s.MaxAutoOrbitCount <= 0 {
|
||||
return "[]"
|
||||
}
|
||||
out, _ := utils.EncodeJSONMaps(map[string]any{
|
||||
"userId": user.UserId,
|
||||
"questType": s.QuestType,
|
||||
"chapterId": s.ChapterId,
|
||||
"questId": s.QuestId,
|
||||
"maxAutoOrbitCount": s.MaxAutoOrbitCount,
|
||||
"clearedAutoOrbitCount": s.ClearedAutoOrbitCount,
|
||||
"lastClearDatetime": s.LastClearDatetime,
|
||||
"latestVersion": s.LatestVersion,
|
||||
})
|
||||
return out
|
||||
})
|
||||
registerStatic(
|
||||
"IUserEventQuestDailyGroupCompleteReward",
|
||||
"IUserQuestReplayFlowRewardGroup",
|
||||
"IUserQuestAutoOrbit",
|
||||
"IUserQuestSceneChoice",
|
||||
"IUserQuestSceneChoiceHistory",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE user_quest_auto_orbit (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY REFERENCES users(user_id),
|
||||
quest_type INTEGER NOT NULL DEFAULT 0,
|
||||
chapter_id INTEGER NOT NULL DEFAULT 0,
|
||||
quest_id INTEGER NOT NULL DEFAULT 0,
|
||||
max_auto_orbit_count INTEGER NOT NULL DEFAULT 0,
|
||||
cleared_auto_orbit_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_clear_datetime INTEGER NOT NULL DEFAULT 0,
|
||||
latest_version INTEGER NOT NULL DEFAULT 0,
|
||||
accumulated_drops_json TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
INSERT INTO user_quest_auto_orbit (user_id) SELECT user_id FROM users;
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS user_quest_auto_orbit;
|
||||
Reference in New Issue
Block a user