Skip to content

Commit 25cbe86

Browse files
Implement Tower accumulation reward claiming
1 parent 1dc5b8f commit 25cbe86

13 files changed

Lines changed: 272 additions & 66 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package masterdata
2+
3+
import (
4+
"log"
5+
"sort"
6+
7+
"lunar-tear/server/internal/utils"
8+
)
9+
10+
type TowerTier struct {
11+
QuestMissionClearCount int32
12+
Rewards []RewardItem
13+
}
14+
15+
type TowerCatalog struct {
16+
TiersByChapter map[int32][]TowerTier
17+
}
18+
19+
func (c *TowerCatalog) CollectRewards(chapterId, oldCount, targetCount int32) ([]RewardItem, int32) {
20+
var items []RewardItem
21+
highest := int32(0)
22+
for _, t := range c.TiersByChapter[chapterId] {
23+
if t.QuestMissionClearCount > oldCount && t.QuestMissionClearCount <= targetCount {
24+
items = append(items, t.Rewards...)
25+
if t.QuestMissionClearCount > highest {
26+
highest = t.QuestMissionClearCount
27+
}
28+
}
29+
}
30+
return items, highest
31+
}
32+
33+
func LoadTowerCatalog() *TowerCatalog {
34+
// chapterId -> accumulation reward group id
35+
accumRewardRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationReward]("m_event_quest_tower_accumulation_reward")
36+
if err != nil {
37+
log.Fatalf("load event quest tower accumulation reward table: %v", err)
38+
}
39+
groupByChapter := make(map[int32]int32, len(accumRewardRows))
40+
for _, r := range accumRewardRows {
41+
groupByChapter[r.EventQuestChapterId] = r.EventQuestTowerAccumulationRewardGroupId
42+
}
43+
44+
// reward group id -> reward items
45+
rewardGroupRows, err := utils.ReadTable[EntityMEventQuestTowerRewardGroup]("m_event_quest_tower_reward_group")
46+
if err != nil {
47+
log.Fatalf("load event quest tower reward group table: %v", err)
48+
}
49+
itemsByRewardGroup := make(map[int32][]RewardItem)
50+
for _, r := range rewardGroupRows {
51+
itemsByRewardGroup[r.EventQuestTowerRewardGroupId] = append(itemsByRewardGroup[r.EventQuestTowerRewardGroupId], RewardItem{
52+
PossessionType: r.PossessionType,
53+
PossessionId: r.PossessionId,
54+
Count: r.Count,
55+
})
56+
}
57+
58+
// accumulation group id -> tiers (threshold + resolved reward items)
59+
accumGroupRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationRewardGroup]("m_event_quest_tower_accumulation_reward_group")
60+
if err != nil {
61+
log.Fatalf("load event quest tower accumulation reward group table: %v", err)
62+
}
63+
tiersByGroup := make(map[int32][]TowerTier)
64+
for _, r := range accumGroupRows {
65+
tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId] = append(tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId], TowerTier{
66+
QuestMissionClearCount: r.QuestMissionClearCount,
67+
Rewards: itemsByRewardGroup[r.EventQuestTowerRewardGroupId],
68+
})
69+
}
70+
71+
// resolve per-chapter, sorted ascending by threshold
72+
tiersByChapter := make(map[int32][]TowerTier, len(groupByChapter))
73+
for chapterId, groupId := range groupByChapter {
74+
tiers := tiersByGroup[groupId]
75+
sort.Slice(tiers, func(i, j int) bool {
76+
return tiers[i].QuestMissionClearCount < tiers[j].QuestMissionClearCount
77+
})
78+
tiersByChapter[chapterId] = tiers
79+
}
80+
81+
log.Printf("tower catalog loaded: %d chapters", len(tiersByChapter))
82+
83+
return &TowerCatalog{TiersByChapter: tiersByChapter}
84+
}

server/internal/runtime/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ func buildCatalogs() (*Catalogs, error) {
139139

140140
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
141141

142+
towerCatalog := masterdata.LoadTowerCatalog()
143+
142144
return &Catalogs{
143145
GameConfig: gameConfig,
144146
Parts: partsCatalog,
@@ -164,6 +166,7 @@ func buildCatalogs() (*Catalogs, error) {
164166
Companion: companionCatalog,
165167
SideStory: sideStoryCatalog,
166168
BigHunt: bigHuntCatalog,
169+
Tower: towerCatalog,
167170
QuestHandler: questHandler,
168171
GachaHandler: gachaHandler,
169172
}, nil

server/internal/runtime/holder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Catalogs struct {
5050
Companion *masterdata.CompanionCatalog
5151
SideStory *masterdata.SideStoryCatalog
5252
BigHunt *masterdata.BigHuntCatalog
53+
Tower *masterdata.TowerCatalog
5354

5455
// Catalog-derived handlers must rebuild on every reload because they
5556
// embed/cache pointers to specific catalog instances.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
pb "lunar-tear/server/gen/proto"
8+
"lunar-tear/server/internal/gametime"
9+
"lunar-tear/server/internal/model"
10+
"lunar-tear/server/internal/store"
11+
)
12+
13+
func (s *QuestServiceServer) ReceiveTowerAccumulationReward(ctx context.Context, req *pb.ReceiveTowerAccumulationRewardRequest) (*pb.ReceiveTowerAccumulationRewardResponse, error) {
14+
log.Printf("[QuestService] ReceiveTowerAccumulationReward: eventQuestChapterId=%d targetMissionClearCount=%d",
15+
req.EventQuestChapterId, req.TargetMissionClearCount)
16+
17+
cat := s.holder.Get()
18+
tower := cat.Tower
19+
granter := cat.QuestHandler.Granter
20+
21+
userId := CurrentUserId(ctx, s.users, s.sessions)
22+
nowMillis := gametime.NowMillis()
23+
24+
s.users.UpdateUser(userId, func(user *store.UserState) {
25+
rec := user.TowerAccumulationRewards[req.EventQuestChapterId]
26+
old := rec.LatestRewardReceiveQuestMissionClearCount
27+
28+
items, highest := tower.CollectRewards(req.EventQuestChapterId, old, req.TargetMissionClearCount)
29+
if highest <= old {
30+
log.Printf("[QuestService] ReceiveTowerAccumulationReward: nothing to grant for chapter=%d (claimed=%d, target=%d)",
31+
req.EventQuestChapterId, old, req.TargetMissionClearCount)
32+
return
33+
}
34+
35+
for _, it := range items {
36+
granter.GrantFull(user, model.PossessionType(it.PossessionType), it.PossessionId, it.Count, nowMillis)
37+
}
38+
39+
rec.EventQuestChapterId = req.EventQuestChapterId
40+
rec.LatestRewardReceiveQuestMissionClearCount = highest
41+
rec.LatestVersion = nowMillis
42+
user.TowerAccumulationRewards[req.EventQuestChapterId] = rec
43+
44+
log.Printf("[QuestService] ReceiveTowerAccumulationReward: chapter=%d granted %d item(s), claimed %d -> %d",
45+
req.EventQuestChapterId, len(items), old, highest)
46+
})
47+
48+
return &pb.ReceiveTowerAccumulationRewardResponse{}, nil
49+
}

server/internal/store/clone.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func CloneUserState(u UserState) UserState {
2626
Unlocks: maps.Clone(u.Gimmick.Unlocks),
2727
}
2828
out.CageOrnamentRewards = maps.Clone(u.CageOrnamentRewards)
29+
out.TowerAccumulationRewards = maps.Clone(u.TowerAccumulationRewards)
2930
out.ConsumableItems = maps.Clone(u.ConsumableItems)
3031
out.Materials = maps.Clone(u.Materials)
3132
out.Parts = maps.Clone(u.Parts)

server/internal/store/seed.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -123,30 +123,31 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
123123
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
124124
Unlocks: make(map[GimmickKey]GimmickUnlockState),
125125
},
126-
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
127-
ConsumableItems: make(map[int32]int32),
128-
Materials: make(map[int32]int32),
129-
Thoughts: make(map[string]ThoughtState),
130-
Parts: make(map[string]PartsState),
131-
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
132-
PartsPresets: make(map[int32]PartsPresetState),
133-
PartsPresetTags: make(map[int32]PartsPresetTagState),
134-
PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState),
135-
ImportantItems: make(map[int32]int32),
136-
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
137-
WeaponSkills: make(map[string][]WeaponSkillState),
138-
WeaponAbilities: make(map[string][]WeaponAbilityState),
139-
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
140-
WeaponNotes: make(map[int32]WeaponNoteState),
141-
NaviCutInPlayed: make(map[int32]bool),
142-
ViewedMovies: make(map[int32]int64),
143-
ContentsStories: make(map[int32]int64),
144-
DrawnOmikuji: make(map[int32]int64),
145-
PremiumItems: make(map[int32]int64),
146-
DokanConfirmed: make(map[int32]bool),
147-
ShopItems: make(map[int32]UserShopItemState),
148-
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
149-
ExploreScores: make(map[int32]ExploreScoreState),
126+
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
127+
TowerAccumulationRewards: make(map[int32]TowerAccumulationRewardState),
128+
ConsumableItems: make(map[int32]int32),
129+
Materials: make(map[int32]int32),
130+
Thoughts: make(map[string]ThoughtState),
131+
Parts: make(map[string]PartsState),
132+
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
133+
PartsPresets: make(map[int32]PartsPresetState),
134+
PartsPresetTags: make(map[int32]PartsPresetTagState),
135+
PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState),
136+
ImportantItems: make(map[int32]int32),
137+
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
138+
WeaponSkills: make(map[string][]WeaponSkillState),
139+
WeaponAbilities: make(map[string][]WeaponAbilityState),
140+
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
141+
WeaponNotes: make(map[int32]WeaponNoteState),
142+
NaviCutInPlayed: make(map[int32]bool),
143+
ViewedMovies: make(map[int32]int64),
144+
ContentsStories: make(map[int32]int64),
145+
DrawnOmikuji: make(map[int32]int64),
146+
PremiumItems: make(map[int32]int64),
147+
DokanConfirmed: make(map[int32]bool),
148+
ShopItems: make(map[int32]UserShopItemState),
149+
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
150+
ExploreScores: make(map[int32]ExploreScoreState),
150151

151152
CharacterBoards: make(map[int32]CharacterBoardState),
152153
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),

server/internal/store/sqlite/load.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func initMaps(u *store.UserState) {
7777
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
7878
u.ExploreScores = make(map[int32]store.ExploreScoreState)
7979
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
80+
u.TowerAccumulationRewards = make(map[int32]store.TowerAccumulationRewardState)
8081
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
8182
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
8283
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
@@ -651,6 +652,13 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
651652
u.CageOrnamentRewards[v.CageOrnamentId] = v
652653
})
653654

655+
queryRows(db, `SELECT event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version
656+
FROM user_event_quest_tower_accumulation_rewards WHERE user_id=?`, uid, func(rows *sql.Rows) {
657+
var v store.TowerAccumulationRewardState
658+
rows.Scan(&v.EventQuestChapterId, &v.LatestRewardReceiveQuestMissionClearCount, &v.LatestVersion)
659+
u.TowerAccumulationRewards[v.EventQuestChapterId] = v
660+
})
661+
654662
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
655663
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
656664
var v store.UserShopItemState

server/internal/store/sqlite/save.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
454454
return err
455455
}
456456
}
457+
for _, v := range u.TowerAccumulationRewards {
458+
if err := exec(`INSERT INTO user_event_quest_tower_accumulation_rewards (user_id, event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version) VALUES (?,?,?,?)`,
459+
uid, v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion); err != nil {
460+
return err
461+
}
462+
}
457463
for _, v := range u.ShopItems {
458464
if err := exec(`INSERT INTO user_shop_items (user_id, shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version) VALUES (?,?,?,?,?)`,
459465
uid, v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion); err != nil {
@@ -994,6 +1000,11 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
9941000
return []any{v.CageOrnamentId, v.AcquisitionDatetime, v.LatestVersion}
9951001
},
9961002
"cage_ornament_id, acquisition_datetime, latest_version")
1003+
diffMapInt32(tx, uid, before.TowerAccumulationRewards, after.TowerAccumulationRewards, "user_event_quest_tower_accumulation_rewards", "event_quest_chapter_id",
1004+
func(v store.TowerAccumulationRewardState) []any {
1005+
return []any{v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion}
1006+
},
1007+
"event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version")
9971008
diffMapInt32(tx, uid, before.ShopItems, after.ShopItems, "user_shop_items", "shop_item_id",
9981009
func(v store.UserShopItemState) []any {
9991010
return []any{v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion}

server/internal/store/sqlite/user.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
7979

8080
// Child tables in reverse-dependency order (matches schema's goose Down).
8181
childTables := []string{
82+
"user_event_quest_tower_accumulation_rewards",
8283
"user_cage_ornament_rewards",
8384
"user_shop_replaceable_lineup",
8485
"user_shop_items",

server/internal/store/types.go

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -63,47 +63,48 @@ type UserState struct {
6363
Gacha GachaState
6464
Notifications NotificationState
6565

66-
Characters map[int32]CharacterState
67-
Costumes map[string]CostumeState
68-
Weapons map[string]WeaponState
69-
Companions map[string]CompanionState
70-
Thoughts map[string]ThoughtState
71-
DeckCharacters map[string]DeckCharacterState
72-
Decks map[DeckKey]DeckState
73-
TripleDecks map[DeckKey]TripleDeckState
74-
Quests map[int32]UserQuestState
75-
QuestMissions map[QuestMissionKey]UserQuestMissionState
76-
Missions map[int32]UserMissionState
77-
WeaponStories map[int32]WeaponStoryState
78-
Gimmick GimmickState
79-
CageOrnamentRewards map[int32]CageOrnamentRewardState
80-
ConsumableItems map[int32]int32
81-
Materials map[int32]int32
82-
Parts map[string]PartsState
83-
PartsGroupNotes map[int32]PartsGroupNoteState
84-
PartsPresets map[int32]PartsPresetState
85-
PartsPresetTags map[int32]PartsPresetTagState
86-
PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState
87-
ImportantItems map[int32]int32
88-
CostumeActiveSkills map[string]CostumeActiveSkillState
89-
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
90-
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
91-
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
92-
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
93-
WeaponNotes map[int32]WeaponNoteState
94-
DeckSubWeapons map[string][]string
95-
DeckParts map[string][]string
96-
NaviCutInPlayed map[int32]bool
97-
ViewedMovies map[int32]int64
98-
ContentsStories map[int32]int64
99-
DrawnOmikuji map[int32]int64
100-
PremiumItems map[int32]int64
101-
DokanConfirmed map[int32]bool
102-
PortalCageStatus PortalCageStatusState
103-
GuerrillaFreeOpen GuerrillaFreeOpenState
104-
ShopItems map[int32]UserShopItemState
105-
ShopReplaceable UserShopReplaceableState
106-
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
66+
Characters map[int32]CharacterState
67+
Costumes map[string]CostumeState
68+
Weapons map[string]WeaponState
69+
Companions map[string]CompanionState
70+
Thoughts map[string]ThoughtState
71+
DeckCharacters map[string]DeckCharacterState
72+
Decks map[DeckKey]DeckState
73+
TripleDecks map[DeckKey]TripleDeckState
74+
Quests map[int32]UserQuestState
75+
QuestMissions map[QuestMissionKey]UserQuestMissionState
76+
Missions map[int32]UserMissionState
77+
WeaponStories map[int32]WeaponStoryState
78+
Gimmick GimmickState
79+
CageOrnamentRewards map[int32]CageOrnamentRewardState
80+
TowerAccumulationRewards map[int32]TowerAccumulationRewardState
81+
ConsumableItems map[int32]int32
82+
Materials map[int32]int32
83+
Parts map[string]PartsState
84+
PartsGroupNotes map[int32]PartsGroupNoteState
85+
PartsPresets map[int32]PartsPresetState
86+
PartsPresetTags map[int32]PartsPresetTagState
87+
PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState
88+
ImportantItems map[int32]int32
89+
CostumeActiveSkills map[string]CostumeActiveSkillState
90+
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
91+
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
92+
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
93+
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
94+
WeaponNotes map[int32]WeaponNoteState
95+
DeckSubWeapons map[string][]string
96+
DeckParts map[string][]string
97+
NaviCutInPlayed map[int32]bool
98+
ViewedMovies map[int32]int64
99+
ContentsStories map[int32]int64
100+
DrawnOmikuji map[int32]int64
101+
PremiumItems map[int32]int64
102+
DokanConfirmed map[int32]bool
103+
PortalCageStatus PortalCageStatusState
104+
GuerrillaFreeOpen GuerrillaFreeOpenState
105+
ShopItems map[int32]UserShopItemState
106+
ShopReplaceable UserShopReplaceableState
107+
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
107108

108109
Explore ExploreState
109110
ExploreScores map[int32]ExploreScoreState
@@ -192,6 +193,9 @@ func (u *UserState) EnsureMaps() {
192193
if u.CageOrnamentRewards == nil {
193194
u.CageOrnamentRewards = make(map[int32]CageOrnamentRewardState)
194195
}
196+
if u.TowerAccumulationRewards == nil {
197+
u.TowerAccumulationRewards = make(map[int32]TowerAccumulationRewardState)
198+
}
195199
if u.ConsumableItems == nil {
196200
u.ConsumableItems = make(map[int32]int32)
197201
}
@@ -868,6 +872,12 @@ type CageOrnamentRewardState struct {
868872
LatestVersion int64
869873
}
870874

875+
type TowerAccumulationRewardState struct {
876+
EventQuestChapterId int32
877+
LatestRewardReceiveQuestMissionClearCount int32
878+
LatestVersion int64
879+
}
880+
871881
type PartsState struct {
872882
UserPartsUuid string
873883
PartsId int32

0 commit comments

Comments
 (0)