mirror of Walter-Sparrow / lunar-tear
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement memoir sub-status system with level-based unlocks

+228 -1
+2
server/internal/masterdata/numericalfunc.go
··· 39 39 p[1]*value*value/1000 + 40 40 p[2]*value/1000 + 41 41 p[3] 42 + case model.NumericalFunctionTypePartsMainOption: 43 + return p[0]*value/1000 + p[1] 42 44 default: 43 45 return 0 44 46 }
+68
server/internal/masterdata/parts.go
··· 7 7 "lunar-tear/server/internal/utils" 8 8 ) 9 9 10 + type PartsStatusMainDef struct { 11 + StatusKindType int32 12 + StatusCalculationType int32 13 + StatusChangeInitialValue int32 14 + StatusNumericalFunctionId int32 15 + } 16 + 10 17 type PartsCatalog struct { 11 18 PartsById map[int32]EntityMParts 12 19 DefaultPartsStatusMainByLotteryGroup map[int32]int32 ··· 14 21 RateByGroupAndLevel map[int32]map[int32]int32 15 22 PriceByGroupAndLevel map[int32]map[int32]int32 16 23 SellPriceByRarity map[model.RarityType]NumericalFunc 24 + 25 + PartsStatusMainById map[int32]PartsStatusMainDef 26 + SubStatusPool map[int32][]int32 // lotteryGroupId -> eligible PartsStatusMainIds 27 + SubStatusUnlockLvls map[model.RarityType][]int32 // rarity -> levels where sub-slots unlock 28 + FuncResolver *FunctionResolver 17 29 } 18 30 19 31 func LoadPartsCatalog() (*PartsCatalog, error) { ··· 83 95 priceByGroupAndLevel[p.PartsLevelUpPriceGroupId][p.LevelLowerLimit] = p.Gold 84 96 } 85 97 98 + partsStatusMainById, subStatusPool := buildPartsStatusMain() 99 + 100 + unlockLvls := []int32{3, 6, 9, 12} 101 + subStatusUnlockLvls := map[model.RarityType][]int32{ 102 + model.RarityNormal: unlockLvls, 103 + model.RarityRare: unlockLvls, 104 + model.RaritySRare: unlockLvls, 105 + model.RaritySSRare: unlockLvls, 106 + } 107 + 86 108 return &PartsCatalog{ 87 109 PartsById: partsById, 88 110 DefaultPartsStatusMainByLotteryGroup: defaultPartsStatusMainByLotteryGroup, ··· 90 112 RateByGroupAndLevel: rateByGroupAndLevel, 91 113 PriceByGroupAndLevel: priceByGroupAndLevel, 92 114 SellPriceByRarity: sellPriceByRarity, 115 + PartsStatusMainById: partsStatusMainById, 116 + SubStatusPool: subStatusPool, 117 + SubStatusUnlockLvls: subStatusUnlockLvls, 118 + FuncResolver: funcResolver, 93 119 }, nil 94 120 } 121 + 122 + // buildPartsStatusMain constructs the 36 PartsStatusMain definitions and 123 + // groups them into sub-status lottery pools by tier (1-4). 124 + // The data mirrors EntityMPartsStatusMainTable.json which is structured as 125 + // 9 stat categories x 4 tiers. Tier within each category maps to the 126 + // PartsStatusSubLotteryGroupId on the part definition. 127 + func buildPartsStatusMain() (map[int32]PartsStatusMainDef, map[int32][]int32) { 128 + type statCat struct { 129 + kindType int32 130 + calcType int32 131 + initVals [4]int32 132 + funcStart int32 133 + } 134 + cats := []statCat{ 135 + {2, 1, [4]int32{50, 100, 150, 250}, 101}, // Attack flat 136 + {7, 1, [4]int32{50, 100, 150, 250}, 101}, // Vitality flat 137 + {2, 2, [4]int32{10, 30, 70, 120}, 105}, // Attack % 138 + {7, 2, [4]int32{10, 30, 70, 120}, 105}, // Vitality % 139 + {6, 2, [4]int32{10, 30, 70, 120}, 105}, // HP % 140 + {6, 1, [4]int32{600, 1200, 1800, 3000}, 109}, // HP flat 141 + {4, 1, [4]int32{10, 30, 70, 120}, 113}, // CritRatio 142 + {3, 1, [4]int32{20, 50, 80, 100}, 117}, // CritAttack 143 + {1, 1, [4]int32{10, 20, 30, 40}, 121}, // Agility 144 + } 145 + 146 + defs := make(map[int32]PartsStatusMainDef, 36) 147 + pool := map[int32][]int32{1: {}, 2: {}, 3: {}, 4: {}} 148 + id := int32(1) 149 + for _, c := range cats { 150 + for tier := 0; tier < 4; tier++ { 151 + defs[id] = PartsStatusMainDef{ 152 + StatusKindType: c.kindType, 153 + StatusCalculationType: c.calcType, 154 + StatusChangeInitialValue: c.initVals[tier], 155 + StatusNumericalFunctionId: c.funcStart + int32(tier), 156 + } 157 + pool[int32(tier+1)] = append(pool[int32(tier+1)], id) 158 + id++ 159 + } 160 + } 161 + return defs, pool 162 + }
+50
server/internal/service/parts.go
··· 59 59 gold := sellFunc.Evaluate(part.Level) 60 60 totalGold += gold 61 61 delete(user.Parts, uuid) 62 + for k := range user.PartsStatusSubs { 63 + if k.UserPartsUuid == uuid { 64 + delete(user.PartsStatusSubs, k) 65 + } 66 + } 62 67 log.Printf("[PartsService] Sell: uuid=%s partsId=%d level=%d -> %d gold", uuid, part.PartsId, part.Level, gold) 63 68 } 64 69 ··· 131 136 isSuccess = true 132 137 log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)", 133 138 part.PartsId, part.Level-1, part.Level, successRate, goldCost) 139 + 140 + s.grantSubStatuses(user, req.UserPartsUuid, part, partDef, nowMillis) 134 141 } else { 135 142 log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)", 136 143 part.PartsId, part.Level, successRate, goldCost) ··· 146 153 return &pb.PartsEnhanceResponse{ 147 154 IsSuccess: isSuccess, 148 155 }, nil 156 + } 157 + 158 + func (s *PartsServiceServer) grantSubStatuses(user *store.UserState, uuid string, part store.PartsState, partDef masterdata.EntityMParts, nowMillis int64) { 159 + unlockLevels := s.catalog.SubStatusUnlockLvls[partDef.RarityType] 160 + pool := s.catalog.SubStatusPool[partDef.PartsStatusSubLotteryGroupId] 161 + if len(pool) == 0 { 162 + return 163 + } 164 + 165 + for slotIdx, lvl := range unlockLevels { 166 + if part.Level != lvl { 167 + continue 168 + } 169 + statusIndex := int32(slotIdx + 1) 170 + key := store.PartsStatusSubKey{UserPartsUuid: uuid, StatusIndex: statusIndex} 171 + if _, exists := user.PartsStatusSubs[key]; exists { 172 + continue 173 + } 174 + 175 + pick := pool[rand.Intn(len(pool))] 176 + def, ok := s.catalog.PartsStatusMainById[pick] 177 + if !ok { 178 + continue 179 + } 180 + 181 + statusValue := def.StatusChangeInitialValue 182 + if f, ok := s.catalog.FuncResolver.Resolve(def.StatusNumericalFunctionId); ok { 183 + statusValue = f.Evaluate(part.Level) 184 + } 185 + 186 + user.PartsStatusSubs[key] = store.PartsStatusSubState{ 187 + UserPartsUuid: uuid, 188 + StatusIndex: statusIndex, 189 + PartsStatusSubLotteryId: pick, 190 + Level: part.Level, 191 + StatusKindType: def.StatusKindType, 192 + StatusCalculationType: def.StatusCalculationType, 193 + StatusChangeValue: statusValue, 194 + LatestVersion: nowMillis, 195 + } 196 + log.Printf("[PartsService] Enhance: granted sub-status slot=%d lotteryId=%d kind=%d calc=%d val=%d", 197 + statusIndex, pick, def.StatusKindType, def.StatusCalculationType, statusValue) 198 + } 149 199 } 150 200 151 201 func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsReplacePresetRequest) (*pb.PartsReplacePresetResponse, error) {
+1
server/internal/store/clone.go
··· 30 30 out.Parts = maps.Clone(u.Parts) 31 31 out.PartsGroupNotes = maps.Clone(u.PartsGroupNotes) 32 32 out.PartsPresets = maps.Clone(u.PartsPresets) 33 + out.PartsStatusSubs = maps.Clone(u.PartsStatusSubs) 33 34 out.ImportantItems = maps.Clone(u.ImportantItems) 34 35 out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills) 35 36 out.WeaponSkills = cloneSliceMap(u.WeaponSkills)
+1
server/internal/store/seed.go
··· 129 129 Parts: make(map[string]PartsState), 130 130 PartsGroupNotes: make(map[int32]PartsGroupNoteState), 131 131 PartsPresets: make(map[int32]PartsPresetState), 132 + PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState), 132 133 ImportantItems: make(map[int32]int32), 133 134 CostumeActiveSkills: make(map[string]CostumeActiveSkillState), 134 135 WeaponSkills: make(map[string][]WeaponSkillState),
+11
server/internal/store/sqlite/load.go
··· 60 60 u.Parts = make(map[string]store.PartsState) 61 61 u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState) 62 62 u.PartsPresets = make(map[int32]store.PartsPresetState) 63 + u.PartsStatusSubs = make(map[store.PartsStatusSubKey]store.PartsStatusSubState) 63 64 u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState) 64 65 u.ConsumableItems = make(map[int32]int32) 65 66 u.Materials = make(map[int32]int32) ··· 451 452 rows.Scan(&v.UserPartsPresetNumber, &v.UserPartsUuid01, &v.UserPartsUuid02, &v.UserPartsUuid03, 452 453 &v.Name, &v.UserPartsPresetTagNumber, &v.LatestVersion) 453 454 u.PartsPresets[v.UserPartsPresetNumber] = v 455 + }) 456 + 457 + queryRows(db, `SELECT user_parts_uuid, status_index, parts_status_sub_lottery_id, level, 458 + status_kind_type, status_calculation_type, status_change_value, latest_version 459 + FROM user_parts_status_subs WHERE user_id=?`, uid, 460 + func(rows *sql.Rows) { 461 + var v store.PartsStatusSubState 462 + rows.Scan(&v.UserPartsUuid, &v.StatusIndex, &v.PartsStatusSubLotteryId, &v.Level, 463 + &v.StatusKindType, &v.StatusCalculationType, &v.StatusChangeValue, &v.LatestVersion) 464 + u.PartsStatusSubs[store.PartsStatusSubKey{UserPartsUuid: v.UserPartsUuid, StatusIndex: v.StatusIndex}] = v 454 465 }) 455 466 456 467 queryRows(db, `SELECT deck_type, max_deck_power, latest_version FROM user_deck_type_notes WHERE user_id=?`, uid,
+18
server/internal/store/sqlite/save.go
··· 290 290 return err 291 291 } 292 292 } 293 + for _, v := range u.PartsStatusSubs { 294 + if err := exec(`INSERT INTO user_parts_status_subs (user_id, user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`, 295 + uid, v.UserPartsUuid, v.StatusIndex, v.PartsStatusSubLotteryId, v.Level, v.StatusKindType, v.StatusCalculationType, v.StatusChangeValue, v.LatestVersion); err != nil { 296 + return err 297 + } 298 + } 293 299 for _, v := range u.DeckTypeNotes { 294 300 if err := exec(`INSERT INTO user_deck_type_notes (user_id, deck_type, max_deck_power, latest_version) VALUES (?,?,?,?)`, 295 301 uid, int32(v.DeckType), v.MaxDeckPower, v.LatestVersion); err != nil { ··· 807 813 func(v store.PartsPresetState) []any { 808 814 return []any{v.UserPartsPresetNumber, v.UserPartsUuid01, v.UserPartsUuid02, v.UserPartsUuid03, v.Name, v.UserPartsPresetTagNumber, v.LatestVersion} 809 815 }, "user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03, name, user_parts_preset_tag_number, latest_version") 816 + 817 + for k, v := range after.PartsStatusSubs { 818 + if old, ok := before.PartsStatusSubs[k]; !ok || old != v { 819 + exec(`INSERT OR REPLACE INTO user_parts_status_subs (user_id, user_parts_uuid, status_index, parts_status_sub_lottery_id, level, status_kind_type, status_calculation_type, status_change_value, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`, 820 + uid, k.UserPartsUuid, k.StatusIndex, v.PartsStatusSubLotteryId, v.Level, v.StatusKindType, v.StatusCalculationType, v.StatusChangeValue, v.LatestVersion) 821 + } 822 + } 823 + for k := range before.PartsStatusSubs { 824 + if _, ok := after.PartsStatusSubs[k]; !ok { 825 + exec(`DELETE FROM user_parts_status_subs WHERE user_id=? AND user_parts_uuid=? AND status_index=?`, uid, k.UserPartsUuid, k.StatusIndex) 826 + } 827 + } 810 828 811 829 // Deck type notes (key is model.DeckType which is int32-based) 812 830 for k, v := range after.DeckTypeNotes {
+1
server/internal/store/sqlite/user.go
··· 123 123 "user_deck_sub_weapons", 124 124 "user_decks", 125 125 "user_deck_characters", 126 + "user_parts_status_subs", 126 127 "user_parts_presets", 127 128 "user_parts_group_notes", 128 129 "user_parts",
+20
server/internal/store/types.go
··· 80 80 Parts map[string]PartsState 81 81 PartsGroupNotes map[int32]PartsGroupNoteState 82 82 PartsPresets map[int32]PartsPresetState 83 + PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState 83 84 ImportantItems map[int32]int32 84 85 CostumeActiveSkills map[string]CostumeActiveSkillState 85 86 WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid ··· 196 197 } 197 198 if u.PartsPresets == nil { 198 199 u.PartsPresets = make(map[int32]PartsPresetState) 200 + } 201 + if u.PartsStatusSubs == nil { 202 + u.PartsStatusSubs = make(map[PartsStatusSubKey]PartsStatusSubState) 199 203 } 200 204 if u.ImportantItems == nil { 201 205 u.ImportantItems = make(map[int32]int32) ··· 831 835 Name string 832 836 UserPartsPresetTagNumber int32 833 837 LatestVersion int64 838 + } 839 + 840 + type PartsStatusSubKey struct { 841 + UserPartsUuid string 842 + StatusIndex int32 843 + } 844 + 845 + type PartsStatusSubState struct { 846 + UserPartsUuid string 847 + StatusIndex int32 848 + PartsStatusSubLotteryId int32 849 + Level int32 850 + StatusKindType int32 851 + StatusCalculationType int32 852 + StatusChangeValue int32 853 + LatestVersion int64 834 854 } 835 855 836 856 type NotificationState struct {
+5
server/internal/userdata/changed_tables.go
··· 161 161 if !mapsEqualStruct(before.PartsPresets, after.PartsPresets) { 162 162 add("IUserPartsPreset") 163 163 } 164 + if !mapsEqualStruct(before.PartsStatusSubs, after.PartsStatusSubs) { 165 + add("IUserPartsStatusSub") 166 + } 164 167 if !mapsEqualStruct(before.CostumeActiveSkills, after.CostumeActiveSkills) { 165 168 add("IUserCostumeActiveSkill") 166 169 } ··· 348 351 return []string{"userId", "userThoughtUuid"} 349 352 case "IUserParts": 350 353 return []string{"userId", "userPartsUuid"} 354 + case "IUserPartsStatusSub": 355 + return []string{"userId", "userPartsUuid", "statusIndex"} 351 356 case "IUserDeckCharacter": 352 357 return []string{"userId", "userDeckCharacterUuid"} 353 358 case "IUserDeck":
+33 -1
server/internal/userdata/proj_inventory.go
··· 114 114 s, _ := utils.EncodeJSONMaps(SortedCostumeLotteryEffectPendingRecords(user)...) 115 115 return s 116 116 }) 117 + register("IUserPartsStatusSub", func(user store.UserState) string { 118 + s, _ := utils.EncodeJSONMaps(sortedPartsStatusSubRecords(user)...) 119 + return s 120 + }) 117 121 registerStatic( 118 122 "IUserCostumeLevelBonusReleaseStatus", 119 123 "IUserCostumeLotteryEffectAbility", 120 124 "IUserCostumeLotteryEffectStatusUp", 121 125 "IUserPartsPresetTag", 122 - "IUserPartsStatusSub", 123 126 ) 124 127 } 125 128 ··· 488 491 "name": row.Name, 489 492 "userPartsPresetTagNumber": row.UserPartsPresetTagNumber, 490 493 "latestVersion": row.LatestVersion, 494 + }) 495 + } 496 + return records 497 + } 498 + 499 + func sortedPartsStatusSubRecords(user store.UserState) []map[string]any { 500 + keys := make([]store.PartsStatusSubKey, 0, len(user.PartsStatusSubs)) 501 + for k := range user.PartsStatusSubs { 502 + keys = append(keys, k) 503 + } 504 + sort.Slice(keys, func(i, j int) bool { 505 + if keys[i].UserPartsUuid != keys[j].UserPartsUuid { 506 + return keys[i].UserPartsUuid < keys[j].UserPartsUuid 507 + } 508 + return keys[i].StatusIndex < keys[j].StatusIndex 509 + }) 510 + records := make([]map[string]any, 0, len(keys)) 511 + for _, k := range keys { 512 + row := user.PartsStatusSubs[k] 513 + records = append(records, map[string]any{ 514 + "userId": user.UserId, 515 + "userPartsUuid": row.UserPartsUuid, 516 + "statusIndex": row.StatusIndex, 517 + "partsStatusSubLotteryId": row.PartsStatusSubLotteryId, 518 + "level": row.Level, 519 + "statusKindType": row.StatusKindType, 520 + "statusCalculationType": row.StatusCalculationType, 521 + "statusChangeValue": row.StatusChangeValue, 522 + "latestVersion": row.LatestVersion, 491 523 }) 492 524 } 493 525 return records
+18
server/migrations/20260422100818_add_parts_status_subs.sql
··· 1 + -- +goose Up 2 + CREATE TABLE user_parts_status_subs ( 3 + user_id INTEGER NOT NULL REFERENCES users(user_id), 4 + user_parts_uuid TEXT NOT NULL, 5 + status_index INTEGER NOT NULL, 6 + parts_status_sub_lottery_id INTEGER NOT NULL DEFAULT 0, 7 + level INTEGER NOT NULL DEFAULT 0, 8 + status_kind_type INTEGER NOT NULL DEFAULT 0, 9 + status_calculation_type INTEGER NOT NULL DEFAULT 0, 10 + status_change_value INTEGER NOT NULL DEFAULT 0, 11 + latest_version INTEGER NOT NULL DEFAULT 0, 12 + PRIMARY KEY (user_id, user_parts_uuid, status_index) 13 + ); 14 + 15 + UPDATE user_parts SET level = 1; 16 + 17 + -- +goose Down 18 + DROP TABLE IF EXISTS user_parts_status_subs;