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

This commit is contained in:
Ilya Groshev
2026-05-25 09:31:53 +03:00
parent 2d0c0d8ef0
commit dc7c1df4fd
21 changed files with 825 additions and 69 deletions
+170
View File
@@ -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
}
+113
View File
@@ -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
}
+57
View File
@@ -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
}
+85
View File
@@ -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
}
+101
View File
@@ -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
}