From c961fde8ac234b0f942cb7dd710c1e997b56f5cf Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Wed, 27 May 2026 13:23:07 +0300 Subject: [PATCH] Share exp-cap helper across enhance flows; fix quest-reward rebirth --- server/internal/gameutil/exp.go | 13 +++++++++++ .../internal/masterdata/character_rebirth.go | 19 ++++++++++++++++ server/internal/questflow/handler.go | 4 +++- server/internal/questflow/rewards.go | 13 ++++------- server/internal/runtime/build.go | 13 +++++------ server/internal/service/costume.go | 7 +++++- server/internal/service/weapon.go | 22 +++++-------------- 7 files changed, 57 insertions(+), 34 deletions(-) diff --git a/server/internal/gameutil/exp.go b/server/internal/gameutil/exp.go index dc766f3..bde4222 100644 --- a/server/internal/gameutil/exp.go +++ b/server/internal/gameutil/exp.go @@ -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 +} diff --git a/server/internal/masterdata/character_rebirth.go b/server/internal/masterdata/character_rebirth.go index 0a67908..b32ae8f 100644 --- a/server/internal/masterdata/character_rebirth.go +++ b/server/internal/masterdata/character_rebirth.go @@ -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 { diff --git a/server/internal/questflow/handler.go b/server/internal/questflow/handler.go index b426658..923b261 100644 --- a/server/internal/questflow/handler.go +++ b/server/internal/questflow/handler.go @@ -32,9 +32,10 @@ type QuestHandler struct { Granter *store.PossessionGranter SideStoryChapterByEventQuestId map[int32]int32 Campaigns *campaign.Catalog + CharacterRebirth *masterdata.CharacterRebirthCatalog } -func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog) *QuestHandler { +func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog, characterRebirth *masterdata.CharacterRebirthCatalog) *QuestHandler { granter := BuildGranter(catalog) var sideStoryChapters map[int32]int32 if sideStory != nil { @@ -46,6 +47,7 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo Granter: granter, SideStoryChapterByEventQuestId: sideStoryChapters, Campaigns: campaigns, + CharacterRebirth: characterRebirth, } } diff --git a/server/internal/questflow/rewards.go b/server/internal/questflow/rewards.go index 931bcb2..110b773 100644 --- a/server/internal/questflow/rewards.go +++ b/server/internal/questflow/rewards.go @@ -197,8 +197,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 @@ -206,14 +208,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) diff --git a/server/internal/runtime/build.go b/server/internal/runtime/build.go index c25118f..4c85501 100644 --- a/server/internal/runtime/build.go +++ b/server/internal/runtime/build.go @@ -41,7 +41,12 @@ func buildCatalogs() (*Catalogs, error) { 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) + 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() @@ -133,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) diff --git a/server/internal/service/costume.go b/server/internal/service/costume.go index 06181b7..e2d8b3c 100644 --- a/server/internal/service/costume.go +++ b/server/internal/service/costume.go @@ -90,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 diff --git a/server/internal/service/weapon.go b/server/internal/service/weapon.go index c68cd1c..4ee8539 100644 --- a/server/internal/service/weapon.go +++ b/server/internal/service/weapon.go @@ -131,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] @@ -759,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]