mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Compare commits
1 Commits
2d0c0d8ef0
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,
|
||||||
|
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(base + b.bonusPermil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpBonus struct {
|
||||||
|
bonusPermil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b ExpBonus) Apply(base int32) int32 {
|
||||||
|
return base * (1000 + b.bonusPermil) / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaminaMul struct {
|
||||||
|
permil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StaminaMul) Apply(base int32) int32 {
|
||||||
|
if m.permil == 1000 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return base * m.permil / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropRateMul struct {
|
||||||
|
bonusPermil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropRateMul) Apply(base int32) int32 {
|
||||||
|
return (base*(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
|
||||||
|
}
|
||||||
@@ -158,5 +158,9 @@ func buildPartsStatusMain() (map[int32]PartsStatusMainDef, map[int32][]int32) {
|
|||||||
id++
|
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
|
return defs, pool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type QuestCatalog struct {
|
|||||||
RoutesBySeason map[int32][]int32
|
RoutesBySeason map[int32][]int32
|
||||||
RouteCompletionQuestId map[int32]int32
|
RouteCompletionQuestId map[int32]int32
|
||||||
BattleOnlyTargetSceneByQuestId map[int32]int32
|
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||||
|
MainQuestChapterIdByQuestId map[int32]int32
|
||||||
|
EventQuestTypeByChapterId map[int32]int32
|
||||||
|
|
||||||
UserExpThresholds []int32
|
UserExpThresholds []int32
|
||||||
CharacterExpThresholds []int32
|
CharacterExpThresholds []int32
|
||||||
@@ -382,12 +384,23 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
||||||
}
|
}
|
||||||
routeIdByQuestId := make(map[int32]int32)
|
routeIdByQuestId := make(map[int32]int32)
|
||||||
|
mainQuestChapterIdByQuestId := make(map[int32]int32)
|
||||||
for _, sequence := range sequences {
|
for _, sequence := range sequences {
|
||||||
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
||||||
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
|
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))
|
sortedChapters := make([]EntityMMainQuestChapter, len(chapters))
|
||||||
copy(sortedChapters, chapters)
|
copy(sortedChapters, chapters)
|
||||||
sort.Slice(sortedChapters, func(i, j int) bool {
|
sort.Slice(sortedChapters, func(i, j int) bool {
|
||||||
@@ -589,6 +602,8 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
RoutesBySeason: routesBySeason,
|
RoutesBySeason: routesBySeason,
|
||||||
RouteCompletionQuestId: routeCompletionQuestId,
|
RouteCompletionQuestId: routeCompletionQuestId,
|
||||||
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||||
|
MainQuestChapterIdByQuestId: mainQuestChapterIdByQuestId,
|
||||||
|
EventQuestTypeByChapterId: eventQuestTypeByChapterId,
|
||||||
|
|
||||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ func (h *QuestHandler) HandleBigHuntQuestStart(user *store.UserState, questId, u
|
|||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
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]
|
questState := user.Quests[questId]
|
||||||
@@ -33,13 +34,15 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForBigHunt(questId)
|
||||||
|
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
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 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
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]
|
questState := user.Quests[questId]
|
||||||
@@ -42,14 +43,16 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForEvent(eventQuestChapterId, questId)
|
||||||
|
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ func (h *QuestHandler) HandleExtraQuestStart(user *store.UserState, questId, use
|
|||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
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]
|
questState := user.Quests[questId]
|
||||||
@@ -40,13 +41,15 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForExtra(questId)
|
||||||
|
outcome := h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package questflow
|
package questflow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
@@ -28,9 +31,10 @@ type QuestHandler struct {
|
|||||||
Config *masterdata.GameConfig
|
Config *masterdata.GameConfig
|
||||||
Granter *store.PossessionGranter
|
Granter *store.PossessionGranter
|
||||||
SideStoryChapterByEventQuestId map[int32]int32
|
SideStoryChapterByEventQuestId map[int32]int32
|
||||||
|
Campaigns *campaign.Catalog
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
|
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog) *QuestHandler {
|
||||||
granter := BuildGranter(catalog)
|
granter := BuildGranter(catalog)
|
||||||
var sideStoryChapters map[int32]int32
|
var sideStoryChapters map[int32]int32
|
||||||
if sideStory != nil {
|
if sideStory != nil {
|
||||||
@@ -41,6 +45,7 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo
|
|||||||
Config: config,
|
Config: config,
|
||||||
Granter: granter,
|
Granter: granter,
|
||||||
SideStoryChapterByEventQuestId: sideStoryChapters,
|
SideStoryChapterByEventQuestId: sideStoryChapters,
|
||||||
|
Campaigns: campaigns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +75,40 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
|||||||
releaseConditions[groupId] = conds
|
releaseConditions[groupId] = conds
|
||||||
}
|
}
|
||||||
partsById := make(map[int32]store.PartsRef, len(catalog.PartsById))
|
partsById := make(map[int32]store.PartsRef, len(catalog.PartsById))
|
||||||
|
partsVariants := make(map[int32]map[int32][]int32)
|
||||||
for id, p := range catalog.PartsById {
|
for id, p := range catalog.PartsById {
|
||||||
partsById[id] = store.PartsRef{
|
partsById[id] = store.PartsRef{
|
||||||
PartsGroupId: p.PartsGroupId,
|
PartsGroupId: p.PartsGroupId,
|
||||||
|
RarityType: p.RarityType,
|
||||||
|
PartsInitialLotteryId: p.PartsInitialLotteryId,
|
||||||
PartsStatusMainLotteryGroupId: p.PartsStatusMainLotteryGroupId,
|
PartsStatusMainLotteryGroupId: p.PartsStatusMainLotteryGroupId,
|
||||||
|
PartsStatusSubLotteryGroupId: p.PartsStatusSubLotteryGroupId,
|
||||||
|
}
|
||||||
|
if partsVariants[p.PartsGroupId] == nil {
|
||||||
|
partsVariants[p.PartsGroupId] = map[int32][]int32{}
|
||||||
|
}
|
||||||
|
partsVariants[p.PartsGroupId][p.RarityType] = append(partsVariants[p.PartsGroupId][p.RarityType], p.PartsId)
|
||||||
|
}
|
||||||
|
for _, byRarity := range partsVariants {
|
||||||
|
for _, ids := range byRarity {
|
||||||
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partsSubDefs := make(map[int32]store.PartsStatusSubDef, len(catalog.PartsStatusMainById))
|
||||||
|
for id, d := range catalog.PartsStatusMainById {
|
||||||
|
var fn func(int32) int32
|
||||||
|
if f, ok := catalog.FuncResolver.Resolve(d.StatusNumericalFunctionId); ok {
|
||||||
|
fn = f.Evaluate
|
||||||
|
}
|
||||||
|
partsSubDefs[id] = store.PartsStatusSubDef{
|
||||||
|
StatusKindType: d.StatusKindType,
|
||||||
|
StatusCalculationType: d.StatusCalculationType,
|
||||||
|
StatusChangeInitialValue: d.StatusChangeInitialValue,
|
||||||
|
StatusFunc: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &store.PossessionGranter{
|
return &store.PossessionGranter{
|
||||||
CostumeById: costumeById,
|
CostumeById: costumeById,
|
||||||
WeaponById: weaponById,
|
WeaponById: weaponById,
|
||||||
@@ -84,5 +117,8 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
|||||||
ReleaseConditions: releaseConditions,
|
ReleaseConditions: releaseConditions,
|
||||||
PartsById: partsById,
|
PartsById: partsById,
|
||||||
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
|
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
|
||||||
|
PartsVariantsByGroupRarity: partsVariants,
|
||||||
|
PartsSubStatusPool: catalog.SubStatusPool,
|
||||||
|
PartsSubStatusDefs: partsSubDefs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
if quest.Stamina > 0 {
|
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]
|
questState := user.Quests[questId]
|
||||||
@@ -259,7 +260,7 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
|
||||||
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
wasMenuReplay := user.MainQuest.SavedContext.Active
|
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||||
|
|
||||||
@@ -277,8 +278,9 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, h.targetForMain(questId), nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
@@ -322,18 +324,19 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target := h.targetForMain(questId)
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
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
|
skipTicketId := h.Config.ConsumableItemIdForQuestSkipTicket
|
||||||
user.ConsumableItems[skipTicketId] -= skipCount
|
user.ConsumableItems[skipTicketId] -= skipCount
|
||||||
if user.ConsumableItems[skipTicketId] < 0 {
|
if user.ConsumableItems[skipTicketId] < 0 {
|
||||||
user.ConsumableItems[skipTicketId] = 0
|
user.ConsumableItems[skipTicketId] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var allDrops []RewardGrant
|
var allDrops []RewardGrant
|
||||||
for range skipCount {
|
for range skipCount {
|
||||||
drops := h.computeDropRewards(questDef)
|
drops := h.computeDropRewards(questDef, target, nowMillis)
|
||||||
for _, drop := range drops {
|
for _, drop := range drops {
|
||||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
@@ -40,7 +41,7 @@ func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef m
|
|||||||
return rewardGroupId
|
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{}
|
outcome := FinishOutcome{}
|
||||||
questState, ok := user.Quests[questId]
|
questState, ok := user.Quests[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -123,25 +124,28 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
outcome.DropRewards = h.computeDropRewards(questDef, target, nowMillis)
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest) []RewardGrant {
|
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest, target campaign.QuestTarget, nowMillis int64) []RewardGrant {
|
||||||
if questDef.QuestPickupRewardGroupId == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var drops []RewardGrant
|
var drops []RewardGrant
|
||||||
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
|
var dropRate campaign.DropRateMul
|
||||||
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
|
if h.Campaigns != nil {
|
||||||
drops = append(drops, RewardGrant{
|
dropRate = h.Campaigns.QuestDropRate(target, h.campaignFilter(nowMillis))
|
||||||
PossessionType: model.PossessionType(bdr.PossessionType),
|
}
|
||||||
PossessionId: bdr.PossessionId,
|
if questDef.QuestPickupRewardGroupId != 0 {
|
||||||
Count: bdr.Count,
|
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
|
||||||
})
|
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
|
||||||
|
drops = append(drops, RewardGrant{
|
||||||
|
PossessionType: model.PossessionType(bdr.PossessionType),
|
||||||
|
PossessionId: bdr.PossessionId,
|
||||||
|
Count: dropRate.Apply(bdr.Count),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return drops
|
return h.appendBonusDrops(drops, target, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gacha"
|
"lunar-tear/server/internal/gacha"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/masterdata/memorydb"
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
@@ -35,7 +36,12 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
return nil, fmt.Errorf("load quest catalog: %w", err)
|
return nil, fmt.Errorf("load quest catalog: %w", err)
|
||||||
}
|
}
|
||||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
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())
|
||||||
|
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog, campaignCatalog)
|
||||||
userdata.SetQuestHandler(questHandler)
|
userdata.SetQuestHandler(questHandler)
|
||||||
|
|
||||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||||
@@ -172,6 +178,7 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
BigHunt: bigHuntCatalog,
|
BigHunt: bigHuntCatalog,
|
||||||
Tower: towerCatalog,
|
Tower: towerCatalog,
|
||||||
Labyrinth: labyrinthCatalog,
|
Labyrinth: labyrinthCatalog,
|
||||||
|
Campaign: campaignCatalog,
|
||||||
QuestHandler: questHandler,
|
QuestHandler: questHandler,
|
||||||
GachaHandler: gachaHandler,
|
GachaHandler: gachaHandler,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gacha"
|
"lunar-tear/server/internal/gacha"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/masterdata/memorydb"
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
@@ -52,23 +53,17 @@ type Catalogs struct {
|
|||||||
BigHunt *masterdata.BigHuntCatalog
|
BigHunt *masterdata.BigHuntCatalog
|
||||||
Tower *masterdata.TowerCatalog
|
Tower *masterdata.TowerCatalog
|
||||||
Labyrinth *masterdata.LabyrinthCatalog
|
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
|
QuestHandler *questflow.QuestHandler
|
||||||
GachaHandler *gacha.GachaHandler
|
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 {
|
type Holder struct {
|
||||||
binPath string
|
binPath string
|
||||||
cur atomic.Pointer[Catalogs]
|
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) {
|
func NewHolder(binPath string) (*Holder, error) {
|
||||||
h := &Holder{binPath: binPath}
|
h := &Holder{binPath: binPath}
|
||||||
if err := h.Reload(); err != nil {
|
if err := h.Reload(); err != nil {
|
||||||
@@ -77,9 +72,6 @@ func NewHolder(binPath string) (*Holder, error) {
|
|||||||
return h, nil
|
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 {
|
func (h *Holder) Reload() error {
|
||||||
if err := memorydb.Init(h.binPath); err != nil {
|
if err := memorydb.Init(h.binPath); err != nil {
|
||||||
return fmt.Errorf("memorydb.Init: %w", err)
|
return fmt.Errorf("memorydb.Init: %w", err)
|
||||||
@@ -91,16 +83,11 @@ func (h *Holder) Reload() error {
|
|||||||
h.cur.Store(c)
|
h.cur.Store(c)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if err := os.Chtimes(h.binPath, now, now); err != nil {
|
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)
|
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func (h *Holder) Get() *Catalogs {
|
||||||
return h.cur.Load()
|
return h.cur.Load()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
@@ -50,6 +51,12 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
return
|
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)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
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 {
|
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
||||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expBonus.Apply(expPerUnit * count)
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/runtime"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
@@ -180,17 +182,23 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
|||||||
successRate = r
|
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) {
|
if rand.Intn(1000) < int(successRate) {
|
||||||
part.Level++
|
part.Level++
|
||||||
isSuccess = true
|
isSuccess = true
|
||||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
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, goldCost)
|
part.PartsId, part.Level-1, part.Level, successRate, baseRate, goldCost)
|
||||||
|
|
||||||
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰ base=%d‰, cost=%d gold)",
|
||||||
part.PartsId, part.Level, successRate, goldCost)
|
part.PartsId, part.Level, successRate, baseRate, goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
part.LatestVersion = nowMillis
|
part.LatestVersion = nowMillis
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
@@ -91,6 +92,12 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
return
|
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)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
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 {
|
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
||||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expBonus.Apply(expPerUnit * count)
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||||
@@ -702,6 +709,12 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
return
|
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)
|
totalExp := int32(0)
|
||||||
consumedCount := int32(0)
|
consumedCount := int32(0)
|
||||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||||
@@ -722,7 +735,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
||||||
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += baseExp
|
totalExp += expBonus.Apply(baseExp)
|
||||||
|
|
||||||
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||||
for itemId, count := range medals {
|
for itemId, count := range medals {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -102,7 +103,19 @@ type WeaponStoryReleaseCond struct {
|
|||||||
|
|
||||||
type PartsRef struct {
|
type PartsRef struct {
|
||||||
PartsGroupId int32
|
PartsGroupId int32
|
||||||
|
RarityType int32
|
||||||
|
PartsInitialLotteryId int32
|
||||||
PartsStatusMainLotteryGroupId 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 {
|
type PossessionGranter struct {
|
||||||
@@ -114,6 +127,9 @@ type PossessionGranter struct {
|
|||||||
|
|
||||||
PartsById map[int32]PartsRef
|
PartsById map[int32]PartsRef
|
||||||
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
||||||
|
PartsVariantsByGroupRarity map[int32]map[int32][]int32
|
||||||
|
PartsSubStatusPool map[int32][]int32
|
||||||
|
PartsSubStatusDefs map[int32]PartsStatusSubDef
|
||||||
|
|
||||||
LastChangedStoryWeaponIds []int32
|
LastChangedStoryWeaponIds []int32
|
||||||
}
|
}
|
||||||
@@ -184,26 +200,73 @@ func (g *PossessionGranter) GrantCompanion(user *UserState, companionId int32, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *PossessionGranter) GrantParts(user *UserState, partsId int32, nowMillis int64) {
|
func (g *PossessionGranter) GrantParts(user *UserState, requestedPartsId int32, nowMillis int64) {
|
||||||
var mainStatId int32
|
ref, refOk := g.PartsById[requestedPartsId]
|
||||||
if ref, ok := g.PartsById[partsId]; ok {
|
if !refOk {
|
||||||
mainStatId = g.DefaultPartsStatusMainByLotteryGroup[ref.PartsStatusMainLotteryGroupId]
|
key := uuid.New().String()
|
||||||
if _, exists := user.PartsGroupNotes[ref.PartsGroupId]; !exists {
|
user.Parts[key] = PartsState{
|
||||||
user.PartsGroupNotes[ref.PartsGroupId] = PartsGroupNoteState{
|
UserPartsUuid: key,
|
||||||
PartsGroupId: ref.PartsGroupId,
|
PartsId: requestedPartsId,
|
||||||
FirstAcquisitionDatetime: nowMillis,
|
Level: 1,
|
||||||
LatestVersion: nowMillis,
|
AcquisitionDatetime: nowMillis,
|
||||||
}
|
}
|
||||||
|
log.Printf("[GrantParts] unknown partsId=%d, granted as-is with no variant roll", requestedPartsId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
key := uuid.New().String()
|
||||||
user.Parts[key] = PartsState{
|
user.Parts[key] = PartsState{
|
||||||
UserPartsUuid: key,
|
UserPartsUuid: key,
|
||||||
PartsId: partsId,
|
PartsId: chosenPartsId,
|
||||||
Level: 1,
|
Level: 1,
|
||||||
PartsStatusMainId: mainStatId,
|
PartsStatusMainId: mainStatId,
|
||||||
AcquisitionDatetime: nowMillis,
|
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] requested=%d chosen=%d variant=%d group=%d rarity=%d preUnlockedSubs=%d", requestedPartsId, chosenPartsId, initialCount, chosenRef.PartsGroupId, chosenRef.RarityType, initialCount-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package userdata
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
@@ -13,13 +14,30 @@ func sortedQuestRecords(user store.UserState) []map[string]any {
|
|||||||
ids = append(ids, int(id))
|
ids = append(ids, int(id))
|
||||||
}
|
}
|
||||||
sort.Ints(ids)
|
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))
|
records := make([]map[string]any, 0, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
row := user.Quests[int32(id)]
|
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{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"questId": row.QuestId,
|
"questId": row.QuestId,
|
||||||
"questStateType": row.QuestStateType,
|
"questStateType": stateType,
|
||||||
"isBattleOnly": row.IsBattleOnly,
|
"isBattleOnly": row.IsBattleOnly,
|
||||||
"latestStartDatetime": row.LatestStartDatetime,
|
"latestStartDatetime": row.LatestStartDatetime,
|
||||||
"clearCount": row.ClearCount,
|
"clearCount": row.ClearCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user