From 61507599fcdc2f71dacf72e2440a6804b4500a6d Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Thu, 16 Apr 2026 15:39:22 +0300 Subject: [PATCH] Add weapon awakening functionality --- server/internal/masterdata/weapon.go | 44 ++++++++++++ server/internal/model/status.go | 8 +++ server/internal/service/weapon.go | 82 ++++++++++++++++++++-- server/internal/store/types.go | 9 +++ server/internal/userdata/proj_inventory.go | 19 ++++- 5 files changed, 157 insertions(+), 5 deletions(-) diff --git a/server/internal/masterdata/weapon.go b/server/internal/masterdata/weapon.go index 925b47e..3971f24 100644 --- a/server/internal/masterdata/weapon.go +++ b/server/internal/masterdata/weapon.go @@ -94,6 +94,27 @@ type WeaponAbilityEnhanceMaterialRow struct { SortOrder int32 `json:"SortOrder"` } +type WeaponAwakenRow struct { + WeaponId int32 `json:"WeaponId"` + WeaponAwakenEffectGroupId int32 `json:"WeaponAwakenEffectGroupId"` + WeaponAwakenMaterialGroupId int32 `json:"WeaponAwakenMaterialGroupId"` + ConsumeGold int32 `json:"ConsumeGold"` + LevelLimitUp int32 `json:"LevelLimitUp"` +} + +type WeaponAwakenEffectGroupRow struct { + WeaponAwakenEffectGroupId int32 `json:"WeaponAwakenEffectGroupId"` + WeaponAwakenEffectType int32 `json:"WeaponAwakenEffectType"` + WeaponAwakenEffectId int32 `json:"WeaponAwakenEffectId"` +} + +type WeaponAwakenMaterialGroupRow struct { + WeaponAwakenMaterialGroupId int32 `json:"WeaponAwakenMaterialGroupId"` + MaterialId int32 `json:"MaterialId"` + Count int32 `json:"Count"` + SortOrder int32 `json:"SortOrder"` +} + type weaponRarityEnhanceRow struct { RarityType int32 `json:"RarityType"` BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"` @@ -137,6 +158,9 @@ type WeaponCatalog struct { LimitBreakCostByMaterialByEnhanceId map[int32]NumericalFunc BaseExpByEnhanceId map[int32]int32 ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow + + AwakenByWeaponId map[int32]WeaponAwakenRow + AwakenMaterialsByGroupId map[int32][]WeaponAwakenMaterialGroupRow } func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { @@ -199,6 +223,15 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { return nil, fmt.Errorf("load weapon story release condition table: %w", err) } + awakenRows, err := utils.ReadJSON[WeaponAwakenRow]("EntityMWeaponAwakenTable.json") + if err != nil { + return nil, fmt.Errorf("load weapon awaken table: %w", err) + } + awakenMatRows, err := utils.ReadJSON[WeaponAwakenMaterialGroupRow]("EntityMWeaponAwakenMaterialGroupTable.json") + if err != nil { + return nil, fmt.Errorf("load weapon awaken material group table: %w", err) + } + catalog := &WeaponCatalog{ Weapons: make(map[int32]WeaponMasterRow, len(weapons)), Materials: matCatalog.ByType[model.MaterialTypeWeaponEnhancement], @@ -225,6 +258,9 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { LimitBreakCostByMaterialByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)), BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)), ReleaseConditionsByGroupId: make(map[int32][]WeaponStoryReleaseConditionRow), + + AwakenByWeaponId: make(map[int32]WeaponAwakenRow, len(awakenRows)), + AwakenMaterialsByGroupId: make(map[int32][]WeaponAwakenMaterialGroupRow), } for _, w := range weapons { @@ -353,6 +389,14 @@ func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) { catalog.ReleaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c) } + for _, row := range awakenRows { + catalog.AwakenByWeaponId[row.WeaponId] = row + } + for _, row := range awakenMatRows { + catalog.AwakenMaterialsByGroupId[row.WeaponAwakenMaterialGroupId] = append( + catalog.AwakenMaterialsByGroupId[row.WeaponAwakenMaterialGroupId], row) + } + // Rarity-based enhancement fallback: for weapons with WeaponSpecificEnhanceId == 0, // use EntityMWeaponRarityTable curves via synthetic enhance IDs (-RarityType). rarityByType := make(map[int32]weaponRarityEnhanceRow, len(rarityEnhanceRows)) diff --git a/server/internal/model/status.go b/server/internal/model/status.go index 0f67709..2d02d6d 100644 --- a/server/internal/model/status.go +++ b/server/internal/model/status.go @@ -21,3 +21,11 @@ const ( CostumeAwakenEffectTypeAbility CostumeAwakenEffectType = 2 CostumeAwakenEffectTypeItemAcquire CostumeAwakenEffectType = 3 ) + +type WeaponAwakenEffectType int32 + +const ( + WeaponAwakenEffectTypeUnknown WeaponAwakenEffectType = 0 + WeaponAwakenEffectTypeStatusUp WeaponAwakenEffectType = 1 + WeaponAwakenEffectTypeAbility WeaponAwakenEffectType = 2 +) diff --git a/server/internal/service/weapon.go b/server/internal/service/weapon.go index f87c4c6..fb13c0d 100644 --- a/server/internal/service/weapon.go +++ b/server/internal/service/weapon.go @@ -18,6 +18,7 @@ var weaponDiffTables = []string{ "IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", + "IUserWeaponAwaken", "IUserMaterial", "IUserConsumableItem", } @@ -26,11 +27,19 @@ var limitBreakDiffTables = []string{ "IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", + "IUserWeaponAwaken", "IUserMaterial", "IUserConsumableItem", "IUserWeaponNote", } +var weaponAwakenDiffTables = []string{ + "IUserWeapon", + "IUserWeaponAwaken", + "IUserMaterial", + "IUserConsumableItem", +} + type WeaponServiceServer struct { pb.UnimplementedWeaponServiceServer users store.UserRepository @@ -176,7 +185,8 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p tracker := userdata.NewDeleteTracker(). Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}). Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}) + Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). + Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { totalGold := int32(0) @@ -206,6 +216,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p delete(user.Weapons, uuid) delete(user.WeaponSkills, uuid) delete(user.WeaponAbilities, uuid) + delete(user.WeaponAwakens, uuid) } if totalGold > 0 { @@ -217,7 +228,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p return nil, fmt.Errorf("weapon sell: %w", err) } - sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserConsumableItem"} + sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserWeaponAwaken", "IUserConsumableItem"} tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), sellDiffTables) diff := tracker.Apply(snapshot, tables) @@ -583,7 +594,8 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li tracker := userdata.NewDeleteTracker(). Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}). Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}) + Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). + Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { weapon, ok := user.Weapons[req.UserWeaponUuid] @@ -626,6 +638,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li delete(user.Weapons, uuid) delete(user.WeaponSkills, uuid) delete(user.WeaponAbilities, uuid) + delete(user.WeaponAwakens, uuid) consumedCount++ } @@ -668,7 +681,8 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan tracker := userdata.NewDeleteTracker(). Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}). Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). - Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}) + Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"}). + Track("IUserWeaponAwaken", oldUser, userdata.SortedWeaponAwakenRecords, []string{"userId", "userWeaponUuid"}) var changedStoryIds []int32 snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { @@ -714,6 +728,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan delete(user.Weapons, uuid) delete(user.WeaponSkills, uuid) delete(user.WeaponAbilities, uuid) + delete(user.WeaponAwakens, uuid) consumedCount++ } @@ -795,3 +810,62 @@ func (s *WeaponServiceServer) checkWeaponStoryUnlocks(user *store.UserState, wea } return nil } + +func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRequest) (*pb.WeaponAwakenResponse, error) { + log.Printf("[WeaponService] Awaken: uuid=%s", req.UserWeaponUuid) + + userId := currentUserId(ctx, s.users, s.sessions) + nowMillis := gametime.NowMillis() + + snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) { + weapon, ok := user.Weapons[req.UserWeaponUuid] + if !ok { + log.Printf("[WeaponService] Awaken: weapon uuid=%s not found", req.UserWeaponUuid) + return + } + + awakenRow, ok := s.catalog.AwakenByWeaponId[weapon.WeaponId] + if !ok { + log.Printf("[WeaponService] Awaken: no awaken data for weaponId=%d", weapon.WeaponId) + return + } + + if _, already := user.WeaponAwakens[req.UserWeaponUuid]; already { + log.Printf("[WeaponService] Awaken: weapon uuid=%s already awakened", req.UserWeaponUuid) + return + } + + mats := s.catalog.AwakenMaterialsByGroupId[awakenRow.WeaponAwakenMaterialGroupId] + for _, mat := range mats { + cur := user.Materials[mat.MaterialId] + cost := mat.Count + if cur < cost { + log.Printf("[WeaponService] Awaken: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost) + cost = cur + } + user.Materials[mat.MaterialId] = cur - cost + } + + if awakenRow.ConsumeGold > 0 { + user.ConsumableItems[s.config.ConsumableItemIdForGold] -= awakenRow.ConsumeGold + log.Printf("[WeaponService] Awaken: gold cost=%d", awakenRow.ConsumeGold) + } + + user.WeaponAwakens[req.UserWeaponUuid] = store.WeaponAwakenState{ + UserWeaponUuid: req.UserWeaponUuid, + LatestVersion: nowMillis, + } + + weapon.LatestVersion = nowMillis + user.Weapons[req.UserWeaponUuid] = weapon + log.Printf("[WeaponService] Awaken: weaponId=%d awakened", weapon.WeaponId) + }) + if err != nil { + return nil, fmt.Errorf("weapon awaken: %w", err) + } + + tables := userdata.FullClientTableMap(snapshot) + diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponAwakenDiffTables)) + + return &pb.WeaponAwakenResponse{DiffUserData: diff}, nil +} diff --git a/server/internal/store/types.go b/server/internal/store/types.go index 2e82928..0c6cc5e 100644 --- a/server/internal/store/types.go +++ b/server/internal/store/types.go @@ -83,6 +83,7 @@ type UserState struct { CostumeActiveSkills map[string]CostumeActiveSkillState WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid + WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid DeckTypeNotes map[model.DeckType]DeckTypeNoteState WeaponNotes map[int32]WeaponNoteState DeckSubWeapons map[string][]string @@ -205,6 +206,9 @@ func (u *UserState) EnsureMaps() { if u.WeaponAbilities == nil { u.WeaponAbilities = make(map[string][]WeaponAbilityState) } + if u.WeaponAwakens == nil { + u.WeaponAwakens = make(map[string]WeaponAwakenState) + } if u.DeckTypeNotes == nil { u.DeckTypeNotes = make(map[model.DeckType]DeckTypeNoteState) } @@ -462,6 +466,11 @@ type WeaponAbilityState struct { Level int32 } +type WeaponAwakenState struct { + UserWeaponUuid string + LatestVersion int64 +} + type DeckTypeNoteState struct { DeckType model.DeckType MaxDeckPower int32 diff --git a/server/internal/userdata/proj_inventory.go b/server/internal/userdata/proj_inventory.go index 0c08707..c11be8f 100644 --- a/server/internal/userdata/proj_inventory.go +++ b/server/internal/userdata/proj_inventory.go @@ -101,13 +101,16 @@ func init() { s, _ := encodeJSONMaps(sortedCageOrnamentRewardRecords(user)...) return s }) + register("IUserWeaponAwaken", func(user store.UserState) string { + s, _ := encodeJSONMaps(SortedWeaponAwakenRecords(user)...) + return s + }) registerStatic( "IUserCostumeLevelBonusReleaseStatus", "IUserCostumeLotteryEffect", "IUserCostumeLotteryEffectAbility", "IUserCostumeLotteryEffectStatusUp", "IUserCostumeLotteryEffectPending", - "IUserWeaponAwaken", "IUserPartsPresetTag", "IUserPartsStatusSub", ) @@ -532,6 +535,20 @@ func SortedWeaponAbilityRecords(user store.UserState) []map[string]any { return records } +func SortedWeaponAwakenRecords(user store.UserState) []map[string]any { + keys := sortedStringKeys(user.WeaponAwakens) + records := make([]map[string]any, 0, len(keys)) + for _, key := range keys { + row := user.WeaponAwakens[key] + records = append(records, map[string]any{ + "userId": user.UserId, + "userWeaponUuid": row.UserWeaponUuid, + "latestVersion": row.LatestVersion, + }) + } + return records +} + func exploreRecord(user store.UserState) map[string]any { return map[string]any{ "userId": user.UserId,