diff --git a/server/internal/masterdata/weapon.go b/server/internal/masterdata/weapon.go index ac68c20..7992623 100644 --- a/server/internal/masterdata/weapon.go +++ b/server/internal/masterdata/weapon.go @@ -42,6 +42,8 @@ type WeaponCatalog struct { BaseExpByEnhanceId map[int32]int32 ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup + LevelingEnhanceIdByWeaponId map[int32]int32 + AwakenByWeaponId map[int32]EntityMWeaponAwaken AwakenMaterialsByGroupId map[int32][]EntityMWeaponAwakenMaterialGroup } @@ -142,6 +144,8 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)), ReleaseConditionsByGroupId: make(map[int32][]EntityMWeaponStoryReleaseConditionGroup), + LevelingEnhanceIdByWeaponId: make(map[int32]int32, len(weapons)), + AwakenByWeaponId: make(map[int32]EntityMWeaponAwaken, len(awakenRows)), AwakenMaterialsByGroupId: make(map[int32][]EntityMWeaponAwakenMaterialGroup), } @@ -342,5 +346,28 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { } log.Printf("[WeaponCatalog] rarity fallback: assigned synthetic enhance IDs to %d weapons", fallbackCount) + // Build LevelingEnhanceIdByWeaponId: every member of an evolution chain + // inherits the chain root's (post-rarity-fallback) WeaponSpecificEnhanceId + // so cumulative exp stays consistent across Evolve. Weapons not in any + // chain map to their own enhance id. + for _, rows := range grouped { + if len(rows) == 0 { + continue + } + rootMaster, ok := catalog.Weapons[rows[0].WeaponId] + if !ok { + continue + } + rootEnhanceId := rootMaster.WeaponSpecificEnhanceId + for _, row := range rows { + catalog.LevelingEnhanceIdByWeaponId[row.WeaponId] = rootEnhanceId + } + } + for wid, w := range catalog.Weapons { + if _, exists := catalog.LevelingEnhanceIdByWeaponId[wid]; !exists { + catalog.LevelingEnhanceIdByWeaponId[wid] = w.WeaponSpecificEnhanceId + } + } + return catalog, nil } diff --git a/server/internal/service/weapon.go b/server/internal/service/weapon.go index 1c78640..2db1624 100644 --- a/server/internal/service/weapon.go +++ b/server/internal/service/weapon.go @@ -122,8 +122,26 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh } weapon.Exp += totalExp - if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId] + if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok { weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) + if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + cap := maxFunc.Evaluate(weapon.LimitBreakCount) + if weapon.Level > cap { + weapon.Level = cap + if int(cap) >= 0 && int(cap) < len(thresholds) { + weapon.Exp = thresholds[cap] + } + } + } + } + + note := user.WeaponNotes[weapon.WeaponId] + if note.MaxLevel < weapon.Level { + note.WeaponId = weapon.WeaponId + note.MaxLevel = weapon.Level + note.LatestVersion = nowMillis + user.WeaponNotes[weapon.WeaponId] = note } weapon.LatestVersion = nowMillis @@ -240,10 +258,52 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost) } + oldWeaponId := wm.WeaponId weapon.WeaponId = evolvedId weapon.LatestVersion = nowMillis user.Weapons[req.UserWeaponUuid] = weapon + note, hasNote := user.WeaponNotes[evolvedId] + if !hasNote { + note = store.WeaponNoteState{ + WeaponId: evolvedId, + MaxLevel: weapon.Level, + MaxLimitBreakCount: weapon.LimitBreakCount, + FirstAcquisitionDatetime: nowMillis, + LatestVersion: nowMillis, + } + user.WeaponNotes[evolvedId] = note + } else { + changed := false + if note.MaxLevel < weapon.Level { + note.MaxLevel = weapon.Level + changed = true + } + if note.MaxLimitBreakCount < weapon.LimitBreakCount { + note.MaxLimitBreakCount = weapon.LimitBreakCount + changed = true + } + if changed { + note.WeaponId = evolvedId + note.LatestVersion = nowMillis + user.WeaponNotes[evolvedId] = note + } + } + + if oldStory, hasOldStory := user.WeaponStories[oldWeaponId]; hasOldStory { + newStory, hasNewStory := user.WeaponStories[evolvedId] + if !hasNewStory || newStory.ReleasedMaxStoryIndex < oldStory.ReleasedMaxStoryIndex { + if user.WeaponStories == nil { + user.WeaponStories = make(map[int32]store.WeaponStoryState) + } + user.WeaponStories[evolvedId] = store.WeaponStoryState{ + WeaponId: evolvedId, + ReleasedMaxStoryIndex: oldStory.ReleasedMaxStoryIndex, + LatestVersion: nowMillis, + } + } + } + evolvedMaster, ok := catalog.Weapons[evolvedId] if ok { if slots, ok := catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok { @@ -259,7 +319,7 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) } } - log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId) + log.Printf("[WeaponService] Evolve: weaponId %d -> %d", oldWeaponId, evolvedId) checkWeaponStoryUnlocks(catalog, user, evolvedId, weapon.Level, nowMillis) }) @@ -657,7 +717,8 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan continue } - baseExp := catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId] + matLevelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[matWeapon.WeaponId] + baseExp := catalog.BaseExpByEnhanceId[matLevelingEnhanceId] if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType { baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000 } @@ -683,8 +744,26 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan } weapon.Exp += totalExp - if thresholds, ok := catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId] + if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok { weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds) + if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok { + cap := maxFunc.Evaluate(weapon.LimitBreakCount) + if weapon.Level > cap { + weapon.Level = cap + if int(cap) >= 0 && int(cap) < len(thresholds) { + weapon.Exp = thresholds[cap] + } + } + } + } + + note := user.WeaponNotes[weapon.WeaponId] + if note.MaxLevel < weapon.Level { + note.WeaponId = weapon.WeaponId + note.MaxLevel = weapon.Level + note.LatestVersion = nowMillis + user.WeaponNotes[weapon.WeaponId] = note } weapon.LatestVersion = nowMillis diff --git a/server/internal/userdata/changed_tables.go b/server/internal/userdata/changed_tables.go index 2f11c87..854a68e 100644 --- a/server/internal/userdata/changed_tables.go +++ b/server/internal/userdata/changed_tables.go @@ -294,17 +294,16 @@ func ComputeDelta(before, after *store.UserState, changedTables []string) map[st diff := make(map[string]*pb.DiffData, len(changedTables)) for _, table := range changedTables { afterJSON := projectTable(table, *after) + updates := afterJSON deleteKeys := "[]" if kf := keyFieldsForTable(table); len(kf) > 0 { - beforeJSON := projectTable(table, *before) - deleteKeys = ComputeDeleteKeys( - parseJSONRecords(beforeJSON), - parseJSONRecords(afterJSON), - kf, - ) + beforeRecs := parseJSONRecords(projectTable(table, *before)) + afterRecs := parseJSONRecords(afterJSON) + updates = ComputeUpdateRecords(beforeRecs, afterRecs, kf) + deleteKeys = ComputeDeleteKeys(beforeRecs, afterRecs, kf) } diff[table] = &pb.DiffData{ - UpdateRecordsJson: afterJSON, + UpdateRecordsJson: updates, DeleteKeysJson: deleteKeys, } } diff --git a/server/internal/userdata/diffset.go b/server/internal/userdata/diffset.go index a585d15..5973215 100644 --- a/server/internal/userdata/diffset.go +++ b/server/internal/userdata/diffset.go @@ -3,6 +3,7 @@ package userdata import ( "encoding/json" "fmt" + "reflect" "strings" pb "lunar-tear/server/gen/proto" @@ -120,6 +121,34 @@ func ComputeDeleteKeys(oldRecords, newRecords []map[string]any, keyFields []stri return string(b) } +func ComputeUpdateRecords(oldRecords, newRecords []map[string]any, keyFields []string) string { + if len(newRecords) == 0 { + return "[]" + } + + oldByKey := make(map[string]map[string]any, len(oldRecords)) + for _, r := range oldRecords { + oldByKey[compositeKey(r, keyFields)] = r + } + + var changed []map[string]any + for _, r := range newRecords { + prev, exists := oldByKey[compositeKey(r, keyFields)] + if !exists || !reflect.DeepEqual(prev, r) { + changed = append(changed, r) + } + } + + if len(changed) == 0 { + return "[]" + } + b, err := json.Marshal(changed) + if err != nil { + return "[]" + } + return string(b) +} + func compositeKey(record map[string]any, fields []string) string { var sb strings.Builder for i, f := range fields {