From ef69c54949dbe5410dbe06aad891fd905e107db2 Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Thu, 21 May 2026 17:49:19 +0300 Subject: [PATCH] Pair gacha costume bonuses via curated lookup table --- .../masterdata/costume_weapon_pairings.go | 286 ++++++++++++++++++ server/internal/masterdata/gacha_pool.go | 75 +---- server/internal/model/gimmick.go | 12 +- server/internal/questflow/quest.go | 2 +- 4 files changed, 298 insertions(+), 77 deletions(-) create mode 100644 server/internal/masterdata/costume_weapon_pairings.go diff --git a/server/internal/masterdata/costume_weapon_pairings.go b/server/internal/masterdata/costume_weapon_pairings.go new file mode 100644 index 0000000..65c0a1f --- /dev/null +++ b/server/internal/masterdata/costume_weapon_pairings.go @@ -0,0 +1,286 @@ +package masterdata + +// Source: based on public_costumes_link_export_2026-05-21_142314.csv by @Keziah, +// with each weapon resolved to its m_weapon_evolution_group EvolutionOrder=1 root +var costumeWeaponPairings = map[int32]int32{ + 10100: 101001, + 10101: 101011, + 10102: 101021, + 10103: 101031, + 10104: 101041, + 10105: 101051, + 10106: 101061, + 10107: 101071, + 10108: 101081, + 10109: 101091, + 10110: 101101, + 10111: 101121, + 10112: 101111, + 10113: 101131, + 10114: 101141, + 10115: 101151, + 10116: 101161, + 10117: 101171, + 10118: 101181, + 10119: 101191, + 10120: 101201, + 10121: 101211, + 21000: 210161, + 21001: 210031, + 21002: 210171, + 21003: 210181, + 21004: 210191, + 21005: 210271, + 22000: 220081, + 22001: 220021, + 22002: 220051, + 22003: 220061, + 22004: 220141, + 22005: 220161, + 22006: 220181, + 22007: 220191, + 22008: 220211, + 22009: 220231, + 22010: 220241, + 23000: 230001, + 23001: 230021, + 23004: 230051, + 23005: 230151, + 23006: 230261, + 23007: 230271, + 23008: 230281, + 24000: 240091, + 24001: 240121, + 24002: 240131, + 24003: 240011, + 24004: 240081, + 24005: 240201, + 24006: 240221, + 24007: 240241, + 24008: 240271, + 24009: 240311, + 25000: 250121, + 25001: 250071, + 25002: 250011, + 25003: 250151, + 25005: 250021, + 25006: 250171, + 25007: 250221, + 25008: 250231, + 25009: 250261, + 31000: 310081, + 31001: 310061, + 31002: 310021, + 31004: 310191, + 31005: 310211, + 31008: 310221, + 31009: 310241, + 31010: 310261, + 31011: 310291, + 31013: 310321, + 31014: 310331, + 31015: 310371, + 31016: 310401, + 31017: 310411, + 31018: 310421, + 31019: 310431, + 31020: 310461, + 31021: 310471, + 31022: 310481, + 31023: 310511, + 31024: 310531, + 31025: 310541, + 31026: 310551, + 31027: 310571, + 31028: 310591, + 31029: 310621, + 31030: 310641, + 31031: 310661, + 31032: 310691, + 31033: 310701, + 31034: 310711, + 32000: 320081, + 32001: 320041, + 32002: 320011, + 32003: 320111, + 32004: 320051, + 32005: 320141, + 32006: 320151, + 32007: 320171, + 32008: 320181, + 32009: 320201, + 32011: 320231, + 32012: 320241, + 32013: 320271, + 32014: 320281, + 32015: 320301, + 32016: 320331, + 32017: 320351, + 32018: 320371, + 32019: 320381, + 32020: 320391, + 32021: 320421, + 32022: 320431, + 32023: 320441, + 32024: 320451, + 32025: 320461, + 32026: 320471, + 32027: 320501, + 32028: 320531, + 32029: 320541, + 32030: 320551, + 32031: 320561, + 32032: 320581, + 32033: 320601, + 32034: 320611, + 32035: 320621, + 32036: 320641, + 33000: 330001, + 33001: 330121, + 33002: 330011, + 33003: 330021, + 33005: 330161, + 33006: 330171, + 33007: 330191, + 33009: 330211, + 33010: 330231, + 33011: 330261, + 33012: 330281, + 33013: 330321, + 33014: 330341, + 33015: 330381, + 33016: 330401, + 33017: 330421, + 33018: 330451, + 33019: 330471, + 33020: 330501, + 33021: 330521, + 33022: 330541, + 33023: 330551, + 33024: 330561, + 33025: 330571, + 33026: 330581, + 33027: 330591, + 33028: 330601, + 33029: 330631, + 33030: 330641, + 33031: 330671, + 33032: 330691, + 33033: 330701, + 34000: 340011, + 34001: 340121, + 34002: 340151, + 34003: 340161, + 34004: 340131, + 34005: 340071, + 34009: 340231, + 34010: 340241, + 34011: 340251, + 34012: 340261, + 34013: 340281, + 34014: 340291, + 34015: 340301, + 34016: 340321, + 34017: 340341, + 34018: 340351, + 34019: 340361, + 34020: 340381, + 34021: 340391, + 34022: 340411, + 34023: 340421, + 34024: 340441, + 34025: 340451, + 34026: 340461, + 34027: 340491, + 34028: 340501, + 34029: 340521, + 34030: 340531, + 34031: 340541, + 34032: 340571, + 34033: 340601, + 34034: 340611, + 34035: 340621, + 34036: 340631, + 34037: 340651, + 34038: 340681, + 34039: 340701, + 34040: 340721, + 34041: 340731, + 34042: 340751, + 34043: 340761, + 34044: 340781, + 34045: 340801, + 34046: 340831, + 34047: 340861, + 34048: 340871, + 35000: 350011, + 35001: 350161, + 35002: 350141, + 35003: 350061, + 35005: 350081, + 35006: 350121, + 35008: 350181, + 35009: 350191, + 35010: 350221, + 35011: 350231, + 35012: 350261, + 35013: 350271, + 35014: 350301, + 35015: 350321, + 35016: 350341, + 35017: 350361, + 35018: 350391, + 35019: 350401, + 35020: 350411, + 35021: 350431, + 35022: 350441, + 35023: 350451, + 35024: 350461, + 35025: 350491, + 35026: 350501, + 35027: 350511, + 35028: 350531, + 35029: 350551, + 35030: 350601, + 35031: 350621, + 35032: 350631, + 35033: 350641, + 35034: 350661, + 35035: 350681, + 35036: 350691, + 35037: 350701, + 35038: 350711, + 41000: 410031, + 41001: 410071, + 41002: 410111, + 41003: 410151, + 42000: 420031, + 42001: 420071, + 42002: 420111, + 42003: 420151, + 43000: 430031, + 43001: 430071, + 43002: 430111, + 43003: 430151, + 44000: 440031, + 44001: 440071, + 44002: 440111, + 44003: 440151, + 44004: 440191, + 45000: 450031, + 45001: 450071, + 45002: 450111, + 45003: 450151, + 51001: 510011, + 51002: 510021, + 51003: 510031, + 52001: 520011, + 52002: 520021, + 53001: 530011, + 53002: 530021, + 54001: 540011, + 54002: 540021, + 55001: 550011, + 55002: 550021, + 55003: 550031, +} diff --git a/server/internal/masterdata/gacha_pool.go b/server/internal/masterdata/gacha_pool.go index 411bcdb..436278a 100644 --- a/server/internal/masterdata/gacha_pool.go +++ b/server/internal/masterdata/gacha_pool.go @@ -3,7 +3,6 @@ package masterdata import ( "fmt" "log" - "slices" "sort" "lunar-tear/server/internal/model" @@ -126,27 +125,16 @@ func LoadGachaPool() (*GachaCatalog, error) { } catalogCostumeSet := make(map[int32]bool, len(catalogCostumes)) - costumeTermId := make(map[int32]int32, len(catalogCostumes)) for _, c := range catalogCostumes { catalogCostumeSet[c.CostumeId] = true - costumeTermId[c.CostumeId] = c.CatalogTermId } catalogWeaponSet := make(map[int32]bool, len(catalogWeapons)) for _, w := range catalogWeapons { catalogWeaponSet[w.WeaponId] = true } - costumeWeaponType := make(map[int32]int32, len(costumes)) - for _, c := range costumes { - costumeWeaponType[c.CostumeId] = c.SkillfulWeaponType - } - - weaponTypeById := make(map[int32]int32, len(weapons)) - weaponRarityById := make(map[int32]int32, len(weapons)) restrictedWeapons := make(map[int32]bool) for _, w := range weapons { - weaponTypeById[w.WeaponId] = w.WeaponType - weaponRarityById[w.WeaponId] = w.RarityType if w.IsRestrictDiscard { restrictedWeapons[w.WeaponId] = true } @@ -262,60 +250,12 @@ func LoadGachaPool() (*GachaCatalog, error) { log.Printf("[GachaPool] pool excludes %d evolved, %d quest-granted costumes, %d quest-granted weapons, %d restricted weapons", evolvedFilteredCount, questGrantedCostumeCount, questGrantedWeaponCount, restrictedCount) - type weaponKey struct { - TermId int32 - WeaponType int32 - Rarity int32 - } - weaponsByKey := make(map[weaponKey][]int32) - for _, cw := range catalogWeapons { - if evolvedWeapons[cw.WeaponId] || restrictedWeapons[cw.WeaponId] { - continue - } - wt := weaponTypeById[cw.WeaponId] - r := weaponRarityById[cw.WeaponId] - if wt == 0 || r < model.RaritySRare { - continue - } - k := weaponKey{TermId: cw.CatalogTermId, WeaponType: wt, Rarity: r} - weaponsByKey[k] = append(weaponsByKey[k], cw.WeaponId) - } - for k, ids := range weaponsByKey { - slices.Sort(ids) - weaponsByKey[k] = ids - } - - exact, pattern, bestGuess := 0, 0, 0 - for costumeId, item := range pool.CostumeById { - tid := costumeTermId[costumeId] - wt := costumeWeaponType[costumeId] - k := weaponKey{TermId: tid, WeaponType: wt, Rarity: item.RarityType} - candidates := weaponsByKey[k] - if len(candidates) == 0 { - continue - } - if len(candidates) == 1 { - pool.CostumeWeaponMap[costumeId] = candidates[0] - exact++ - continue - } - idPattern := costumeId*10 + 1 - found := false - for _, wid := range candidates { - if wid == idPattern { - pool.CostumeWeaponMap[costumeId] = wid - pattern++ - found = true - break - } - } - if !found { - pool.CostumeWeaponMap[costumeId] = candidates[0] - bestGuess++ + for costumeId := range pool.CostumeById { + if wid, ok := costumeWeaponPairings[costumeId]; ok { + pool.CostumeWeaponMap[costumeId] = wid } } - log.Printf("[GachaPool] costume-weapon pairing: %d exact, %d id-pattern, %d best-guess, %d total", - exact, pattern, bestGuess, len(pool.CostumeWeaponMap)) + log.Printf("[GachaPool] costume-weapon pairing: %d entries from lookup table", len(pool.CostumeWeaponMap)) for _, m := range materials { pool.Materials = append(pool.Materials, GachaPoolItem{ @@ -330,7 +270,6 @@ func LoadGachaPool() (*GachaCatalog, error) { func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) { pool.ShopFeaturedByMedal = make(map[int32][]ShopFeaturedEntry) - shopPairs := 0 for _, cells := range shop.ExchangeShopCells { consumableId := shop.Items[cells[0].ShopItemId].PriceId @@ -350,16 +289,12 @@ func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) { continue } entries = append(entries, ShopFeaturedEntry{CostumeId: costumeId, WeaponId: weaponId}) - if costumeId != 0 && weaponId != 0 { - pool.CostumeWeaponMap[costumeId] = weaponId - shopPairs++ - } } if len(entries) > 0 { pool.ShopFeaturedByMedal[consumableId] = entries } } - log.Printf("[GachaPool] shop featured: %d consumables, %d costume-weapon pairs overridden", len(pool.ShopFeaturedByMedal), shopPairs) + log.Printf("[GachaPool] shop featured: %d consumables", len(pool.ShopFeaturedByMedal)) } func (pool *GachaCatalog) PruneUnpairedCostumes() { diff --git a/server/internal/model/gimmick.go b/server/internal/model/gimmick.go index f1317a2..3c11644 100644 --- a/server/internal/model/gimmick.go +++ b/server/internal/model/gimmick.go @@ -4,12 +4,12 @@ type GimmickType int32 const ( GimmickTypeUnknown GimmickType = 0 - GimmickTypeCageTreasureHunt GimmickType = 1 // "Fickle Black Birds" — in-cage flick-and-tap birds - GimmickTypeCageIntervalDropItem GimmickType = 2 // "Lost Items" — in-cage 3-dot drops that respawn on interval - GimmickTypeBrokenObelisk GimmickType = 3 // "Broken Scarecrow" (per tip text); zero rows in m_gimmick, unused - GimmickTypeIronGrill GimmickType = 4 // unused (zero rows in m_gimmick), in-game name unknown - GimmickTypeRadioMessage GimmickType = 5 // unused (zero rows in m_gimmick); client has GimmickRadioMessage class but no data - GimmickTypeFirstBrokenObelisk GimmickType = 6 // variant of Broken Scarecrow; zero rows in m_gimmick, unused + GimmickTypeCageTreasureHunt GimmickType = 1 // "Fickle Black Birds" — in-cage flick-and-tap birds + GimmickTypeCageIntervalDropItem GimmickType = 2 // "Lost Items" — in-cage 3-dot drops that respawn on interval + GimmickTypeBrokenObelisk GimmickType = 3 // "Broken Scarecrow" (per tip text); zero rows in m_gimmick, unused + GimmickTypeIronGrill GimmickType = 4 // unused (zero rows in m_gimmick), in-game name unknown + GimmickTypeRadioMessage GimmickType = 5 // unused (zero rows in m_gimmick); client has GimmickRadioMessage class but no data + GimmickTypeFirstBrokenObelisk GimmickType = 6 // variant of Broken Scarecrow; zero rows in m_gimmick, unused GimmickTypeMapOnlyCageTreasureHunt GimmickType = 7 // "Hidden Black Birds" — world-map birds; per-tap reward from m_cage_ornament_reward GimmickTypeMapOnlyCageIntervalDrop GimmickType = 8 // map-side variant of Lost Items GimmickTypeReport GimmickType = 9 // "Hidden Stories" — hidden mission markers diff --git a/server/internal/questflow/quest.go b/server/internal/questflow/quest.go index 71f389c..482f4ba 100644 --- a/server/internal/questflow/quest.go +++ b/server/internal/questflow/quest.go @@ -185,8 +185,8 @@ func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 { func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome *FinishOutcome, nowMillis int64, wasReplay bool) { questState := user.Quests[questId] + h.applyExpAndGoldRewards(user, questId, nowMillis) if !questState.IsRewardGranted { - h.applyExpAndGoldRewards(user, questId, nowMillis) if !wasReplay { h.applyFirstClearItemRewards(user, questId, nowMillis) outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,