Initial commit

This commit is contained in:
Ilya Groshev
2026-04-14 09:28:26 +03:00
commit 02f511f40c
161 changed files with 21541 additions and 0 deletions
+366
View File
@@ -0,0 +1,366 @@
package masterdata
import (
"log"
"sort"
"time"
"lunar-tear/server/internal/utils"
)
type bigHuntBossQuestRow struct {
BigHuntBossQuestId int32 `json:"BigHuntBossQuestId"`
BigHuntBossId int32 `json:"BigHuntBossId"`
BigHuntQuestGroupId int32 `json:"BigHuntQuestGroupId"`
BigHuntBossQuestScoreCoefficientId int32 `json:"BigHuntBossQuestScoreCoefficientId"`
BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"`
DailyChallengeCount int32 `json:"DailyChallengeCount"`
}
type BigHuntBossQuestRow struct {
BigHuntBossQuestId int32
BigHuntBossId int32
BigHuntQuestGroupId int32
BigHuntScoreRewardGroupScheduleId int32
DailyChallengeCount int32
}
type bigHuntQuestRow struct {
BigHuntQuestId int32 `json:"BigHuntQuestId"`
QuestId int32 `json:"QuestId"`
BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"`
}
type BigHuntQuestRow struct {
BigHuntQuestId int32
QuestId int32
BigHuntQuestScoreCoefficientId int32
}
type bigHuntQuestScoreCoefficientRow struct {
BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"`
ScoreDifficultBonusPermil int32 `json:"ScoreDifficultBonusPermil"`
}
type bigHuntBossRow struct {
BigHuntBossId int32 `json:"BigHuntBossId"`
BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"`
AttributeType int32 `json:"AttributeType"`
}
type BigHuntBossRow struct {
BigHuntBossId int32
BigHuntBossGradeGroupId int32
AttributeType int32
}
type bigHuntBossGradeGroupRow struct {
BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"`
NecessaryScore int64 `json:"NecessaryScore"`
AssetGradeIconId int32 `json:"AssetGradeIconId"`
}
type GradeThreshold struct {
NecessaryScore int64
AssetGradeIconId int32
}
type bigHuntScheduleRow struct {
BigHuntScheduleId int32 `json:"BigHuntScheduleId"`
ChallengeStartDatetime int64 `json:"ChallengeStartDatetime"`
ChallengeEndDatetime int64 `json:"ChallengeEndDatetime"`
}
type scoreRewardScheduleRow struct {
BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"`
GroupIndex int32 `json:"GroupIndex"`
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
StartDatetime int64 `json:"StartDatetime"`
}
type ScoreRewardScheduleEntry struct {
BigHuntScoreRewardGroupId int32
StartDatetime int64
}
type scoreRewardGroupRow struct {
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
NecessaryScore int64 `json:"NecessaryScore"`
BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"`
}
type ScoreRewardThreshold struct {
NecessaryScore int64
BigHuntRewardGroupId int32
}
type rewardGroupRow struct {
BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"`
SortOrder int32 `json:"SortOrder"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type RewardItem struct {
PossessionType int32
PossessionId int32
Count int32
}
type weeklyRewardScheduleRow struct {
BigHuntWeeklyAttributeScoreRewardGroupScheduleId int32 `json:"BigHuntWeeklyAttributeScoreRewardGroupScheduleId"`
AttributeType int32 `json:"AttributeType"`
GroupIndex int32 `json:"GroupIndex"`
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
StartDatetime int64 `json:"StartDatetime"`
}
type BigHuntWeeklyRewardKey struct {
ScheduleId int32
AttributeType int32
}
type BigHuntCatalog struct {
BossQuestById map[int32]BigHuntBossQuestRow
QuestById map[int32]BigHuntQuestRow
ScoreCoefficients map[int32]int32
BossByBossId map[int32]BigHuntBossRow
GradeThresholds map[int32][]GradeThreshold
ActiveScheduleId int32
ScoreRewardSchedules map[int32][]ScoreRewardScheduleEntry
ScoreRewardThresholds map[int32][]ScoreRewardThreshold
RewardItems map[int32][]RewardItem
WeeklyRewardSchedules map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry
}
func (c *BigHuntCatalog) ResolveActiveScoreRewardGroupId(scheduleId int32, nowMillis int64) int32 {
entries := c.ScoreRewardSchedules[scheduleId]
for _, e := range entries {
if nowMillis >= e.StartDatetime {
return e.BigHuntScoreRewardGroupId
}
}
if len(entries) > 0 {
return entries[len(entries)-1].BigHuntScoreRewardGroupId
}
return 0
}
func (c *BigHuntCatalog) ResolveActiveWeeklyRewardGroupId(key BigHuntWeeklyRewardKey, nowMillis int64) int32 {
entries := c.WeeklyRewardSchedules[key]
for _, e := range entries {
if nowMillis >= e.StartDatetime {
return e.BigHuntScoreRewardGroupId
}
}
if len(entries) > 0 {
return entries[len(entries)-1].BigHuntScoreRewardGroupId
}
return 0
}
func (c *BigHuntCatalog) ResolveGradeIconId(bossId int32, score int64) int32 {
boss, ok := c.BossByBossId[bossId]
if !ok {
return 0
}
thresholds := c.GradeThresholds[boss.BigHuntBossGradeGroupId]
var iconId int32
for _, t := range thresholds {
if score >= t.NecessaryScore {
iconId = t.AssetGradeIconId
} else {
break
}
}
return iconId
}
func (c *BigHuntCatalog) CollectNewRewards(scoreRewardGroupId int32, oldMax, newMax int64) []RewardItem {
thresholds := c.ScoreRewardThresholds[scoreRewardGroupId]
var items []RewardItem
for _, t := range thresholds {
if t.NecessaryScore > oldMax && t.NecessaryScore <= newMax {
items = append(items, c.RewardItems[t.BigHuntRewardGroupId]...)
}
}
return items
}
func LoadBigHuntCatalog() *BigHuntCatalog {
bossQuestRows, err := utils.ReadJSON[bigHuntBossQuestRow]("EntityMBigHuntBossQuestTable.json")
if err != nil {
log.Fatalf("load big hunt boss quest table: %v", err)
}
bossQuestById := make(map[int32]BigHuntBossQuestRow, len(bossQuestRows))
for _, r := range bossQuestRows {
bossQuestById[r.BigHuntBossQuestId] = BigHuntBossQuestRow{
BigHuntBossQuestId: r.BigHuntBossQuestId,
BigHuntBossId: r.BigHuntBossId,
BigHuntQuestGroupId: r.BigHuntQuestGroupId,
BigHuntScoreRewardGroupScheduleId: r.BigHuntScoreRewardGroupScheduleId,
DailyChallengeCount: r.DailyChallengeCount,
}
}
questRows, err := utils.ReadJSON[bigHuntQuestRow]("EntityMBigHuntQuestTable.json")
if err != nil {
log.Fatalf("load big hunt quest table: %v", err)
}
questById := make(map[int32]BigHuntQuestRow, len(questRows))
for _, r := range questRows {
questById[r.BigHuntQuestId] = BigHuntQuestRow{
BigHuntQuestId: r.BigHuntQuestId,
QuestId: r.QuestId,
BigHuntQuestScoreCoefficientId: r.BigHuntQuestScoreCoefficientId,
}
}
coeffRows, err := utils.ReadJSON[bigHuntQuestScoreCoefficientRow]("EntityMBigHuntQuestScoreCoefficientTable.json")
if err != nil {
log.Fatalf("load big hunt quest score coefficient table: %v", err)
}
scoreCoefficients := make(map[int32]int32, len(coeffRows))
for _, r := range coeffRows {
scoreCoefficients[r.BigHuntQuestScoreCoefficientId] = r.ScoreDifficultBonusPermil
}
bossRows, err := utils.ReadJSON[bigHuntBossRow]("EntityMBigHuntBossTable.json")
if err != nil {
log.Fatalf("load big hunt boss table: %v", err)
}
bossByBossId := make(map[int32]BigHuntBossRow, len(bossRows))
for _, r := range bossRows {
bossByBossId[r.BigHuntBossId] = BigHuntBossRow{
BigHuntBossId: r.BigHuntBossId,
BigHuntBossGradeGroupId: r.BigHuntBossGradeGroupId,
AttributeType: r.AttributeType,
}
}
gradeRows, err := utils.ReadJSON[bigHuntBossGradeGroupRow]("EntityMBigHuntBossGradeGroupTable.json")
if err != nil {
log.Fatalf("load big hunt boss grade group table: %v", err)
}
gradeThresholds := make(map[int32][]GradeThreshold)
for _, r := range gradeRows {
gradeThresholds[r.BigHuntBossGradeGroupId] = append(gradeThresholds[r.BigHuntBossGradeGroupId], GradeThreshold{
NecessaryScore: r.NecessaryScore,
AssetGradeIconId: r.AssetGradeIconId,
})
}
for k := range gradeThresholds {
sort.Slice(gradeThresholds[k], func(i, j int) bool {
return gradeThresholds[k][i].NecessaryScore < gradeThresholds[k][j].NecessaryScore
})
}
scheduleRows, err := utils.ReadJSON[bigHuntScheduleRow]("EntityMBigHuntScheduleTable.json")
if err != nil {
log.Fatalf("load big hunt schedule table: %v", err)
}
nowMillis := time.Now().UnixMilli()
var activeScheduleId int32
var latestEndDatetime int64
for _, r := range scheduleRows {
if nowMillis >= r.ChallengeStartDatetime && nowMillis <= r.ChallengeEndDatetime {
activeScheduleId = r.BigHuntScheduleId
break
}
if r.ChallengeEndDatetime > latestEndDatetime {
latestEndDatetime = r.ChallengeEndDatetime
activeScheduleId = r.BigHuntScheduleId
}
}
rewardSchedRows, err := utils.ReadJSON[scoreRewardScheduleRow]("EntityMBigHuntScoreRewardGroupScheduleTable.json")
if err != nil {
log.Fatalf("load big hunt score reward group schedule table: %v", err)
}
scoreRewardSchedules := make(map[int32][]ScoreRewardScheduleEntry)
for _, r := range rewardSchedRows {
scoreRewardSchedules[r.BigHuntScoreRewardGroupScheduleId] = append(
scoreRewardSchedules[r.BigHuntScoreRewardGroupScheduleId],
ScoreRewardScheduleEntry{
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
StartDatetime: r.StartDatetime,
},
)
}
for k := range scoreRewardSchedules {
sort.Slice(scoreRewardSchedules[k], func(i, j int) bool {
return scoreRewardSchedules[k][i].StartDatetime > scoreRewardSchedules[k][j].StartDatetime
})
}
rewardGroupRows, err := utils.ReadJSON[scoreRewardGroupRow]("EntityMBigHuntScoreRewardGroupTable.json")
if err != nil {
log.Fatalf("load big hunt score reward group table: %v", err)
}
scoreRewardThresholds := make(map[int32][]ScoreRewardThreshold)
for _, r := range rewardGroupRows {
scoreRewardThresholds[r.BigHuntScoreRewardGroupId] = append(
scoreRewardThresholds[r.BigHuntScoreRewardGroupId],
ScoreRewardThreshold{
NecessaryScore: r.NecessaryScore,
BigHuntRewardGroupId: r.BigHuntRewardGroupId,
},
)
}
for k := range scoreRewardThresholds {
sort.Slice(scoreRewardThresholds[k], func(i, j int) bool {
return scoreRewardThresholds[k][i].NecessaryScore < scoreRewardThresholds[k][j].NecessaryScore
})
}
rewardItemRows, err := utils.ReadJSON[rewardGroupRow]("EntityMBigHuntRewardGroupTable.json")
if err != nil {
log.Fatalf("load big hunt reward group table: %v", err)
}
rewardItems := make(map[int32][]RewardItem)
for _, r := range rewardItemRows {
rewardItems[r.BigHuntRewardGroupId] = append(rewardItems[r.BigHuntRewardGroupId], RewardItem{
PossessionType: r.PossessionType,
PossessionId: r.PossessionId,
Count: r.Count,
})
}
weeklySchedRows, err := utils.ReadJSON[weeklyRewardScheduleRow]("EntityMBigHuntWeeklyAttributeScoreRewardGroupScheduleTable.json")
if err != nil {
log.Fatalf("load big hunt weekly attribute score reward group schedule table: %v", err)
}
weeklyRewardSchedules := make(map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry)
for _, r := range weeklySchedRows {
key := BigHuntWeeklyRewardKey{
ScheduleId: r.BigHuntWeeklyAttributeScoreRewardGroupScheduleId,
AttributeType: r.AttributeType,
}
weeklyRewardSchedules[key] = append(weeklyRewardSchedules[key], ScoreRewardScheduleEntry{
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
StartDatetime: r.StartDatetime,
})
}
for k := range weeklyRewardSchedules {
sort.Slice(weeklyRewardSchedules[k], func(i, j int) bool {
return weeklyRewardSchedules[k][i].StartDatetime > weeklyRewardSchedules[k][j].StartDatetime
})
}
log.Printf("big hunt catalog loaded: %d boss quests, %d quests, %d bosses, %d score coefficients, %d reward groups, schedule=%d",
len(bossQuestById), len(questById), len(bossByBossId), len(scoreCoefficients), len(rewardItems), activeScheduleId)
return &BigHuntCatalog{
BossQuestById: bossQuestById,
QuestById: questById,
ScoreCoefficients: scoreCoefficients,
BossByBossId: bossByBossId,
GradeThresholds: gradeThresholds,
ActiveScheduleId: activeScheduleId,
ScoreRewardSchedules: scoreRewardSchedules,
ScoreRewardThresholds: scoreRewardThresholds,
RewardItems: rewardItems,
WeeklyRewardSchedules: weeklyRewardSchedules,
}
}
@@ -0,0 +1,65 @@
package masterdata
import (
"log"
"lunar-tear/server/internal/utils"
)
type cageOrnament struct {
CageOrnamentId int32 `json:"CageOrnamentId"`
CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"`
}
type cageOrnamentRewardRow struct {
CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type CageOrnamentReward struct {
PossessionType int32
PossessionId int32
Count int32
}
type CageOrnamentCatalog struct {
ornamentToRewardId map[int32]int32
rewards map[int32]CageOrnamentReward
}
func (c *CageOrnamentCatalog) LookupReward(cageOrnamentId int32) (CageOrnamentReward, bool) {
rewardId, ok := c.ornamentToRewardId[cageOrnamentId]
if !ok || rewardId == 0 {
return CageOrnamentReward{}, false
}
entry, ok := c.rewards[rewardId]
return entry, ok
}
func LoadCageOrnamentCatalog() *CageOrnamentCatalog {
ornaments, err := utils.ReadJSON[cageOrnament]("EntityMCageOrnamentTable.json")
if err != nil {
log.Fatalf("load cage ornament table: %v", err)
}
rewards, err := utils.ReadJSON[cageOrnamentRewardRow]("EntityMCageOrnamentRewardTable.json")
if err != nil {
log.Fatalf("load cage ornament reward table: %v", err)
}
cat := &CageOrnamentCatalog{
ornamentToRewardId: make(map[int32]int32, len(ornaments)),
rewards: make(map[int32]CageOrnamentReward, len(rewards)),
}
for _, o := range ornaments {
cat.ornamentToRewardId[o.CageOrnamentId] = o.CageOrnamentRewardId
}
for _, r := range rewards {
cat.rewards[r.CageOrnamentRewardId] = CageOrnamentReward{
PossessionType: r.PossessionType,
PossessionId: r.PossessionId,
Count: r.Count,
}
}
return cat
}
@@ -0,0 +1,74 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/utils"
)
type CharacterRebirthRow struct {
CharacterId int32 `json:"CharacterId"`
CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"`
}
type CharacterRebirthStepRow struct {
CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"`
BeforeRebirthCount int32 `json:"BeforeRebirthCount"`
CostumeLevelLimitUp int32 `json:"CostumeLevelLimitUp"`
CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"`
}
type CharacterRebirthMaterialRow struct {
CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
}
type StepKey struct {
GroupId int32
BeforeRebirthCount int32
}
type CharacterRebirthCatalog struct {
StepGroupByCharacterId map[int32]int32
StepByGroupAndCount map[StepKey]CharacterRebirthStepRow
MaterialsByGroupId map[int32][]CharacterRebirthMaterialRow
}
func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) {
rebirthRows, err := utils.ReadJSON[CharacterRebirthRow]("EntityMCharacterRebirthTable.json")
if err != nil {
return nil, fmt.Errorf("load character rebirth table: %w", err)
}
stepRows, err := utils.ReadJSON[CharacterRebirthStepRow]("EntityMCharacterRebirthStepGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load character rebirth step group table: %w", err)
}
materialRows, err := utils.ReadJSON[CharacterRebirthMaterialRow]("EntityMCharacterRebirthMaterialGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load character rebirth material group table: %w", err)
}
stepGroupByCharacterId := make(map[int32]int32, len(rebirthRows))
for _, r := range rebirthRows {
stepGroupByCharacterId[r.CharacterId] = r.CharacterRebirthStepGroupId
}
stepByGroupAndCount := make(map[StepKey]CharacterRebirthStepRow, len(stepRows))
for _, s := range stepRows {
stepByGroupAndCount[StepKey{GroupId: s.CharacterRebirthStepGroupId, BeforeRebirthCount: s.BeforeRebirthCount}] = s
}
materialsByGroupId := make(map[int32][]CharacterRebirthMaterialRow)
for _, m := range materialRows {
materialsByGroupId[m.CharacterRebirthMaterialGroupId] = append(materialsByGroupId[m.CharacterRebirthMaterialGroupId], m)
}
return &CharacterRebirthCatalog{
StepGroupByCharacterId: stepGroupByCharacterId,
StepByGroupAndCount: stepByGroupAndCount,
MaterialsByGroupId: materialsByGroupId,
}, nil
}
@@ -0,0 +1,183 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/utils"
)
type CharacterBoardPanelRow struct {
CharacterBoardPanelId int32 `json:"CharacterBoardPanelId"`
CharacterBoardId int32 `json:"CharacterBoardId"`
CharacterBoardPanelUnlockConditionGroupId int32 `json:"CharacterBoardPanelUnlockConditionGroupId"`
CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"`
CharacterBoardPanelReleaseRewardGroupId int32 `json:"CharacterBoardPanelReleaseRewardGroupId"`
CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"`
SortOrder int32 `json:"SortOrder"`
ParentCharacterBoardPanelId int32 `json:"ParentCharacterBoardPanelId"`
PlaceIndex int32 `json:"PlaceIndex"`
}
type CharacterBoardReleasePossessionRow struct {
CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type CharacterBoardReleaseEffectRow struct {
CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"`
SortOrder int32 `json:"SortOrder"`
CharacterBoardEffectType int32 `json:"CharacterBoardEffectType"`
CharacterBoardEffectId int32 `json:"CharacterBoardEffectId"`
EffectValue int32 `json:"EffectValue"`
}
type CharacterBoardRow struct {
CharacterBoardId int32 `json:"CharacterBoardId"`
CharacterBoardGroupId int32 `json:"CharacterBoardGroupId"`
CharacterBoardUnlockConditionGroupId int32 `json:"CharacterBoardUnlockConditionGroupId"`
ReleaseRank int32 `json:"ReleaseRank"`
}
type CharacterBoardStatusUpRow struct {
CharacterBoardStatusUpId int32 `json:"CharacterBoardStatusUpId"`
CharacterBoardStatusUpType int32 `json:"CharacterBoardStatusUpType"`
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
}
type CharacterBoardAbilityRow struct {
CharacterBoardAbilityId int32 `json:"CharacterBoardAbilityId"`
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
AbilityId int32 `json:"AbilityId"`
}
type CharacterBoardAbilityMaxLevelRow struct {
CharacterId int32 `json:"CharacterId"`
AbilityId int32 `json:"AbilityId"`
MaxLevel int32 `json:"MaxLevel"`
}
type CharacterBoardEffectTargetRow struct {
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
GroupIndex int32 `json:"GroupIndex"`
CharacterBoardEffectTargetType int32 `json:"CharacterBoardEffectTargetType"`
TargetValue int32 `json:"TargetValue"`
}
type CharacterBoardAssignmentRow struct {
CharacterId int32 `json:"CharacterId"`
CharacterBoardCategoryId int32 `json:"CharacterBoardCategoryId"`
SortOrder int32 `json:"SortOrder"`
CharacterBoardAssignmentType int32 `json:"CharacterBoardAssignmentType"`
}
type CharacterBoardGroupRow struct {
CharacterBoardGroupId int32 `json:"CharacterBoardGroupId"`
CharacterBoardCategoryId int32 `json:"CharacterBoardCategoryId"`
SortOrder int32 `json:"SortOrder"`
CharacterBoardGroupType int32 `json:"CharacterBoardGroupType"`
TextAssetId int32 `json:"TextAssetId"`
}
type CharacterBoardCatalog struct {
PanelById map[int32]CharacterBoardPanelRow
PanelsByBoardId map[int32][]CharacterBoardPanelRow
ReleaseCostsByGroupId map[int32][]CharacterBoardReleasePossessionRow
ReleaseEffectsByGroupId map[int32][]CharacterBoardReleaseEffectRow
StatusUpById map[int32]CharacterBoardStatusUpRow
AbilityById map[int32]CharacterBoardAbilityRow
AbilityMaxLevel map[store.CharacterBoardAbilityKey]int32
EffectTargetsByGroupId map[int32][]CharacterBoardEffectTargetRow
BoardById map[int32]CharacterBoardRow
}
func LoadCharacterBoardCatalog() (*CharacterBoardCatalog, error) {
panels, err := utils.ReadJSON[CharacterBoardPanelRow]("EntityMCharacterBoardPanelTable.json")
if err != nil {
return nil, fmt.Errorf("load character board panel table: %w", err)
}
costs, err := utils.ReadJSON[CharacterBoardReleasePossessionRow]("EntityMCharacterBoardPanelReleasePossessionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load character board release possession table: %w", err)
}
effects, err := utils.ReadJSON[CharacterBoardReleaseEffectRow]("EntityMCharacterBoardPanelReleaseEffectGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load character board release effect table: %w", err)
}
boards, err := utils.ReadJSON[CharacterBoardRow]("EntityMCharacterBoardTable.json")
if err != nil {
return nil, fmt.Errorf("load character board table: %w", err)
}
statusUps, err := utils.ReadJSON[CharacterBoardStatusUpRow]("EntityMCharacterBoardStatusUpTable.json")
if err != nil {
return nil, fmt.Errorf("load character board status up table: %w", err)
}
abilities, err := utils.ReadJSON[CharacterBoardAbilityRow]("EntityMCharacterBoardAbilityTable.json")
if err != nil {
return nil, fmt.Errorf("load character board ability table: %w", err)
}
abilityMaxLevels, err := utils.ReadJSON[CharacterBoardAbilityMaxLevelRow]("EntityMCharacterBoardAbilityMaxLevelTable.json")
if err != nil {
return nil, fmt.Errorf("load character board ability max level table: %w", err)
}
targets, err := utils.ReadJSON[CharacterBoardEffectTargetRow]("EntityMCharacterBoardEffectTargetGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load character board effect target table: %w", err)
}
catalog := &CharacterBoardCatalog{
PanelById: make(map[int32]CharacterBoardPanelRow, len(panels)),
PanelsByBoardId: make(map[int32][]CharacterBoardPanelRow),
ReleaseCostsByGroupId: make(map[int32][]CharacterBoardReleasePossessionRow),
ReleaseEffectsByGroupId: make(map[int32][]CharacterBoardReleaseEffectRow),
StatusUpById: make(map[int32]CharacterBoardStatusUpRow, len(statusUps)),
AbilityById: make(map[int32]CharacterBoardAbilityRow, len(abilities)),
AbilityMaxLevel: make(map[store.CharacterBoardAbilityKey]int32, len(abilityMaxLevels)),
EffectTargetsByGroupId: make(map[int32][]CharacterBoardEffectTargetRow),
BoardById: make(map[int32]CharacterBoardRow, len(boards)),
}
for _, p := range panels {
catalog.PanelById[p.CharacterBoardPanelId] = p
catalog.PanelsByBoardId[p.CharacterBoardId] = append(catalog.PanelsByBoardId[p.CharacterBoardId], p)
}
for _, c := range costs {
catalog.ReleaseCostsByGroupId[c.CharacterBoardPanelReleasePossessionGroupId] = append(
catalog.ReleaseCostsByGroupId[c.CharacterBoardPanelReleasePossessionGroupId], c)
}
for _, e := range effects {
catalog.ReleaseEffectsByGroupId[e.CharacterBoardPanelReleaseEffectGroupId] = append(
catalog.ReleaseEffectsByGroupId[e.CharacterBoardPanelReleaseEffectGroupId], e)
}
for _, b := range boards {
catalog.BoardById[b.CharacterBoardId] = b
}
for _, s := range statusUps {
catalog.StatusUpById[s.CharacterBoardStatusUpId] = s
}
for _, a := range abilities {
catalog.AbilityById[a.CharacterBoardAbilityId] = a
}
for _, m := range abilityMaxLevels {
catalog.AbilityMaxLevel[store.CharacterBoardAbilityKey{
CharacterId: m.CharacterId,
AbilityId: m.AbilityId,
}] = m.MaxLevel
}
for _, t := range targets {
catalog.EffectTargetsByGroupId[t.CharacterBoardEffectTargetGroupId] = append(
catalog.EffectTargetsByGroupId[t.CharacterBoardEffectTargetGroupId], t)
}
return catalog, nil
}
@@ -0,0 +1,62 @@
package masterdata
import (
"log"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/utils"
)
type characterViewerField struct {
CharacterViewerFieldId int32 `json:"CharacterViewerFieldId"`
ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"`
}
type characterViewerFieldEntry struct {
FieldId int32
RequiredQuestId int32
}
type CharacterViewerCatalog struct {
fields []characterViewerFieldEntry
}
func (c *CharacterViewerCatalog) ReleasedFieldIds(user store.UserState) []int32 {
var released []int32
for _, f := range c.fields {
if f.RequiredQuestId == 0 {
released = append(released, f.FieldId)
continue
}
q, ok := user.Quests[f.RequiredQuestId]
if ok && q.QuestStateType == model.UserQuestStateTypeCleared {
released = append(released, f.FieldId)
}
}
return released
}
func LoadCharacterViewerCatalog(resolver *ConditionResolver) *CharacterViewerCatalog {
fields, err := utils.ReadJSON[characterViewerField]("EntityMCharacterViewerFieldTable.json")
if err != nil {
log.Fatalf("load character viewer field table: %v", err)
}
cat := &CharacterViewerCatalog{}
for _, f := range fields {
entry := characterViewerFieldEntry{FieldId: f.CharacterViewerFieldId}
if qid, ok := resolver.RequiredQuestId(f.ReleaseEvaluateConditionId); ok {
entry.RequiredQuestId = qid
}
cat.fields = append(cat.fields, entry)
}
sort.Slice(cat.fields, func(i, j int) bool {
return cat.fields[i].FieldId < cat.fields[j].FieldId
})
log.Printf("character viewer catalog loaded: %d fields", len(cat.fields))
return cat
}
+86
View File
@@ -0,0 +1,86 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/utils"
)
type companionRow struct {
CompanionId int32 `json:"CompanionId"`
CompanionCategoryType int32 `json:"CompanionCategoryType"`
}
type companionCategoryRow struct {
CompanionCategoryType int32 `json:"CompanionCategoryType"`
EnhancementCostNumericalFunctionId int32 `json:"EnhancementCostNumericalFunctionId"`
}
type companionEnhancementMaterialRow struct {
CompanionCategoryType int32 `json:"CompanionCategoryType"`
Level int32 `json:"Level"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
}
type CompanionLevelKey struct {
CategoryType int32
Level int32
}
type CompanionMaterialCost struct {
MaterialId int32
Count int32
}
type CompanionCatalog struct {
CompanionById map[int32]companionRow
GoldCostByCategory map[int32]NumericalFunc
MaterialsByKey map[CompanionLevelKey]CompanionMaterialCost
}
func LoadCompanionCatalog() (*CompanionCatalog, error) {
companions, err := utils.ReadJSON[companionRow]("EntityMCompanionTable.json")
if err != nil {
return nil, fmt.Errorf("load companion table: %w", err)
}
categories, err := utils.ReadJSON[companionCategoryRow]("EntityMCompanionCategoryTable.json")
if err != nil {
return nil, fmt.Errorf("load companion category table: %w", err)
}
materials, err := utils.ReadJSON[companionEnhancementMaterialRow]("EntityMCompanionEnhancementMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load companion enhancement material table: %w", err)
}
funcResolver, err := LoadFunctionResolver()
if err != nil {
return nil, fmt.Errorf("load function resolver: %w", err)
}
companionById := make(map[int32]companionRow, len(companions))
for _, c := range companions {
companionById[c.CompanionId] = c
}
goldCostByCategory := make(map[int32]NumericalFunc, len(categories))
for _, cat := range categories {
if f, ok := funcResolver.Resolve(cat.EnhancementCostNumericalFunctionId); ok {
goldCostByCategory[cat.CompanionCategoryType] = f
}
}
materialsByKey := make(map[CompanionLevelKey]CompanionMaterialCost, len(materials))
for _, m := range materials {
key := CompanionLevelKey{CategoryType: m.CompanionCategoryType, Level: m.Level}
materialsByKey[key] = CompanionMaterialCost{MaterialId: m.MaterialId, Count: m.Count}
}
return &CompanionCatalog{
CompanionById: companionById,
GoldCostByCategory: goldCostByCategory,
MaterialsByKey: materialsByKey,
}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type evaluateCondition struct {
EvaluateConditionId int32 `json:"EvaluateConditionId"`
EvaluateConditionFunctionType model.EvaluateConditionFunctionType `json:"EvaluateConditionFunctionType"`
EvaluateConditionEvaluateType model.EvaluateConditionEvaluateType `json:"EvaluateConditionEvaluateType"`
EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"`
}
type evaluateConditionValueGroup struct {
EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"`
GroupIndex int32 `json:"GroupIndex"`
Value int64 `json:"Value"`
}
const defaultGroupIndex = 1
type ConditionResolver struct {
requiredQuestByCondId map[int32]int32
}
func LoadConditionResolver() (*ConditionResolver, error) {
conditions, err := utils.ReadJSON[evaluateCondition]("EntityMEvaluateConditionTable.json")
if err != nil {
return nil, fmt.Errorf("load evaluate condition table: %w", err)
}
valueGroups, err := utils.ReadJSON[evaluateConditionValueGroup]("EntityMEvaluateConditionValueGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load evaluate condition value group table: %w", err)
}
condById := make(map[int32]evaluateCondition, len(conditions))
for _, c := range conditions {
condById[c.EvaluateConditionId] = c
}
type vgKey struct {
GroupId int32
GroupIndex int32
}
vgByKey := make(map[vgKey]int64, len(valueGroups))
for _, vg := range valueGroups {
vgByKey[vgKey{vg.EvaluateConditionValueGroupId, vg.GroupIndex}] = vg.Value
}
resolved := make(map[int32]int32)
for _, c := range conditions {
if c.EvaluateConditionFunctionType == model.EvaluateConditionFunctionTypeQuestClear &&
c.EvaluateConditionEvaluateType == model.EvaluateConditionEvaluateTypeIdContain {
if questId, ok := vgByKey[vgKey{c.EvaluateConditionValueGroupId, defaultGroupIndex}]; ok {
resolved[c.EvaluateConditionId] = int32(questId)
}
}
}
return &ConditionResolver{requiredQuestByCondId: resolved}, nil
}
func (r *ConditionResolver) RequiredQuestId(conditionId int32) (int32, bool) {
qid, ok := r.requiredQuestByCondId[conditionId]
return qid, ok
}
+89
View File
@@ -0,0 +1,89 @@
package masterdata
import (
"fmt"
"strconv"
"lunar-tear/server/internal/utils"
)
type configRow struct {
ConfigKey string `json:"ConfigKey"`
Value string `json:"Value"`
}
type GameConfig struct {
ConsumableItemIdForGold int32
ConsumableItemIdForMedal int32
ConsumableItemIdForRareMedal int32
ConsumableItemIdForArenaCoin int32
ConsumableItemIdForExploreTicket int32
ConsumableItemIdForMomPoint int32
ConsumableItemIdForPremiumGachaTicket int32
ConsumableItemIdForQuestSkipTicket int32
CharacterRebirthAvailableCount int32
CharacterRebirthConsumeGold int32
CostumeAwakenAvailableCount int32
CostumeLimitBreakAvailableCount int32
MaterialSameWeaponExpCoefficientPermil int32
UserStaminaRecoverySecond int32
RewardGachaDailyMaxCount int32
QuestSkipMaxCountAtOnce int32
WeaponLimitBreakAvailableCount int32
}
func LoadGameConfig() (*GameConfig, error) {
rows, err := utils.ReadJSON[configRow]("EntityMConfigTable.json")
if err != nil {
return nil, fmt.Errorf("load config table: %w", err)
}
kv := make(map[string]string, len(rows))
for _, r := range rows {
kv[r.ConfigKey] = r.Value
}
cfg := &GameConfig{}
cfg.ConsumableItemIdForGold = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_GOLD")
cfg.ConsumableItemIdForMedal = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_MEDAL")
cfg.ConsumableItemIdForRareMedal = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_RARE_MEDAL")
cfg.ConsumableItemIdForArenaCoin = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_ARENA_COIN")
cfg.ConsumableItemIdForExploreTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_EXPLORE_TICKET")
cfg.ConsumableItemIdForMomPoint = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_MOM_POINT")
cfg.ConsumableItemIdForPremiumGachaTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_PREMIUM_GACHA_TICKET")
cfg.ConsumableItemIdForQuestSkipTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_QUEST_SKIP_TICKET")
cfg.CharacterRebirthAvailableCount = parseInt32(kv, "CHARACTER_REBIRTH_AVAILABLE_COUNT")
cfg.CharacterRebirthConsumeGold = parseInt32(kv, "CHARACTER_REBIRTH_CONSUME_GOLD")
cfg.CostumeAwakenAvailableCount = parseInt32(kv, "COSTUME_AWAKEN_AVAILABLE_COUNT")
cfg.CostumeLimitBreakAvailableCount = parseInt32(kv, "COSTUME_LIMIT_BREAK_AVAILABLE_COUNT")
cfg.MaterialSameWeaponExpCoefficientPermil = parseInt32(kv, "MATERIAL_SAME_WEAPON_EXP_COEFFICIENT_PERMIL")
cfg.UserStaminaRecoverySecond = parseInt32(kv, "USER_STAMINA_RECOVERY_SECOND")
cfg.RewardGachaDailyMaxCount = parseInt32(kv, "REWARD_GACHA_DAILY_MAX_COUNT")
cfg.QuestSkipMaxCountAtOnce = parseInt32(kv, "QUEST_SKIP_MAX_COUNT_AT_ONCE")
cfg.WeaponLimitBreakAvailableCount = parseInt32(kv, "WEAPON_LIMIT_BREAK_AVAILABLE_COUNT")
return cfg, nil
}
func parseInt32(kv map[string]string, key string) int32 {
s, ok := kv[key]
if !ok {
return 0
}
v, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return 0
}
return int32(v)
}
+246
View File
@@ -0,0 +1,246 @@
package masterdata
import (
"fmt"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type CostumeMasterRow struct {
CostumeId int32 `json:"CostumeId"`
CharacterId int32 `json:"CharacterId"`
SkillfulWeaponType int32 `json:"SkillfulWeaponType"`
RarityType int32 `json:"RarityType"`
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"`
}
type costumeRarityRow struct {
RarityType int32 `json:"RarityType"`
CostumeLimitBreakMaterialRarityGroupId int32 `json:"CostumeLimitBreakMaterialRarityGroupId"`
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
LimitBreakCostNumericalFunctionId int32 `json:"LimitBreakCostNumericalFunctionId"`
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
ActiveSkillMaxLevelNumericalFunctionId int32 `json:"ActiveSkillMaxLevelNumericalFunctionId"`
ActiveSkillEnhancementCostNumericalFunctionId int32 `json:"ActiveSkillEnhancementCostNumericalFunctionId"`
}
type CostumeAwakenRow struct {
CostumeId int32 `json:"CostumeId"`
CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"`
CostumeAwakenStepMaterialGroupId int32 `json:"CostumeAwakenStepMaterialGroupId"`
CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"`
}
type costumeAwakenPriceRow struct {
CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"`
AwakenStepLowerLimit int32 `json:"AwakenStepLowerLimit"`
Gold int32 `json:"Gold"`
}
type CostumeAwakenEffectRow struct {
CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"`
AwakenStep int32 `json:"AwakenStep"`
CostumeAwakenEffectType int32 `json:"CostumeAwakenEffectType"`
CostumeAwakenEffectId int32 `json:"CostumeAwakenEffectId"`
}
type CostumeAwakenStatusUpRow struct {
CostumeAwakenStatusUpGroupId int32 `json:"CostumeAwakenStatusUpGroupId"`
SortOrder int32 `json:"SortOrder"`
StatusKindType int32 `json:"StatusKindType"`
StatusCalculationType int32 `json:"StatusCalculationType"`
EffectValue int32 `json:"EffectValue"`
}
type CostumeAwakenItemAcquireRow struct {
CostumeAwakenItemAcquireId int32 `json:"CostumeAwakenItemAcquireId"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type CostumeActiveSkillGroupRow struct {
CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"`
CostumeLimitBreakCountLowerLimit int32 `json:"CostumeLimitBreakCountLowerLimit"`
CostumeActiveSkillId int32 `json:"CostumeActiveSkillId"`
CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"`
}
type CostumeActiveSkillEnhanceMaterialRow struct {
CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"`
SkillLevel int32 `json:"SkillLevel"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type CostumeCatalog struct {
Costumes map[int32]CostumeMasterRow
Materials map[int32]MaterialRow
ExpByRarity map[int32][]int32
EnhanceCostByRarity map[int32]NumericalFunc
MaxLevelByRarity map[int32]NumericalFunc
LimitBreakCostByRarity map[int32]NumericalFunc
AwakenByCostumeId map[int32]CostumeAwakenRow
AwakenPriceByGroup map[int32]int32
AwakenEffectsByGroupAndStep map[int32]map[int32]CostumeAwakenEffectRow
AwakenStatusUpByGroup map[int32][]CostumeAwakenStatusUpRow
AwakenItemAcquireById map[int32]CostumeAwakenItemAcquireRow
ActiveSkillGroupsByGroupId map[int32][]CostumeActiveSkillGroupRow // sorted by CostumeLimitBreakCountLowerLimit desc
ActiveSkillEnhanceMats map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
ActiveSkillMaxLevelByRarity map[int32]NumericalFunc
ActiveSkillCostByRarity map[int32]NumericalFunc
}
func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
costumes, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json")
if err != nil {
return nil, fmt.Errorf("load costume table: %w", err)
}
rarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json")
if err != nil {
return nil, fmt.Errorf("load costume rarity table: %w", err)
}
paramMapRows, err := LoadParameterMap()
if err != nil {
return nil, err
}
funcResolver, err := LoadFunctionResolver()
if err != nil {
return nil, fmt.Errorf("load function resolver: %w", err)
}
awakenRows, err := utils.ReadJSON[CostumeAwakenRow]("EntityMCostumeAwakenTable.json")
if err != nil {
return nil, fmt.Errorf("load costume awaken table: %w", err)
}
awakenPriceRows, err := utils.ReadJSON[costumeAwakenPriceRow]("EntityMCostumeAwakenPriceGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume awaken price table: %w", err)
}
awakenEffectRows, err := utils.ReadJSON[CostumeAwakenEffectRow]("EntityMCostumeAwakenEffectGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume awaken effect table: %w", err)
}
awakenStatusUpRows, err := utils.ReadJSON[CostumeAwakenStatusUpRow]("EntityMCostumeAwakenStatusUpGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume awaken status up table: %w", err)
}
awakenItemAcquireRows, err := utils.ReadJSON[CostumeAwakenItemAcquireRow]("EntityMCostumeAwakenItemAcquireTable.json")
if err != nil {
return nil, fmt.Errorf("load costume awaken item acquire table: %w", err)
}
activeSkillGroupRows, err := utils.ReadJSON[CostumeActiveSkillGroupRow]("EntityMCostumeActiveSkillGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume active skill group table: %w", err)
}
activeSkillMatRows, err := utils.ReadJSON[CostumeActiveSkillEnhanceMaterialRow]("EntityMCostumeActiveSkillEnhancementMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load costume active skill enhancement material table: %w", err)
}
catalog := &CostumeCatalog{
Costumes: make(map[int32]CostumeMasterRow, len(costumes)),
Materials: matCatalog.ByType[model.MaterialTypeCostumeEnhancement],
ExpByRarity: make(map[int32][]int32, len(rarities)),
EnhanceCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
MaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
LimitBreakCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
AwakenByCostumeId: make(map[int32]CostumeAwakenRow, len(awakenRows)),
AwakenPriceByGroup: make(map[int32]int32, len(awakenPriceRows)),
AwakenEffectsByGroupAndStep: make(map[int32]map[int32]CostumeAwakenEffectRow),
AwakenStatusUpByGroup: make(map[int32][]CostumeAwakenStatusUpRow),
AwakenItemAcquireById: make(map[int32]CostumeAwakenItemAcquireRow, len(awakenItemAcquireRows)),
ActiveSkillGroupsByGroupId: make(map[int32][]CostumeActiveSkillGroupRow),
ActiveSkillEnhanceMats: make(map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow),
ActiveSkillMaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
ActiveSkillCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
}
for _, row := range costumes {
catalog.Costumes[row.CostumeId] = row
}
for _, r := range rarities {
if _, ok := catalog.ExpByRarity[r.RarityType]; !ok {
catalog.ExpByRarity[r.RarityType] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
}
if _, ok := catalog.EnhanceCostByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
catalog.EnhanceCostByRarity[r.RarityType] = f
}
}
if _, ok := catalog.MaxLevelByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
catalog.MaxLevelByRarity[r.RarityType] = f
}
}
if _, ok := catalog.LimitBreakCostByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.LimitBreakCostNumericalFunctionId); found {
catalog.LimitBreakCostByRarity[r.RarityType] = f
}
}
if _, ok := catalog.ActiveSkillMaxLevelByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.ActiveSkillMaxLevelNumericalFunctionId); found {
catalog.ActiveSkillMaxLevelByRarity[r.RarityType] = f
}
}
if _, ok := catalog.ActiveSkillCostByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.ActiveSkillEnhancementCostNumericalFunctionId); found {
catalog.ActiveSkillCostByRarity[r.RarityType] = f
}
}
}
for _, row := range awakenRows {
catalog.AwakenByCostumeId[row.CostumeId] = row
}
for _, row := range awakenPriceRows {
catalog.AwakenPriceByGroup[row.CostumeAwakenPriceGroupId] = row.Gold
}
for _, row := range awakenEffectRows {
m, ok := catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId]
if !ok {
m = make(map[int32]CostumeAwakenEffectRow)
catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId] = m
}
m[row.AwakenStep] = row
}
for _, row := range awakenStatusUpRows {
catalog.AwakenStatusUpByGroup[row.CostumeAwakenStatusUpGroupId] = append(
catalog.AwakenStatusUpByGroup[row.CostumeAwakenStatusUpGroupId], row)
}
for _, row := range awakenItemAcquireRows {
catalog.AwakenItemAcquireById[row.CostumeAwakenItemAcquireId] = row
}
for _, row := range activeSkillGroupRows {
gid := row.CostumeActiveSkillGroupId
catalog.ActiveSkillGroupsByGroupId[gid] = append(catalog.ActiveSkillGroupsByGroupId[gid], row)
}
for gid, rows := range catalog.ActiveSkillGroupsByGroupId {
sort.Slice(rows, func(i, j int) bool {
return rows[i].CostumeLimitBreakCountLowerLimit > rows[j].CostumeLimitBreakCountLowerLimit
})
catalog.ActiveSkillGroupsByGroupId[gid] = rows
}
for _, row := range activeSkillMatRows {
key := [2]int32{row.CostumeActiveSkillEnhancementMaterialId, row.SkillLevel}
catalog.ActiveSkillEnhanceMats[key] = append(catalog.ActiveSkillEnhanceMats[key], row)
}
return catalog, nil
}
@@ -0,0 +1,81 @@
package masterdata
import (
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type costumeDupRow struct {
CostumeId int32 `json:"CostumeId"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
func LoadDupExchange() (map[int32][]model.DupExchangeEntry, error) {
result := make(map[int32][]model.DupExchangeEntry)
costumeRows, err := utils.ReadJSON[costumeDupRow]("EntityMCostumeDuplicationExchangePossessionGroupTable.json")
if err != nil {
return nil, err
}
for _, r := range costumeRows {
result[r.CostumeId] = append(result[r.CostumeId], model.DupExchangeEntry{
PossessionType: r.PossessionType,
PossessionId: r.PossessionId,
Count: r.Count,
})
}
return result, nil
}
type lbMaterialRow struct {
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
MaterialId int32 `json:"MaterialId"`
}
type costumeLBRef struct {
CostumeId int32 `json:"CostumeId"`
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
}
const dupExchangeFallbackCount int32 = 10
func EnrichDupExchange(dupMap map[int32][]model.DupExchangeEntry, pool *GachaCatalog) (int, error) {
lbRows, err := utils.ReadJSON[lbMaterialRow]("EntityMCostumeLimitBreakMaterialGroupTable.json")
if err != nil {
return 0, err
}
groupToMaterial := make(map[int32]int32, len(lbRows))
for _, r := range lbRows {
groupToMaterial[r.CostumeLimitBreakMaterialGroupId] = r.MaterialId
}
costumeRows, err := utils.ReadJSON[costumeLBRef]("EntityMCostumeTable.json")
if err != nil {
return 0, err
}
costumeLBGroup := make(map[int32]int32, len(costumeRows))
for _, r := range costumeRows {
costumeLBGroup[r.CostumeId] = r.CostumeLimitBreakMaterialGroupId
}
added := 0
for costumeId := range pool.CostumeById {
if _, exists := dupMap[costumeId]; exists {
continue
}
matId := groupToMaterial[costumeLBGroup[costumeId]]
if matId == 0 {
continue
}
dupMap[costumeId] = []model.DupExchangeEntry{{
PossessionType: int32(model.PossessionTypeMaterial),
PossessionId: matId,
Count: dupExchangeFallbackCount,
}}
added++
}
return added, nil
}
+90
View File
@@ -0,0 +1,90 @@
package masterdata
import (
"fmt"
"sort"
"lunar-tear/server/internal/utils"
)
type ExploreRow struct {
ExploreId int32 `json:"ExploreId"`
ConsumeItemCount int32 `json:"ConsumeItemCount"`
RewardLotteryCount int32 `json:"RewardLotteryCount"`
}
type ExploreGradeScoreRow struct {
ExploreId int32 `json:"ExploreId"`
NecessaryScore int32 `json:"NecessaryScore"`
ExploreGradeId int32 `json:"ExploreGradeId"`
}
type ExploreGradeAssetRow struct {
ExploreGradeId int32 `json:"ExploreGradeId"`
AssetGradeIconId int32 `json:"AssetGradeIconId"`
}
type ExploreCatalog struct {
Explores map[int32]ExploreRow
GradeScores map[int32][]ExploreGradeScoreRow // keyed by ExploreId, sorted desc by NecessaryScore
GradeAssets map[int32]int32 // gradeId -> assetGradeIconId
}
func LoadExploreCatalog() (*ExploreCatalog, error) {
explores, err := utils.ReadJSON[ExploreRow]("EntityMExploreTable.json")
if err != nil {
return nil, fmt.Errorf("load explore table: %w", err)
}
gradeScores, err := utils.ReadJSON[ExploreGradeScoreRow]("EntityMExploreGradeScoreTable.json")
if err != nil {
return nil, fmt.Errorf("load explore grade score table: %w", err)
}
gradeAssets, err := utils.ReadJSON[ExploreGradeAssetRow]("EntityMExploreGradeAssetTable.json")
if err != nil {
return nil, fmt.Errorf("load explore grade asset table: %w", err)
}
catalog := &ExploreCatalog{
Explores: make(map[int32]ExploreRow, len(explores)),
GradeScores: make(map[int32][]ExploreGradeScoreRow),
GradeAssets: make(map[int32]int32, len(gradeAssets)),
}
for _, e := range explores {
catalog.Explores[e.ExploreId] = e
}
for _, gs := range gradeScores {
catalog.GradeScores[gs.ExploreId] = append(catalog.GradeScores[gs.ExploreId], gs)
}
for eid := range catalog.GradeScores {
rows := catalog.GradeScores[eid]
sort.Slice(rows, func(i, j int) bool {
return rows[i].NecessaryScore > rows[j].NecessaryScore
})
catalog.GradeScores[eid] = rows
}
for _, ga := range gradeAssets {
catalog.GradeAssets[ga.ExploreGradeId] = ga.AssetGradeIconId
}
return catalog, nil
}
// GradeForScore returns the AssetGradeIconId for the given explore and score.
// Returns 0 if no matching grade is found.
func (c *ExploreCatalog) GradeForScore(exploreId, score int32) int32 {
rows, ok := c.GradeScores[exploreId]
if !ok {
return 0
}
for _, r := range rows {
if score >= r.NecessaryScore {
return c.GradeAssets[r.ExploreGradeId]
}
}
return 0
}
+364
View File
@@ -0,0 +1,364 @@
package masterdata
import (
"fmt"
"sort"
"strings"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/utils"
)
type gachaMedalRow struct {
GachaMedalId int32 `json:"GachaMedalId"`
ShopTransitionGachaId int32 `json:"ShopTransitionGachaId"`
ConsumableItemId int32 `json:"ConsumableItemId"`
AutoConvertDatetime int64 `json:"AutoConvertDatetime"`
ConversionRate int32 `json:"ConversionRate"`
}
type momBannerRow struct {
MomBannerId int32 `json:"MomBannerId"`
SortOrderDesc int32 `json:"SortOrderDesc"`
DestinationDomainType int32 `json:"DestinationDomainType"`
DestinationDomainId int32 `json:"DestinationDomainId"`
BannerAssetName string `json:"BannerAssetName"`
StartDatetime int64 `json:"StartDatetime"`
EndDatetime int64 `json:"EndDatetime"`
}
type GachaMedalInfo struct {
GachaMedalId int32
ConsumableItemId int32
AutoConvertDatetime int64
ConversionRate int32
}
const chapterGachaIdBase int32 = 200000
func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, error) {
medals, err := utils.ReadJSON[gachaMedalRow]("EntityMGachaMedalTable.json")
if err != nil {
return nil, nil, fmt.Errorf("load gacha medal table: %w", err)
}
banners, err := utils.ReadJSON[momBannerRow]("EntityMMomBannerTable.json")
if err != nil {
return nil, nil, fmt.Errorf("load mom banner table: %w", err)
}
gachaToMedal := make(map[int32]gachaMedalRow)
medalInfoByGacha := make(map[int32]GachaMedalInfo)
for _, m := range medals {
gachaToMedal[m.ShopTransitionGachaId] = m
medalInfoByGacha[m.ShopTransitionGachaId] = GachaMedalInfo{
GachaMedalId: m.GachaMedalId,
ConsumableItemId: m.ConsumableItemId,
AutoConvertDatetime: m.AutoConvertDatetime,
ConversionRate: m.ConversionRate,
}
}
stepupSteps := make(map[int32][]momBannerRow)
var entries []store.GachaCatalogEntry
for _, b := range banners {
if b.DestinationDomainType != model.MomBannerDomainGacha {
continue
}
gachaId := b.DestinationDomainId
if strings.HasPrefix(b.BannerAssetName, model.BannerPrefixStepUp) {
if _, hasMedal := gachaToMedal[gachaId]; !hasMedal {
continue
}
groupId := gachaId / model.StepUpGroupDivisor
stepupSteps[groupId] = append(stepupSteps[groupId], b)
continue
}
labelType := model.GachaLabelPremium
modeType := model.GachaModeBasic
decoration := model.GachaDecorationNormal
isChapter := strings.HasPrefix(b.BannerAssetName, model.BannerPrefixCommon)
if strings.HasPrefix(b.BannerAssetName, model.BannerPrefixLimited) {
decoration = model.GachaDecorationFestival
}
if isChapter {
labelType = model.GachaLabelChapter
modeType = model.GachaModeBox
}
medal, hasMedal := gachaToMedal[gachaId]
if !hasMedal && !isChapter {
continue
}
var medalId int32
var medalConsumableId int32
var ceilingCount int32
if hasMedal {
medalId = medal.GachaMedalId
medalConsumableId = medal.ConsumableItemId
ceilingCount = model.PityCeilingCount
}
var pricePhases []store.GachaPricePhaseEntry
if isChapter {
pricePhases = buildChapterPricePhases(gachaId)
} else {
pricePhases = buildPremiumBasicPricePhases(gachaId)
}
relMainQuest := int32(0)
if isChapter {
relMainQuest = gachaId - chapterGachaIdBase
}
var descriptionTextId int32
if isChapter {
descriptionTextId = gachaId
}
entries = append(entries, store.GachaCatalogEntry{
GachaId: gachaId,
GachaLabelType: labelType,
GachaModeType: modeType,
GachaAutoResetType: model.GachaAutoResetNone,
IsUserGachaUnlock: true,
StartDatetime: b.StartDatetime,
EndDatetime: b.EndDatetime,
RelatedMainQuestChapterId: relMainQuest,
GachaMedalId: medalId,
MedalConsumableItemId: medalConsumableId,
GachaDecorationType: decoration,
SortOrder: b.SortOrderDesc,
BannerAssetName: b.BannerAssetName,
GroupId: gachaId,
CeilingCount: ceilingCount,
PricePhases: pricePhases,
DescriptionTextId: descriptionTextId,
})
}
for groupId, steps := range stepupSteps {
first := steps[0]
gachaId := groupId
medal := gachaToMedal[first.DestinationDomainId]
medalId := medal.GachaMedalId
medalConsumableId := medal.ConsumableItemId
pricePhases := buildStepUpPricePhases(gachaId, len(steps))
var maxStep int32
for _, p := range pricePhases {
if p.StepNumber > maxStep {
maxStep = p.StepNumber
}
}
entries = append(entries, store.GachaCatalogEntry{
GachaId: gachaId,
GachaLabelType: model.GachaLabelPremium,
GachaModeType: model.GachaModeStepup,
GachaAutoResetType: model.GachaAutoResetNone,
IsUserGachaUnlock: true,
StartDatetime: first.StartDatetime,
EndDatetime: first.EndDatetime,
GachaMedalId: medalId,
MedalConsumableItemId: medalConsumableId,
GachaDecorationType: model.GachaDecorationFestival,
SortOrder: first.SortOrderDesc,
BannerAssetName: first.BannerAssetName,
GroupId: groupId,
CeilingCount: model.PityCeilingCount,
PricePhases: pricePhases,
MaxStepNumber: maxStep,
})
}
return entries, medalInfoByGacha, nil
}
const chapterPromoMaxItems = 4
const maxSlideFeatured = 13
func EnrichCatalogPromotions(entries []store.GachaCatalogEntry, pool *GachaCatalog) {
for i := range entries {
if entries[i].GachaLabelType == model.GachaLabelChapter {
entries[i].PromotionItems = buildChapterPromotionItems(pool.Materials)
continue
}
featured := pool.FeaturedByGacha[entries[i].GachaId]
maxRarity := int32(0)
for _, c := range featured.Costumes {
if c.RarityType > maxRarity {
maxRarity = c.RarityType
}
}
for _, w := range featured.Weapons {
if w.RarityType > maxRarity {
maxRarity = w.RarityType
}
}
var topCostumes []GachaPoolItem
for _, c := range featured.Costumes {
if c.RarityType == maxRarity {
topCostumes = append(topCostumes, c)
}
}
var topWeapons []GachaPoolItem
for _, w := range featured.Weapons {
if w.RarityType == maxRarity {
topWeapons = append(topWeapons, w)
}
}
if len(topCostumes)+len(topWeapons) > maxSlideFeatured {
topCostumes = topCostumes[:min(3, len(topCostumes))]
topWeapons = topWeapons[:min(2, len(topWeapons))]
}
var items []store.GachaPromotionItem
if entries[i].GachaModeType == model.GachaModeStepup && len(topCostumes) > 0 {
items = append(items, toPromoItemWithBonus(topCostumes[0], pool))
wid := pool.CostumeWeaponMap[topCostumes[0].PossessionId]
items = append(items, toPromoItem(pool.WeaponById[wid]))
} else {
for _, c := range topCostumes {
items = append(items, toPromoItemWithBonus(c, pool))
}
for _, w := range topWeapons {
items = append(items, toPromoItemWithBonus(w, pool))
}
}
entries[i].PromotionItems = items
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].SortOrder != entries[j].SortOrder {
return entries[i].SortOrder < entries[j].SortOrder
}
return entries[i].GachaId < entries[j].GachaId
})
}
func toPromoItem(item GachaPoolItem) store.GachaPromotionItem {
return store.GachaPromotionItem{
PossessionType: item.PossessionType,
PossessionId: item.PossessionId,
IsTarget: true,
}
}
func toPromoItemWithBonus(item GachaPoolItem, pool *GachaCatalog) store.GachaPromotionItem {
pi := store.GachaPromotionItem{
PossessionType: item.PossessionType,
PossessionId: item.PossessionId,
IsTarget: true,
}
if item.PossessionType == int32(model.PossessionTypeCostume) {
pi.BonusPossessionType = int32(model.PossessionTypeWeapon)
pi.BonusPossessionId = pool.CostumeWeaponMap[item.PossessionId]
}
return pi
}
func buildChapterPromotionItems(materials []GachaPoolItem) []store.GachaPromotionItem {
limit := min(chapterPromoMaxItems, len(materials))
items := make([]store.GachaPromotionItem, 0, limit)
for _, m := range materials[:limit] {
items = append(items, toPromoItem(m))
}
return items
}
func buildPremiumBasicPricePhases(gachaId int32) []store.GachaPricePhaseEntry {
return []store.GachaPricePhaseEntry{
{
PhaseId: gachaId*model.PhaseIdMultiplier + 1,
PriceType: model.PriceTypeGem,
Price: model.PremiumSinglePullPrice,
RegularPrice: model.PremiumSinglePullPrice,
DrawCount: 1,
},
{
PhaseId: gachaId*model.PhaseIdMultiplier + 2,
PriceType: model.PriceTypeGem,
Price: model.PremiumMultiPullPrice,
RegularPrice: model.PremiumMultiPullPrice,
DrawCount: model.PremiumMultiPullCount,
FixedRarityMin: model.RaritySRare,
FixedCount: 1,
},
{
PhaseId: gachaId*model.PhaseIdMultiplier + 3,
PriceType: model.PriceTypeConsumableItem,
PriceId: model.ConsumableIdPremiumTicket,
Price: 1,
RegularPrice: 1,
DrawCount: 1,
},
}
}
func buildStepUpPricePhases(gachaId int32, totalSteps int) []store.GachaPricePhaseEntry {
stepCosts := []int32{model.StepUpStep1Cost, model.StepUpFreeCost, model.StepUpStep3Cost, model.StepUpFreeCost, model.StepUpStep5Cost}
stepCosts = stepCosts[:min(totalSteps, len(stepCosts))]
var phases []store.GachaPricePhaseEntry
for i, cost := range stepCosts {
step := int32(i + 1)
priceType := model.PriceTypePaidGem
if cost == 0 {
priceType = model.PriceTypeGem
}
fixedRarityMin := int32(0)
fixedCount := int32(0)
if step == int32(len(stepCosts)) {
fixedRarityMin = model.RaritySSRare
fixedCount = 1
}
phases = append(phases, store.GachaPricePhaseEntry{
PhaseId: gachaId*model.PhaseIdMultiplier + step,
PriceType: priceType,
Price: cost,
RegularPrice: model.PremiumMultiPullPrice,
DrawCount: model.PremiumMultiPullCount,
FixedRarityMin: fixedRarityMin,
FixedCount: fixedCount,
LimitExecCount: 1,
StepNumber: step,
})
}
return phases
}
func buildChapterPricePhases(gachaId int32) []store.GachaPricePhaseEntry {
return []store.GachaPricePhaseEntry{
{
PhaseId: gachaId*model.PhaseIdMultiplier + 1,
PriceType: model.PriceTypeConsumableItem,
PriceId: model.ConsumableIdChapterTicket,
Price: 1,
RegularPrice: 1,
DrawCount: 1,
},
{
PhaseId: gachaId*model.PhaseIdMultiplier + 2,
PriceType: model.PriceTypeConsumableItem,
PriceId: model.ConsumableIdChapterTicket,
Price: 10,
RegularPrice: 10,
DrawCount: model.PremiumMultiPullCount,
},
}
}
+432
View File
@@ -0,0 +1,432 @@
package masterdata
import (
"fmt"
"log"
"slices"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/utils"
)
type GachaPoolItem struct {
PossessionType int32
PossessionId int32
RarityType model.RarityType
CharacterId int32
}
type FeaturedSet struct {
Costumes []GachaPoolItem
Weapons []GachaPoolItem
}
type BannerPool struct {
CostumesByRarity map[int32][]GachaPoolItem
WeaponsByRarity map[int32][]GachaPoolItem
Featured []GachaPoolItem
}
type ShopFeaturedEntry struct {
CostumeId int32
WeaponId int32
}
type GachaCatalog struct {
CostumesByRarity map[int32][]GachaPoolItem
WeaponsByRarity map[int32][]GachaPoolItem
Materials []GachaPoolItem
CostumeById map[int32]GachaPoolItem
WeaponById map[int32]GachaPoolItem
CostumeWeaponMap map[int32]int32 // costumeId -> paired weaponId
FeaturedByGacha map[int32]FeaturedSet
BannerPools map[int32]*BannerPool
ShopFeaturedByMedal map[int32][]ShopFeaturedEntry // consumableId -> paired entries
}
type costumePoolRow struct {
CostumeId int32 `json:"CostumeId"`
CharacterId int32 `json:"CharacterId"`
SkillfulWeaponType int32 `json:"SkillfulWeaponType"`
RarityType int32 `json:"RarityType"`
}
type weaponPoolRow struct {
WeaponId int32 `json:"WeaponId"`
WeaponType int32 `json:"WeaponType"`
RarityType int32 `json:"RarityType"`
IsRestrictDiscard bool `json:"IsRestrictDiscard"`
}
type catalogCostumeRow struct {
CostumeId int32 `json:"CostumeId"`
CatalogTermId int32 `json:"CatalogTermId"`
}
type catalogWeaponRow struct {
WeaponId int32 `json:"WeaponId"`
CatalogTermId int32 `json:"CatalogTermId"`
}
type materialPoolRow struct {
MaterialId int32 `json:"MaterialId"`
MaterialType int32 `json:"MaterialType"`
RarityType int32 `json:"RarityType"`
}
func LoadGachaPool() (*GachaCatalog, error) {
costumes, err := utils.ReadJSON[costumePoolRow]("EntityMCostumeTable.json")
if err != nil {
return nil, fmt.Errorf("load costume table: %w", err)
}
weapons, err := utils.ReadJSON[weaponPoolRow]("EntityMWeaponTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon table: %w", err)
}
catalogCostumes, err := utils.ReadJSON[catalogCostumeRow]("EntityMCatalogCostumeTable.json")
if err != nil {
return nil, fmt.Errorf("load catalog costume table: %w", err)
}
catalogWeapons, err := utils.ReadJSON[catalogWeaponRow]("EntityMCatalogWeaponTable.json")
if err != nil {
return nil, fmt.Errorf("load catalog weapon table: %w", err)
}
materials, err := utils.ReadJSON[materialPoolRow]("EntityMMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load material table: %w", err)
}
evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon evolution group table: %w", err)
}
evolvedWeapons := buildEvolvedWeaponSet(evoGroupRows)
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
}
}
pool := &GachaCatalog{
CostumesByRarity: make(map[int32][]GachaPoolItem),
WeaponsByRarity: make(map[int32][]GachaPoolItem),
CostumeById: make(map[int32]GachaPoolItem),
WeaponById: make(map[int32]GachaPoolItem),
CostumeWeaponMap: make(map[int32]int32),
FeaturedByGacha: make(map[int32]FeaturedSet),
}
for _, c := range costumes {
if !catalogCostumeSet[c.CostumeId] {
continue
}
if c.RarityType < model.RaritySRare {
continue
}
item := GachaPoolItem{
PossessionType: int32(model.PossessionTypeCostume),
PossessionId: c.CostumeId,
RarityType: c.RarityType,
CharacterId: c.CharacterId,
}
pool.CostumesByRarity[c.RarityType] = append(pool.CostumesByRarity[c.RarityType], item)
pool.CostumeById[c.CostumeId] = item
}
restrictedCount := 0
for _, w := range weapons {
if !catalogWeaponSet[w.WeaponId] {
continue
}
if evolvedWeapons[w.WeaponId] {
continue
}
item := GachaPoolItem{
PossessionType: int32(model.PossessionTypeWeapon),
PossessionId: w.WeaponId,
RarityType: w.RarityType,
}
pool.WeaponById[w.WeaponId] = item
if w.IsRestrictDiscard {
restrictedCount++
continue
}
pool.WeaponsByRarity[w.RarityType] = append(pool.WeaponsByRarity[w.RarityType], item)
}
log.Printf("[GachaPool] excluded %d evolved weapons, %d restricted weapons from pool", len(evolvedWeapons), 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++
}
}
log.Printf("[GachaPool] costume-weapon pairing: %d exact, %d id-pattern, %d best-guess, %d total",
exact, pattern, bestGuess, len(pool.CostumeWeaponMap))
for _, m := range materials {
pool.Materials = append(pool.Materials, GachaPoolItem{
PossessionType: int32(model.PossessionTypeMaterial),
PossessionId: m.MaterialId,
RarityType: m.RarityType,
})
}
return pool, nil
}
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
var entries []ShopFeaturedEntry
for _, cell := range cells {
contents := shop.Contents[cell.ShopItemId]
var costumeId, weaponId int32
for _, c := range contents {
switch c.PossessionType {
case int32(model.PossessionTypeCostume):
costumeId = c.PossessionId
case int32(model.PossessionTypeWeapon):
weaponId = c.PossessionId
}
}
if costumeId == 0 && weaponId == 0 {
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)
}
func (pool *GachaCatalog) PruneUnpairedCostumes() {
pruned := 0
for costumeId := range pool.CostumeById {
if _, ok := pool.CostumeWeaponMap[costumeId]; !ok {
delete(pool.CostumeById, costumeId)
pruned++
}
}
for rarity, items := range pool.CostumesByRarity {
filtered := items[:0]
for _, item := range items {
if _, ok := pool.CostumeWeaponMap[item.PossessionId]; ok {
filtered = append(filtered, item)
}
}
pool.CostumesByRarity[rarity] = filtered
}
log.Printf("[GachaPool] pruned %d unpaired costumes", pruned)
}
func (pool *GachaCatalog) BuildFeaturedMapping(entries []store.GachaCatalogEntry) {
matched := 0
for _, entry := range entries {
if entry.MedalConsumableItemId == 0 {
continue
}
shopEntries, ok := pool.ShopFeaturedByMedal[entry.MedalConsumableItemId]
if !ok || len(shopEntries) == 0 {
continue
}
seenCostume := make(map[int32]bool)
linkedWeapons := make(map[int32]bool)
var costumes []GachaPoolItem
for _, se := range shopEntries {
if se.CostumeId != 0 && !seenCostume[se.CostumeId] {
costumes = append(costumes, pool.CostumeById[se.CostumeId])
seenCostume[se.CostumeId] = true
linkedWeapons[se.WeaponId] = true
}
}
seenWeapon := make(map[int32]bool)
var weapons []GachaPoolItem
for _, se := range shopEntries {
if se.WeaponId != 0 && !linkedWeapons[se.WeaponId] && !seenWeapon[se.WeaponId] {
if item, ok := pool.WeaponById[se.WeaponId]; ok {
weapons = append(weapons, item)
seenWeapon[se.WeaponId] = true
}
}
}
pool.FeaturedByGacha[entry.GachaId] = FeaturedSet{Costumes: costumes, Weapons: weapons}
matched++
}
log.Printf("[GachaPool] featured mapping: %d/%d banners matched via shop", matched, len(entries))
}
func (pool *GachaCatalog) BuildBannerPools(entries []store.GachaCatalogEntry) {
allFeaturedCostumes := make(map[int32]bool)
allFeaturedWeapons := make(map[int32]bool)
for _, fs := range pool.FeaturedByGacha {
for _, c := range fs.Costumes {
allFeaturedCostumes[c.PossessionId] = true
allFeaturedWeapons[pool.CostumeWeaponMap[c.PossessionId]] = true
}
for _, w := range fs.Weapons {
allFeaturedWeapons[w.PossessionId] = true
}
}
commonCostumes := make(map[int32][]GachaPoolItem)
for rarity, items := range pool.CostumesByRarity {
for _, item := range items {
if !allFeaturedCostumes[item.PossessionId] {
commonCostumes[rarity] = append(commonCostumes[rarity], item)
}
}
}
commonWeapons := make(map[int32][]GachaPoolItem)
for rarity, items := range pool.WeaponsByRarity {
for _, item := range items {
if !allFeaturedWeapons[item.PossessionId] {
commonWeapons[rarity] = append(commonWeapons[rarity], item)
}
}
}
commonPool := &BannerPool{
CostumesByRarity: commonCostumes,
WeaponsByRarity: commonWeapons,
}
pool.BannerPools = make(map[int32]*BannerPool)
for _, entry := range entries {
fs, hasFeatured := pool.FeaturedByGacha[entry.GachaId]
if !hasFeatured {
pool.BannerPools[entry.GachaId] = commonPool
continue
}
var allFeatured []GachaPoolItem
bannerCostumes := make(map[int32][]GachaPoolItem)
for rarity, items := range commonCostumes {
bannerCostumes[rarity] = append(bannerCostumes[rarity], items...)
}
bannerWeapons := make(map[int32][]GachaPoolItem)
for rarity, items := range commonWeapons {
bannerWeapons[rarity] = append(bannerWeapons[rarity], items...)
}
for _, c := range fs.Costumes {
bannerCostumes[c.RarityType] = append(bannerCostumes[c.RarityType], c)
allFeatured = append(allFeatured, c)
wid := pool.CostumeWeaponMap[c.PossessionId]
w := pool.WeaponById[wid]
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
allFeatured = append(allFeatured, w)
}
for _, w := range fs.Weapons {
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
allFeatured = append(allFeatured, w)
}
pool.BannerPools[entry.GachaId] = &BannerPool{
CostumesByRarity: bannerCostumes,
WeaponsByRarity: bannerWeapons,
Featured: allFeatured,
}
}
log.Printf("[GachaPool] banner pools: %d banners, %d featured costumes stripped, %d featured weapons stripped",
len(pool.BannerPools), len(allFeaturedCostumes), len(allFeaturedWeapons))
}
func buildEvolvedWeaponSet(rows []WeaponEvolutionGroupRow) map[int32]bool {
grouped := make(map[int32][]WeaponEvolutionGroupRow)
for _, r := range rows {
grouped[r.WeaponEvolutionGroupId] = append(grouped[r.WeaponEvolutionGroupId], r)
}
evolved := make(map[int32]bool)
for _, chain := range grouped {
sort.Slice(chain, func(i, j int) bool {
return chain[i].EvolutionOrder < chain[j].EvolutionOrder
})
for i := 1; i < len(chain); i++ {
evolved[chain[i].WeaponId] = true
}
}
return evolved
}
+76
View File
@@ -0,0 +1,76 @@
package masterdata
import (
"fmt"
"log"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
"lunar-tear/server/internal/utils"
)
type gimmickScheduleRow struct {
GimmickSequenceScheduleId int32 `json:"GimmickSequenceScheduleId"`
StartDatetime int64 `json:"StartDatetime"`
EndDatetime int64 `json:"EndDatetime"`
FirstGimmickSequenceId int32 `json:"FirstGimmickSequenceId"`
ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"`
}
type gimmickScheduleEntry struct {
ScheduleId int32
StartDatetime int64
EndDatetime int64
FirstSequenceId int32
RequiredQuestId int32 // 0 = always active
}
type GimmickCatalog struct {
schedules []gimmickScheduleEntry
}
func LoadGimmickCatalog(resolver *ConditionResolver) (*GimmickCatalog, error) {
rows, err := utils.ReadJSON[gimmickScheduleRow]("EntityMGimmickSequenceScheduleTable.json")
if err != nil {
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
}
entries := make([]gimmickScheduleEntry, 0, len(rows))
for _, r := range rows {
entry := gimmickScheduleEntry{
ScheduleId: r.GimmickSequenceScheduleId,
StartDatetime: r.StartDatetime,
EndDatetime: r.EndDatetime,
FirstSequenceId: r.FirstGimmickSequenceId,
}
if r.ReleaseEvaluateConditionId != 0 {
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
entry.RequiredQuestId = qid
}
}
entries = append(entries, entry)
}
log.Printf("gimmick catalog loaded: %d schedules", len(entries))
return &GimmickCatalog{schedules: entries}, nil
}
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
var keys []store.GimmickSequenceKey
for _, s := range c.schedules {
if nowMillis < s.StartDatetime || nowMillis > s.EndDatetime {
continue
}
if s.RequiredQuestId != 0 {
q, ok := user.Quests[s.RequiredQuestId]
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
continue
}
}
keys = append(keys, store.GimmickSequenceKey{
GimmickSequenceScheduleId: s.ScheduleId,
GimmickSequenceId: s.FirstSequenceId,
})
}
return keys
}
+55
View File
@@ -0,0 +1,55 @@
package masterdata
import (
"log"
"lunar-tear/server/internal/utils"
)
type loginBonusStamp struct {
LoginBonusId int32 `json:"LoginBonusId"`
LowerPageNumber int32 `json:"LowerPageNumber"`
StampNumber int32 `json:"StampNumber"`
RewardPossessionType int32 `json:"RewardPossessionType"`
RewardPossessionId int32 `json:"RewardPossessionId"`
RewardCount int32 `json:"RewardCount"`
}
type loginBonusStampKey struct {
LoginBonusId int32
LowerPageNumber int32
StampNumber int32
}
type LoginBonusReward struct {
PossessionType int32
PossessionId int32
Count int32
}
type LoginBonusCatalog struct {
stamps map[loginBonusStampKey]LoginBonusReward
}
func (c *LoginBonusCatalog) LookupStampReward(loginBonusId, pageNumber, stampNumber int32) (LoginBonusReward, bool) {
entry, ok := c.stamps[loginBonusStampKey{loginBonusId, pageNumber, stampNumber}]
return entry, ok
}
func LoadLoginBonusCatalog() *LoginBonusCatalog {
stamps, err := utils.ReadJSON[loginBonusStamp]("EntityMLoginBonusStampTable.json")
if err != nil {
log.Fatalf("load login bonus stamp table: %v", err)
}
cat := &LoginBonusCatalog{
stamps: make(map[loginBonusStampKey]LoginBonusReward, len(stamps)),
}
for _, s := range stamps {
cat.stamps[loginBonusStampKey{s.LoginBonusId, s.LowerPageNumber, s.StampNumber}] = LoginBonusReward{
PossessionType: s.RewardPossessionType,
PossessionId: s.RewardPossessionId,
Count: s.RewardCount,
}
}
return cat
}
+71
View File
@@ -0,0 +1,71 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type MaterialRow struct {
MaterialId int32 `json:"MaterialId"`
MaterialType model.MaterialType `json:"MaterialType"`
WeaponType int32 `json:"WeaponType"`
EffectValue int32 `json:"EffectValue"`
SellPrice int32 `json:"SellPrice"`
}
type numericalParameterMapRow struct {
NumericalParameterMapId int32 `json:"NumericalParameterMapId"`
ParameterKey int32 `json:"ParameterKey"`
ParameterValue int32 `json:"ParameterValue"`
}
func LoadParameterMap() ([]numericalParameterMapRow, error) {
rows, err := utils.ReadJSON[numericalParameterMapRow]("EntityMNumericalParameterMapTable.json")
if err != nil {
return nil, fmt.Errorf("load numerical parameter map table: %w", err)
}
return rows, nil
}
func BuildExpThresholds(paramMapRows []numericalParameterMapRow, mapId int32) []int32 {
maxKey := int32(0)
for _, r := range paramMapRows {
if r.NumericalParameterMapId == mapId && r.ParameterKey > maxKey {
maxKey = r.ParameterKey
}
}
thresholds := make([]int32, maxKey+1)
for _, r := range paramMapRows {
if r.NumericalParameterMapId == mapId {
thresholds[r.ParameterKey] = r.ParameterValue
}
}
return thresholds
}
type MaterialCatalog struct {
All map[int32]MaterialRow
ByType map[model.MaterialType]map[int32]MaterialRow
}
func LoadMaterialCatalog() (*MaterialCatalog, error) {
rows, err := utils.ReadJSON[MaterialRow]("EntityMMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load material table: %w", err)
}
catalog := &MaterialCatalog{
All: make(map[int32]MaterialRow, len(rows)),
ByType: make(map[model.MaterialType]map[int32]MaterialRow),
}
for _, row := range rows {
catalog.All[row.MaterialId] = row
if catalog.ByType[row.MaterialType] == nil {
catalog.ByType[row.MaterialType] = make(map[int32]MaterialRow)
}
catalog.ByType[row.MaterialType][row.MaterialId] = row
}
return catalog, nil
}
+106
View File
@@ -0,0 +1,106 @@
package masterdata
import (
"fmt"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type numericalFunctionRow struct {
NumericalFunctionId int32 `json:"NumericalFunctionId"`
NumericalFunctionType int32 `json:"NumericalFunctionType"`
NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"`
}
type numericalFunctionParameterRow struct {
NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"`
ParameterIndex int32 `json:"ParameterIndex"`
ParameterValue int32 `json:"ParameterValue"`
}
type NumericalFunc struct {
Type model.NumericalFunctionType
Params []int32
}
func (f NumericalFunc) Evaluate(value int32) int32 {
p := f.Params
switch f.Type {
case model.NumericalFunctionTypeLinear:
return p[1] + p[0]*value
case model.NumericalFunctionTypeMonomial:
v := value - 1
result := v
counter := p[1]
if counter > 1 {
counter--
for counter > 0 {
counter--
result *= v
}
}
return result * p[0]
case model.NumericalFunctionTypeLinearPermil:
return p[0]*value/1000 + p[1]
case model.NumericalFunctionTypePolynomialThird:
return p[3] + (p[2]+(p[1]+p[0]*value)*value)*value
case model.NumericalFunctionTypePolynomialThirdPermil:
return p[0]*value*value*value/1000 +
p[1]*value*value/1000 +
p[2]*value/1000 +
p[3]
default:
return 0
}
}
type FunctionResolver struct {
functions map[int32]NumericalFunc
}
func LoadFunctionResolver() (*FunctionResolver, error) {
funcRows, err := utils.ReadJSON[numericalFunctionRow]("EntityMNumericalFunctionTable.json")
if err != nil {
return nil, fmt.Errorf("load numerical function table: %w", err)
}
paramRows, err := utils.ReadJSON[numericalFunctionParameterRow]("EntityMNumericalFunctionParameterGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load numerical function parameter group table: %w", err)
}
paramsByGroup := make(map[int32][]numericalFunctionParameterRow, len(paramRows))
for _, r := range paramRows {
paramsByGroup[r.NumericalFunctionParameterGroupId] = append(
paramsByGroup[r.NumericalFunctionParameterGroupId], r)
}
for _, group := range paramsByGroup {
sort.Slice(group, func(i, j int) bool {
return group[i].ParameterIndex < group[j].ParameterIndex
})
}
functions := make(map[int32]NumericalFunc, len(funcRows))
for _, fr := range funcRows {
group := paramsByGroup[fr.NumericalFunctionParameterGroupId]
params := make([]int32, len(group))
for _, pr := range group {
if int(pr.ParameterIndex) < len(params) {
params[pr.ParameterIndex] = pr.ParameterValue
}
}
functions[fr.NumericalFunctionId] = NumericalFunc{
Type: model.NumericalFunctionType(fr.NumericalFunctionType),
Params: params,
}
}
return &FunctionResolver{functions: functions}, nil
}
func (r *FunctionResolver) Resolve(functionId int32) (NumericalFunc, bool) {
f, ok := r.functions[functionId]
return f, ok
}
+37
View File
@@ -0,0 +1,37 @@
package masterdata
import (
"log"
"lunar-tear/server/internal/utils"
)
type omikujiEntry struct {
OmikujiId int32 `json:"OmikujiId"`
OmikujiAssetId int32 `json:"OmikujiAssetId"`
}
type OmikujiCatalog struct {
assetIds map[int32]int32
}
func (c *OmikujiCatalog) LookupAssetId(omikujiId int32) int32 {
if id, ok := c.assetIds[omikujiId]; ok {
return id
}
return 0
}
func LoadOmikujiCatalog() *OmikujiCatalog {
entries, err := utils.ReadJSON[omikujiEntry]("EntityMOmikujiTable.json")
if err != nil {
log.Fatalf("load omikuji table: %v", err)
}
cat := &OmikujiCatalog{
assetIds: make(map[int32]int32, len(entries)),
}
for _, e := range entries {
cat.assetIds[e.OmikujiId] = e.OmikujiAssetId
}
return cat
}
+120
View File
@@ -0,0 +1,120 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type PartsRow struct {
PartsId int32 `json:"PartsId"`
RarityType model.RarityType `json:"RarityType"`
PartsGroupId int32 `json:"PartsGroupId"`
PartsStatusMainLotteryGroupId int32 `json:"PartsStatusMainLotteryGroupId"`
}
type PartsRarityRow struct {
RarityType model.RarityType `json:"RarityType"`
PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"`
PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"`
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
}
type partsLevelUpRateRow struct {
PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"`
LevelLowerLimit int32 `json:"LevelLowerLimit"`
SuccessRatePermil int32 `json:"SuccessRatePermil"`
}
type partsLevelUpPriceRow struct {
PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"`
LevelLowerLimit int32 `json:"LevelLowerLimit"`
Gold int32 `json:"Gold"`
}
type PartsCatalog struct {
PartsById map[int32]PartsRow
DefaultPartsStatusMainByLotteryGroup map[int32]int32
RarityByRarityType map[model.RarityType]PartsRarityRow
RateByGroupAndLevel map[int32]map[int32]int32
PriceByGroupAndLevel map[int32]map[int32]int32
SellPriceByRarity map[model.RarityType]NumericalFunc
}
func LoadPartsCatalog() (*PartsCatalog, error) {
partsRows, err := utils.ReadJSON[PartsRow]("EntityMPartsTable.json")
if err != nil {
return nil, fmt.Errorf("load parts table: %w", err)
}
rarityRows, err := utils.ReadJSON[PartsRarityRow]("EntityMPartsRarityTable.json")
if err != nil {
return nil, fmt.Errorf("load parts rarity table: %w", err)
}
rateRows, err := utils.ReadJSON[partsLevelUpRateRow]("EntityMPartsLevelUpRateGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load parts level up rate table: %w", err)
}
priceRows, err := utils.ReadJSON[partsLevelUpPriceRow]("EntityMPartsLevelUpPriceGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load parts level up price table: %w", err)
}
partsById := make(map[int32]PartsRow, len(partsRows))
for _, p := range partsRows {
partsById[p.PartsId] = p
}
// Lottery group ID encodes tier (first digit 1-4) and stat category
// (second digit 1-6). Formula: mainStatId = (category - 1) * 4 + tier.
defaultPartsStatusMainByLotteryGroup := make(map[int32]int32, 24)
for tier := int32(1); tier <= 4; tier++ {
for cat := int32(1); cat <= 6; cat++ {
groupId := tier*10 + cat
mainStatId := (cat-1)*4 + tier
defaultPartsStatusMainByLotteryGroup[groupId] = mainStatId
}
}
funcResolver, err := LoadFunctionResolver()
if err != nil {
return nil, fmt.Errorf("load function resolver: %w", err)
}
rarityByRarityType := make(map[model.RarityType]PartsRarityRow, len(rarityRows))
sellPriceByRarity := make(map[model.RarityType]NumericalFunc, len(rarityRows))
for _, r := range rarityRows {
rarityByRarityType[r.RarityType] = r
if f, ok := funcResolver.Resolve(r.SellPriceNumericalFunctionId); ok {
sellPriceByRarity[r.RarityType] = f
}
}
rateByGroupAndLevel := make(map[int32]map[int32]int32)
for _, r := range rateRows {
if rateByGroupAndLevel[r.PartsLevelUpRateGroupId] == nil {
rateByGroupAndLevel[r.PartsLevelUpRateGroupId] = make(map[int32]int32)
}
rateByGroupAndLevel[r.PartsLevelUpRateGroupId][r.LevelLowerLimit] = r.SuccessRatePermil
}
priceByGroupAndLevel := make(map[int32]map[int32]int32)
for _, p := range priceRows {
if priceByGroupAndLevel[p.PartsLevelUpPriceGroupId] == nil {
priceByGroupAndLevel[p.PartsLevelUpPriceGroupId] = make(map[int32]int32)
}
priceByGroupAndLevel[p.PartsLevelUpPriceGroupId][p.LevelLowerLimit] = p.Gold
}
return &PartsCatalog{
PartsById: partsById,
DefaultPartsStatusMainByLotteryGroup: defaultPartsStatusMainByLotteryGroup,
RarityByRarityType: rarityByRarityType,
RateByGroupAndLevel: rateByGroupAndLevel,
PriceByGroupAndLevel: priceByGroupAndLevel,
SellPriceByRarity: sellPriceByRarity,
}, nil
}
+727
View File
@@ -0,0 +1,727 @@
package masterdata
import (
"fmt"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type QuestSceneRow struct {
QuestSceneId int32 `json:"QuestSceneId"`
QuestId int32 `json:"QuestId"`
SortOrder int32 `json:"SortOrder"`
QuestSceneType model.QuestSceneType `json:"QuestSceneType"`
AssetBackgroundId int32 `json:"AssetBackgroundId"`
EventMapNumberUpper int32 `json:"EventMapNumberUpper"`
EventMapNumberLower int32 `json:"EventMapNumberLower"`
IsMainFlowQuestTarget bool `json:"IsMainFlowQuestTarget"`
IsBattleOnlyTarget bool `json:"IsBattleOnlyTarget"`
QuestResultType model.QuestResultType `json:"QuestResultType"`
IsStorySkipTarget bool `json:"IsStorySkipTarget"`
}
type QuestRow struct {
QuestId int32 `json:"QuestId"`
NameQuestTextId int32 `json:"NameQuestTextId"`
PictureBookNameQuestTextId int32 `json:"PictureBookNameQuestTextId"`
QuestReleaseConditionListId int32 `json:"QuestReleaseConditionListId"`
StoryQuestTextId int32 `json:"StoryQuestTextId"`
QuestDisplayAttributeGroupId int32 `json:"QuestDisplayAttributeGroupId"`
RecommendedDeckPower int32 `json:"RecommendedDeckPower"`
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"`
QuestDeckRestrictionGroupId int32 `json:"QuestDeckRestrictionGroupId"`
QuestMissionGroupId int32 `json:"QuestMissionGroupId"`
Stamina int32 `json:"Stamina"`
UserExp int32 `json:"UserExp"`
CharacterExp int32 `json:"CharacterExp"`
CostumeExp int32 `json:"CostumeExp"`
Gold int32 `json:"Gold"`
DailyClearableCount int32 `json:"DailyClearableCount"`
IsRunInTheBackground bool `json:"IsRunInTheBackground"`
IsCountedAsQuest bool `json:"IsCountedAsQuest"`
QuestBonusId int32 `json:"QuestBonusId"`
IsNotShowAfterClear bool `json:"IsNotShowAfterClear"`
IsBigWinTarget bool `json:"IsBigWinTarget"`
IsUsableSkipTicket bool `json:"IsUsableSkipTicket"`
QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"`
InvisibleQuestMissionGroupId int32 `json:"InvisibleQuestMissionGroupId"`
FieldEffectGroupId int32 `json:"FieldEffectGroupId"`
}
type QuestMissionRow struct {
QuestMissionId int32 `json:"QuestMissionId"`
QuestMissionConditionType model.QuestMissionConditionType `json:"QuestMissionConditionType"`
QuestMissionRewardId int32 `json:"QuestMissionRewardId"`
QuestMissionConditionValueGroupId int32 `json:"QuestMissionConditionValueGroupId"`
}
type QuestMissionGroupRow struct {
QuestMissionGroupId int32 `json:"QuestMissionGroupId"`
SortOrder int32 `json:"SortOrder"`
QuestMissionId int32 `json:"QuestMissionId"`
}
type QuestMissionRewardRow struct {
QuestMissionRewardId int32 `json:"QuestMissionRewardId"`
PossessionType model.PossessionType `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type MainQuestSequenceRow struct {
MainQuestSequenceId int32 `json:"MainQuestSequenceId"`
SortOrder int32 `json:"SortOrder"`
QuestId int32 `json:"QuestId"`
}
type MainQuestRouteRow struct {
MainQuestRouteId int32 `json:"MainQuestRouteId"`
MainQuestSeasonId int32 `json:"MainQuestSeasonId"`
SortOrder int32 `json:"SortOrder"`
CharacterId int32 `json:"CharacterId"`
}
type MainQuestChapterRow struct {
MainQuestChapterId int32 `json:"MainQuestChapterId"`
MainQuestRouteId int32 `json:"MainQuestRouteId"`
SortOrder int32 `json:"SortOrder"`
MainQuestSequenceGroupId int32 `json:"MainQuestSequenceGroupId"`
PortalCageCharacterGroupId int32 `json:"PortalCageCharacterGroupId"`
StartDatetime int64 `json:"StartDatetime"`
IsInvisibleInLibrary bool `json:"IsInvisibleInLibrary"`
JoinLibraryChapterId int32 `json:"JoinLibraryChapterId"`
}
type QuestFirstClearRewardSwitchRow struct {
QuestId int32 `json:"QuestId"`
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
SwitchConditionClearQuestId int32 `json:"SwitchConditionClearQuestId"`
}
type QuestFirstClearRewardGroupRow struct {
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
QuestFirstClearRewardType int32 `json:"QuestFirstClearRewardType"`
SortOrder int32 `json:"SortOrder"`
PossessionType model.PossessionType `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
IsPickup bool `json:"IsPickup"`
}
type QuestReplayFlowRewardGroupRow struct {
QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"`
SortOrder int32 `json:"SortOrder"`
PossessionType model.PossessionType `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type QuestSceneGrantRow struct {
QuestSceneId int32 `json:"QuestSceneId"`
PossessionType model.PossessionType `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type QuestPickupRewardGroupRow struct {
QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"`
SortOrder int32 `json:"SortOrder"`
BattleDropRewardId int32 `json:"BattleDropRewardId"`
}
type BattleDropRewardRow struct {
BattleDropRewardId int32 `json:"BattleDropRewardId"`
PossessionType model.PossessionType `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type QuestSceneBattleRow struct {
QuestSceneId int32 `json:"QuestSceneId"`
BattleGroupId int32 `json:"BattleGroupId"`
}
type BattleGroupRow struct {
BattleGroupId int32 `json:"BattleGroupId"`
WaveNumber int32 `json:"WaveNumber"`
BattleId int32 `json:"BattleId"`
}
type BattleRow struct {
BattleId int32 `json:"BattleId"`
BattleNpcId int32 `json:"BattleNpcId"`
DeckType model.DeckType `json:"DeckType"`
BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"`
}
type BattleNpcDeckRow struct {
BattleNpcId int32 `json:"BattleNpcId"`
DeckType model.DeckType `json:"DeckType"`
BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"`
BattleNpcDeckCharacterUuid01 string `json:"BattleNpcDeckCharacterUuid01"`
BattleNpcDeckCharacterUuid02 string `json:"BattleNpcDeckCharacterUuid02"`
BattleNpcDeckCharacterUuid03 string `json:"BattleNpcDeckCharacterUuid03"`
}
type BattleNpcDropCategoryRow struct {
BattleNpcId int32 `json:"BattleNpcId"`
BattleNpcDeckCharacterUuid string `json:"BattleNpcDeckCharacterUuid"`
BattleDropCategoryId int32 `json:"BattleDropCategoryId"`
}
type BattleDropInfo struct {
QuestSceneId int32
BattleDropCategoryId int32
}
type TutorialUnlockConditionRow struct {
TutorialType int32 `json:"TutorialType"`
TutorialUnlockConditionType int32 `json:"TutorialUnlockConditionType"`
ConditionValue int32 `json:"ConditionValue"`
}
type RentalDeckRow struct {
BattleGroupId int32 `json:"BattleGroupId"`
}
type UserLevelRow struct {
UserLevel int32 `json:"UserLevel"`
MaxStamina int32 `json:"MaxStamina"`
}
type QuestCatalog struct {
SceneById map[int32]QuestSceneRow
MissionById map[int32]QuestMissionRow
QuestById map[int32]QuestRow
MissionIdsByQuestId map[int32][]int32
RouteIdByQuestId map[int32]int32
SceneIdsByQuestId map[int32][]int32
OrderedQuestIds []int32
FirstClearRewardsByGroupId map[int32][]QuestFirstClearRewardGroupRow
FirstClearRewardSwitchesByQuestId map[int32][]QuestFirstClearRewardSwitchRow
MissionRewardsByMissionId map[int32][]QuestMissionRewardRow
WeaponIdsByReleaseConditionGroupId map[int32][]int32
ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow
SceneGrantsBySceneId map[int32][]QuestSceneGrantRow
BattleDropRewardById map[int32]BattleDropRewardRow
PickupRewardIdsByGroupId map[int32][]int32
BattleDropsByQuestId map[int32][]BattleDropInfo
ReplayFlowRewardsByGroupId map[int32][]QuestReplayFlowRewardGroupRow
RentalQuestIds map[int32]bool
TutorialUnlockConditions []TutorialUnlockConditionRow
ChapterLastSceneByQuestId map[int32]int32
SeasonIdByRouteId map[int32]int32
UserExpThresholds []int32
CharacterExpThresholds []int32
CostumeExpByRarity map[int32][]int32
CostumeMaxLevelByRarity map[int32]NumericalFunc
MaxStaminaByLevel map[int32]int32
CostumeById map[int32]CostumeMasterRow
WeaponById map[int32]WeaponMasterRow
WeaponSkillSlots map[int32][]int32
WeaponAbilitySlots map[int32][]int32
*PartsCatalog
}
func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
scenes, err := utils.ReadJSON[QuestSceneRow]("EntityMQuestSceneTable.json")
if err != nil {
return nil, fmt.Errorf("load quest scene table: %w", err)
}
sort.Slice(scenes, func(i, j int) bool {
if scenes[i].QuestId != scenes[j].QuestId {
return scenes[i].QuestId < scenes[j].QuestId
}
if scenes[i].SortOrder != scenes[j].SortOrder {
return scenes[i].SortOrder < scenes[j].SortOrder
}
return scenes[i].QuestSceneId < scenes[j].QuestSceneId
})
missions, err := utils.ReadJSON[QuestMissionRow]("EntityMQuestMissionTable.json")
if err != nil {
return nil, fmt.Errorf("load quest mission table: %w", err)
}
quests, err := utils.ReadJSON[QuestRow]("EntityMQuestTable.json")
if err != nil {
return nil, fmt.Errorf("load quest table: %w", err)
}
missionGroups, err := utils.ReadJSON[QuestMissionGroupRow]("EntityMQuestMissionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load quest mission group table: %w", err)
}
sort.Slice(missionGroups, func(i, j int) bool {
if missionGroups[i].QuestMissionGroupId != missionGroups[j].QuestMissionGroupId {
return missionGroups[i].QuestMissionGroupId < missionGroups[j].QuestMissionGroupId
}
if missionGroups[i].SortOrder != missionGroups[j].SortOrder {
return missionGroups[i].SortOrder < missionGroups[j].SortOrder
}
return missionGroups[i].QuestMissionId < missionGroups[j].QuestMissionId
})
sequences, err := utils.ReadJSON[MainQuestSequenceRow]("EntityMMainQuestSequenceTable.json")
if err != nil {
return nil, fmt.Errorf("load main quest sequence table: %w", err)
}
sort.Slice(sequences, func(i, j int) bool {
if sequences[i].MainQuestSequenceId != sequences[j].MainQuestSequenceId {
return sequences[i].MainQuestSequenceId < sequences[j].MainQuestSequenceId
}
if sequences[i].SortOrder != sequences[j].SortOrder {
return sequences[i].SortOrder < sequences[j].SortOrder
}
return sequences[i].QuestId < sequences[j].QuestId
})
chapters, err := utils.ReadJSON[MainQuestChapterRow]("EntityMMainQuestChapterTable.json")
if err != nil {
return nil, fmt.Errorf("load main quest chapter table: %w", err)
}
routes, err := utils.ReadJSON[MainQuestRouteRow]("EntityMMainQuestRouteTable.json")
if err != nil {
return nil, fmt.Errorf("load main quest route table: %w", err)
}
seasonIdByRouteId := make(map[int32]int32, len(routes))
for _, r := range routes {
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
}
firstClearSwitches, err := utils.ReadJSON[QuestFirstClearRewardSwitchRow]("EntityMQuestFirstClearRewardSwitchTable.json")
if err != nil {
return nil, fmt.Errorf("load quest first clear reward switch table: %w", err)
}
firstClearRewards, err := utils.ReadJSON[QuestFirstClearRewardGroupRow]("EntityMQuestFirstClearRewardGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load quest first clear reward group table: %w", err)
}
sort.Slice(firstClearRewards, func(i, j int) bool {
if firstClearRewards[i].QuestFirstClearRewardGroupId != firstClearRewards[j].QuestFirstClearRewardGroupId {
return firstClearRewards[i].QuestFirstClearRewardGroupId < firstClearRewards[j].QuestFirstClearRewardGroupId
}
if firstClearRewards[i].SortOrder != firstClearRewards[j].SortOrder {
return firstClearRewards[i].SortOrder < firstClearRewards[j].SortOrder
}
return firstClearRewards[i].QuestFirstClearRewardType < firstClearRewards[j].QuestFirstClearRewardType
})
replayFlowRewards, err := utils.ReadJSON[QuestReplayFlowRewardGroupRow]("EntityMQuestReplayFlowRewardGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load quest replay flow reward group table: %w", err)
}
sort.Slice(replayFlowRewards, func(i, j int) bool {
if replayFlowRewards[i].QuestReplayFlowRewardGroupId != replayFlowRewards[j].QuestReplayFlowRewardGroupId {
return replayFlowRewards[i].QuestReplayFlowRewardGroupId < replayFlowRewards[j].QuestReplayFlowRewardGroupId
}
return replayFlowRewards[i].SortOrder < replayFlowRewards[j].SortOrder
})
missionRewards, err := utils.ReadJSON[QuestMissionRewardRow]("EntityMQuestMissionRewardTable.json")
if err != nil {
return nil, fmt.Errorf("load quest mission reward table: %w", err)
}
weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon table: %w", err)
}
weaponSkillGroups, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon skill group table: %w", err)
}
weaponAbilityGroups, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon ability group table: %w", err)
}
releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon story release condition table: %w", err)
}
costumeMasters, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json")
if err != nil {
return nil, fmt.Errorf("load costume table: %w", err)
}
costumeRarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json")
if err != nil {
return nil, fmt.Errorf("load costume rarity table: %w", err)
}
sceneGrants, err := utils.ReadJSON[QuestSceneGrantRow]("EntityMUserQuestSceneGrantPossessionTable.json")
if err != nil {
return nil, fmt.Errorf("load quest scene grant table: %w", err)
}
battleDropRewards, err := utils.ReadJSON[BattleDropRewardRow]("EntityMBattleDropRewardTable.json")
if err != nil {
return nil, fmt.Errorf("load battle drop reward table: %w", err)
}
pickupRewardGroups, err := utils.ReadJSON[QuestPickupRewardGroupRow]("EntityMQuestPickupRewardGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load quest pickup reward group table: %w", err)
}
sort.Slice(pickupRewardGroups, func(i, j int) bool {
if pickupRewardGroups[i].QuestPickupRewardGroupId != pickupRewardGroups[j].QuestPickupRewardGroupId {
return pickupRewardGroups[i].QuestPickupRewardGroupId < pickupRewardGroups[j].QuestPickupRewardGroupId
}
return pickupRewardGroups[i].SortOrder < pickupRewardGroups[j].SortOrder
})
sceneBattles, err := utils.ReadJSON[QuestSceneBattleRow]("EntityMQuestSceneBattleTable.json")
if err != nil {
return nil, fmt.Errorf("load quest scene battle table: %w", err)
}
battleGroups, err := utils.ReadJSON[BattleGroupRow]("EntityMBattleGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load battle group table: %w", err)
}
battles, err := utils.ReadJSON[BattleRow]("EntityMBattleTable.json")
if err != nil {
return nil, fmt.Errorf("load battle table: %w", err)
}
npcDecks, err := utils.ReadJSON[BattleNpcDeckRow]("EntityMBattleNpcDeckTable.json")
if err != nil {
return nil, fmt.Errorf("load battle npc deck table: %w", err)
}
npcDropCategories, err := utils.ReadJSON[BattleNpcDropCategoryRow]("EntityMBattleNpcDeckCharacterDropCategoryTable.json")
if err != nil {
return nil, fmt.Errorf("load battle npc drop category table: %w", err)
}
rentalDecks, err := utils.ReadJSON[RentalDeckRow]("EntityMBattleRentalDeckTable.json")
if err != nil {
return nil, fmt.Errorf("load battle rental deck table: %w", err)
}
tutorialUnlockConds, err := utils.ReadJSON[TutorialUnlockConditionRow]("EntityMTutorialUnlockConditionTable.json")
if err != nil {
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
}
paramMapRows, err := LoadParameterMap()
if err != nil {
return nil, err
}
userLevels, err := utils.ReadJSON[UserLevelRow]("EntityMUserLevelTable.json")
if err != nil {
return nil, fmt.Errorf("load user level table: %w", err)
}
maxStaminaByLevel := make(map[int32]int32, len(userLevels))
for _, ul := range userLevels {
maxStaminaByLevel[ul.UserLevel] = ul.MaxStamina
}
funcResolver, err := LoadFunctionResolver()
if err != nil {
return nil, fmt.Errorf("load function resolver: %w", err)
}
costumeExpByRarity := make(map[int32][]int32, len(costumeRarities))
costumeMaxLevelByRarity := make(map[int32]NumericalFunc, len(costumeRarities))
for _, r := range costumeRarities {
if _, ok := costumeExpByRarity[r.RarityType]; !ok {
costumeExpByRarity[r.RarityType] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
}
if _, ok := costumeMaxLevelByRarity[r.RarityType]; !ok {
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
costumeMaxLevelByRarity[r.RarityType] = f
}
}
}
costumeById := make(map[int32]CostumeMasterRow, len(costumeMasters))
for _, cm := range costumeMasters {
costumeById[cm.CostumeId] = cm
}
weaponById := make(map[int32]WeaponMasterRow, len(weapons))
for _, w := range weapons {
weaponById[w.WeaponId] = w
}
skillSlots := make(map[int32][]int32)
for _, row := range weaponSkillGroups {
skillSlots[row.WeaponSkillGroupId] = append(skillSlots[row.WeaponSkillGroupId], row.SlotNumber)
}
abilitySlots := make(map[int32][]int32)
for _, row := range weaponAbilityGroups {
abilitySlots[row.WeaponAbilityGroupId] = append(abilitySlots[row.WeaponAbilityGroupId], row.SlotNumber)
}
sceneById := make(map[int32]QuestSceneRow, len(scenes))
sceneIdsByQuestId := make(map[int32][]int32)
for _, scene := range scenes {
sceneById[scene.QuestSceneId] = scene
sceneIdsByQuestId[scene.QuestId] = append(sceneIdsByQuestId[scene.QuestId], scene.QuestSceneId)
}
missionById := make(map[int32]QuestMissionRow, len(missions))
for _, mission := range missions {
missionById[mission.QuestMissionId] = mission
}
questById := make(map[int32]QuestRow, len(quests))
for _, quest := range quests {
questById[quest.QuestId] = quest
}
missionIdsByGroupId := make(map[int32][]int32, len(missionGroups))
for _, mg := range missionGroups {
missionIdsByGroupId[mg.QuestMissionGroupId] = append(
missionIdsByGroupId[mg.QuestMissionGroupId], mg.QuestMissionId)
}
missionIdsByQuestId := make(map[int32][]int32)
for questId, quest := range questById {
missionIds := missionIdsByGroupId[quest.QuestMissionGroupId]
if len(missionIds) == 0 {
continue
}
missionIdsByQuestId[questId] = append([]int32(nil), missionIds...)
}
chapterBySequenceId := make(map[int32]MainQuestChapterRow, len(chapters))
for _, chapter := range chapters {
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
}
routeIdByQuestId := make(map[int32]int32)
for _, sequence := range sequences {
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
}
}
sortedChapters := make([]MainQuestChapterRow, len(chapters))
copy(sortedChapters, chapters)
sort.Slice(sortedChapters, func(i, j int) bool {
return sortedChapters[i].SortOrder < sortedChapters[j].SortOrder
})
sequencesByGroupId := make(map[int32][]MainQuestSequenceRow)
for _, seq := range sequences {
sequencesByGroupId[seq.MainQuestSequenceId] = append(sequencesByGroupId[seq.MainQuestSequenceId], seq)
}
var orderedQuestIds []int32
for _, chapter := range sortedChapters {
for _, seq := range sequencesByGroupId[chapter.MainQuestSequenceGroupId] {
orderedQuestIds = append(orderedQuestIds, seq.QuestId)
}
}
chapterLastSceneByQuestId := make(map[int32]int32)
for _, chapter := range sortedChapters {
seqs := sequencesByGroupId[chapter.MainQuestSequenceGroupId]
var chapterLastScene int32
for i := len(seqs) - 1; i >= 0; i-- {
if sids := sceneIdsByQuestId[seqs[i].QuestId]; len(sids) > 0 {
chapterLastScene = sids[len(sids)-1]
break
}
}
if chapterLastScene != 0 {
for _, seq := range seqs {
chapterLastSceneByQuestId[seq.QuestId] = chapterLastScene
}
}
}
firstClearRewardsByGroupId := make(map[int32][]QuestFirstClearRewardGroupRow, len(firstClearRewards))
for _, reward := range firstClearRewards {
firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId] = append(
firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId], reward)
}
replayFlowRewardsByGroupId := make(map[int32][]QuestReplayFlowRewardGroupRow, len(replayFlowRewards))
for _, reward := range replayFlowRewards {
replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId] = append(
replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId], reward)
}
firstClearRewardSwitchesByQuestId := make(map[int32][]QuestFirstClearRewardSwitchRow, len(firstClearSwitches))
for _, switchRow := range firstClearSwitches {
firstClearRewardSwitchesByQuestId[switchRow.QuestId] = append(
firstClearRewardSwitchesByQuestId[switchRow.QuestId], switchRow)
}
missionRewardsByMissionId := make(map[int32][]QuestMissionRewardRow, len(missionRewards))
for _, reward := range missionRewards {
missionRewardsByMissionId[reward.QuestMissionRewardId] = append(
missionRewardsByMissionId[reward.QuestMissionRewardId], reward)
}
weaponIdsByReleaseConditionGroupId := make(map[int32][]int32)
for _, w := range weaponById {
if w.WeaponStoryReleaseConditionGroupId != 0 {
weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId] = append(
weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId], w.WeaponId)
}
}
releaseConditionsByGroupId := make(map[int32][]WeaponStoryReleaseConditionRow)
for _, c := range releaseConditions {
releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append(
releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c)
}
sceneGrantsBySceneId := make(map[int32][]QuestSceneGrantRow)
for _, sg := range sceneGrants {
sceneGrantsBySceneId[sg.QuestSceneId] = append(sceneGrantsBySceneId[sg.QuestSceneId], sg)
}
battleDropRewardById := make(map[int32]BattleDropRewardRow, len(battleDropRewards))
for _, bdr := range battleDropRewards {
battleDropRewardById[bdr.BattleDropRewardId] = bdr
}
pickupRewardIdsByGroupId := make(map[int32][]int32)
for _, pg := range pickupRewardGroups {
pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId] = append(
pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId], pg.BattleDropRewardId)
}
battleGroupBySceneId := make(map[int32]int32, len(sceneBattles))
for _, sb := range sceneBattles {
battleGroupBySceneId[sb.QuestSceneId] = sb.BattleGroupId
}
battleIdsByGroupId := make(map[int32][]int32)
for _, bg := range battleGroups {
battleIdsByGroupId[bg.BattleGroupId] = append(battleIdsByGroupId[bg.BattleGroupId], bg.BattleId)
}
type npcDeckKey struct {
BattleNpcId int32
DeckType model.DeckType
BattleNpcDeckNumber int32
}
npcDeckByKey := make(map[npcDeckKey]BattleNpcDeckRow, len(npcDecks))
for _, d := range npcDecks {
npcDeckByKey[npcDeckKey{d.BattleNpcId, d.DeckType, d.BattleNpcDeckNumber}] = d
}
battleByIdMap := make(map[int32]BattleRow, len(battles))
for _, b := range battles {
battleByIdMap[b.BattleId] = b
}
type dropCatKey struct {
BattleNpcId int32
Uuid string
}
dropCategoryByKey := make(map[dropCatKey]int32, len(npcDropCategories))
for _, dc := range npcDropCategories {
dropCategoryByKey[dropCatKey{dc.BattleNpcId, dc.BattleNpcDeckCharacterUuid}] = dc.BattleDropCategoryId
}
battleDropsByQuestId := make(map[int32][]BattleDropInfo)
for questId := range questById {
sids := sceneIdsByQuestId[questId]
seen := make(map[BattleDropInfo]bool)
var drops []BattleDropInfo
for _, sceneId := range sids {
groupId, ok := battleGroupBySceneId[sceneId]
if !ok {
continue
}
for _, battleId := range battleIdsByGroupId[groupId] {
b, ok := battleByIdMap[battleId]
if !ok {
continue
}
dk := npcDeckKey{b.BattleNpcId, b.DeckType, b.BattleNpcDeckNumber}
deck, ok := npcDeckByKey[dk]
if !ok {
continue
}
for _, uuid := range []string{deck.BattleNpcDeckCharacterUuid01, deck.BattleNpcDeckCharacterUuid02, deck.BattleNpcDeckCharacterUuid03} {
if uuid == "" {
continue
}
catId, ok := dropCategoryByKey[dropCatKey{b.BattleNpcId, uuid}]
if !ok {
continue
}
info := BattleDropInfo{QuestSceneId: sceneId, BattleDropCategoryId: catId}
if !seen[info] {
seen[info] = true
drops = append(drops, info)
}
}
}
}
if len(drops) > 0 {
battleDropsByQuestId[questId] = drops
}
}
rentalBattleGroups := make(map[int32]bool, len(rentalDecks))
for _, rd := range rentalDecks {
rentalBattleGroups[rd.BattleGroupId] = true
}
rentalQuestIds := make(map[int32]bool)
for questId := range questById {
for _, sceneId := range sceneIdsByQuestId[questId] {
if groupId, ok := battleGroupBySceneId[sceneId]; ok && rentalBattleGroups[groupId] {
rentalQuestIds[questId] = true
break
}
}
}
return &QuestCatalog{
SceneById: sceneById,
MissionById: missionById,
QuestById: questById,
MissionIdsByQuestId: missionIdsByQuestId,
RouteIdByQuestId: routeIdByQuestId,
SceneIdsByQuestId: sceneIdsByQuestId,
OrderedQuestIds: orderedQuestIds,
FirstClearRewardsByGroupId: firstClearRewardsByGroupId,
FirstClearRewardSwitchesByQuestId: firstClearRewardSwitchesByQuestId,
MissionRewardsByMissionId: missionRewardsByMissionId,
WeaponIdsByReleaseConditionGroupId: weaponIdsByReleaseConditionGroupId,
ReleaseConditionsByGroupId: releaseConditionsByGroupId,
SceneGrantsBySceneId: sceneGrantsBySceneId,
BattleDropRewardById: battleDropRewardById,
PickupRewardIdsByGroupId: pickupRewardIdsByGroupId,
BattleDropsByQuestId: battleDropsByQuestId,
ReplayFlowRewardsByGroupId: replayFlowRewardsByGroupId,
RentalQuestIds: rentalQuestIds,
TutorialUnlockConditions: tutorialUnlockConds,
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
SeasonIdByRouteId: seasonIdByRouteId,
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
CostumeExpByRarity: costumeExpByRarity,
CostumeMaxLevelByRarity: costumeMaxLevelByRarity,
MaxStaminaByLevel: maxStaminaByLevel,
CostumeById: costumeById,
WeaponById: weaponById,
WeaponSkillSlots: skillSlots,
WeaponAbilitySlots: abilitySlots,
PartsCatalog: partsCatalog,
}, nil
}
+177
View File
@@ -0,0 +1,177 @@
package masterdata
import (
"fmt"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type ShopItemRow struct {
ShopItemId int32 `json:"ShopItemId"`
PriceType int32 `json:"PriceType"`
PriceId int32 `json:"PriceId"`
Price int32 `json:"Price"`
ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"`
}
type ShopContentRow struct {
ShopItemId int32 `json:"ShopItemId"`
PossessionType int32 `json:"PossessionType"`
PossessionId int32 `json:"PossessionId"`
Count int32 `json:"Count"`
}
type ShopContentEffectRow struct {
ShopItemId int32 `json:"ShopItemId"`
EffectTargetType int32 `json:"EffectTargetType"`
EffectValueType int32 `json:"EffectValueType"`
EffectValue int32 `json:"EffectValue"`
}
type shopItemLimitedStockRow struct {
ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"`
MaxCount int32 `json:"MaxCount"`
}
type shopRow struct {
ShopId int32 `json:"ShopId"`
ShopGroupType int32 `json:"ShopGroupType"`
ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"`
}
type shopItemCellGroupRow struct {
ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"`
ShopItemCellId int32 `json:"ShopItemCellId"`
SortOrder int32 `json:"SortOrder"`
}
type shopItemCellRow struct {
ShopItemCellId int32 `json:"ShopItemCellId"`
ShopItemId int32 `json:"ShopItemId"`
}
type ExchangeShopCell struct {
SortOrder int32
ShopItemId int32
}
type ShopCatalog struct {
Items map[int32]ShopItemRow
Contents map[int32][]ShopContentRow
Effects map[int32][]ShopContentEffectRow
MaxStaminaMillis map[int32]int32 // level -> max stamina in millis
LimitedStock map[int32]int32 // stock id -> max count
ItemShopPool []int32 // shop item IDs for the replaceable item shop, sorted by cell sort order
ExchangeShopCells map[int32][]ExchangeShopCell // shopId -> sorted cells for exchange shops
}
type userLevelEntry struct {
UserLevel int32 `json:"UserLevel"`
MaxStamina int32 `json:"MaxStamina"`
}
func LoadShopCatalog() (*ShopCatalog, error) {
items, err := utils.ReadJSON[ShopItemRow]("EntityMShopItemTable.json")
if err != nil {
return nil, fmt.Errorf("load shop item table: %w", err)
}
contents, err := utils.ReadJSON[ShopContentRow]("EntityMShopItemContentPossessionTable.json")
if err != nil {
return nil, fmt.Errorf("load shop content possession table: %w", err)
}
effects, err := utils.ReadJSON[ShopContentEffectRow]("EntityMShopItemContentEffectTable.json")
if err != nil {
return nil, fmt.Errorf("load shop content effect table: %w", err)
}
userLevels, err := utils.ReadJSON[userLevelEntry]("EntityMUserLevelTable.json")
if err != nil {
return nil, fmt.Errorf("load user level table: %w", err)
}
stockRows, err := utils.ReadJSON[shopItemLimitedStockRow]("EntityMShopItemLimitedStockTable.json")
if err != nil {
return nil, fmt.Errorf("load shop item limited stock table: %w", err)
}
catalog := &ShopCatalog{
Items: make(map[int32]ShopItemRow, len(items)),
Contents: make(map[int32][]ShopContentRow, len(contents)),
Effects: make(map[int32][]ShopContentEffectRow, len(effects)),
MaxStaminaMillis: make(map[int32]int32, len(userLevels)),
LimitedStock: make(map[int32]int32, len(stockRows)),
}
for _, row := range items {
catalog.Items[row.ShopItemId] = row
}
for _, row := range contents {
catalog.Contents[row.ShopItemId] = append(catalog.Contents[row.ShopItemId], row)
}
for _, row := range effects {
catalog.Effects[row.ShopItemId] = append(catalog.Effects[row.ShopItemId], row)
}
for _, ul := range userLevels {
catalog.MaxStaminaMillis[ul.UserLevel] = ul.MaxStamina * 1000
}
for _, row := range stockRows {
catalog.LimitedStock[row.ShopItemLimitedStockId] = row.MaxCount
}
shops, err := utils.ReadJSON[shopRow]("EntityMShopTable.json")
if err != nil {
return nil, fmt.Errorf("load shop table: %w", err)
}
cellGroups, err := utils.ReadJSON[shopItemCellGroupRow]("EntityMShopItemCellGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load shop item cell group table: %w", err)
}
cells, err := utils.ReadJSON[shopItemCellRow]("EntityMShopItemCellTable.json")
if err != nil {
return nil, fmt.Errorf("load shop item cell table: %w", err)
}
cellIdToItemId := make(map[int32]int32, len(cells))
for _, c := range cells {
cellIdToItemId[c.ShopItemCellId] = c.ShopItemId
}
cellGroupByCGId := make(map[int32][]shopItemCellGroupRow, len(cellGroups))
for _, cg := range cellGroups {
cellGroupByCGId[cg.ShopItemCellGroupId] = append(cellGroupByCGId[cg.ShopItemCellGroupId], cg)
}
catalog.ExchangeShopCells = make(map[int32][]ExchangeShopCell)
for _, s := range shops {
entries := cellGroupByCGId[s.ShopItemCellGroupId]
if len(entries) == 0 {
continue
}
switch s.ShopGroupType {
case model.ShopGroupTypeItemShop:
var poolCells []ExchangeShopCell
for _, cg := range entries {
if itemId, ok := cellIdToItemId[cg.ShopItemCellId]; ok {
poolCells = append(poolCells, ExchangeShopCell{cg.SortOrder, itemId})
}
}
sort.Slice(poolCells, func(i, j int) bool { return poolCells[i].SortOrder < poolCells[j].SortOrder })
catalog.ItemShopPool = make([]int32, len(poolCells))
for i, pc := range poolCells {
catalog.ItemShopPool[i] = pc.ShopItemId
}
case model.ShopGroupTypeExchangeShop:
var sc []ExchangeShopCell
for _, cg := range entries {
if itemId, ok := cellIdToItemId[cg.ShopItemCellId]; ok {
sc = append(sc, ExchangeShopCell{cg.SortOrder, itemId})
}
}
sort.Slice(sc, func(i, j int) bool { return sc[i].SortOrder < sc[j].SortOrder })
catalog.ExchangeShopCells[s.ShopId] = sc
}
}
return catalog, nil
}
+33
View File
@@ -0,0 +1,33 @@
package masterdata
import (
"log"
"lunar-tear/server/internal/utils"
)
type sideStorySceneRow struct {
SideStoryQuestId int32 `json:"SideStoryQuestId"`
SideStoryQuestSceneId int32 `json:"SideStoryQuestSceneId"`
SortOrder int32 `json:"SortOrder"`
}
type SideStoryCatalog struct {
FirstSceneByQuestId map[int32]int32
}
func LoadSideStoryCatalog() *SideStoryCatalog {
scenes, err := utils.ReadJSON[sideStorySceneRow]("EntityMSideStoryQuestSceneTable.json")
if err != nil {
log.Fatalf("load side story quest scene table: %v", err)
}
firstScene := make(map[int32]int32, len(scenes)/7)
for _, s := range scenes {
if s.SortOrder == 1 {
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
}
}
log.Printf("side story catalog loaded: %d quests", len(firstScene))
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
}
+419
View File
@@ -0,0 +1,419 @@
package masterdata
import (
"fmt"
"log"
"sort"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/utils"
)
type WeaponMasterRow struct {
WeaponId int32 `json:"WeaponId"`
RarityType int32 `json:"RarityType"`
WeaponType int32 `json:"WeaponType"`
WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"`
WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"`
WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"`
WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"`
WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"`
}
type WeaponStoryReleaseConditionRow struct {
WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"`
StoryIndex int32 `json:"StoryIndex"`
WeaponStoryReleaseConditionType model.WeaponStoryReleaseConditionType `json:"WeaponStoryReleaseConditionType"`
ConditionValue int32 `json:"ConditionValue"`
WeaponStoryReleaseConditionOperationGroupId int32 `json:"WeaponStoryReleaseConditionOperationGroupId"`
}
type WeaponSkillGroupRow struct {
WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"`
SlotNumber int32 `json:"SlotNumber"`
SkillId int32 `json:"SkillId"`
WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"`
}
type WeaponAbilityGroupRow struct {
WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"`
SlotNumber int32 `json:"SlotNumber"`
AbilityId int32 `json:"AbilityId"`
WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"`
}
type weaponSpecificEnhanceRow struct {
WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"`
BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"`
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"`
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"`
LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"`
LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"`
MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"`
SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"`
MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"`
AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"`
}
type weaponConsumeExchangeRow struct {
WeaponId int32 `json:"WeaponId"`
ConsumableItemId int32 `json:"ConsumableItemId"`
Count int32 `json:"Count"`
}
type WeaponEvolutionGroupRow struct {
WeaponEvolutionGroupId int32 `json:"WeaponEvolutionGroupId"`
EvolutionOrder int32 `json:"EvolutionOrder"`
WeaponId int32 `json:"WeaponId"`
}
type WeaponEvolutionMaterialRow struct {
WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type WeaponSkillEnhanceMaterialRow struct {
WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"`
SkillLevel int32 `json:"SkillLevel"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type WeaponAbilityEnhanceMaterialRow struct {
WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"`
AbilityLevel int32 `json:"AbilityLevel"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type weaponRarityEnhanceRow struct {
RarityType int32 `json:"RarityType"`
BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"`
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"`
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"`
LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"`
LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"`
MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"`
SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"`
MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"`
AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"`
}
type WeaponCatalog struct {
Weapons map[int32]WeaponMasterRow
Materials map[int32]MaterialRow
ExpByEnhanceId map[int32][]int32
GoldCostByEnhanceId map[int32]NumericalFunc
MaxLevelByEnhanceId map[int32]NumericalFunc
SellPriceByEnhanceId map[int32]NumericalFunc
MedalsByWeaponId map[int32]map[int32]int32 // WeaponId -> ConsumableItemId -> Count
EvolutionNextWeaponId map[int32]int32
EvolutionOrder map[int32]int32 // WeaponId -> 0-based position in evolution chain
EvolutionMaterials map[int32][]WeaponEvolutionMaterialRow // WeaponEvolutionMaterialGroupId -> materials
EvolutionCostByEnhanceId map[int32]NumericalFunc
AbilitySlots map[int32][]int32 // WeaponAbilityGroupId -> slot numbers
SkillGroupsByGroupId map[int32][]WeaponSkillGroupRow
SkillEnhanceMats map[[2]int32][]WeaponSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
SkillMaxLevelByEnhanceId map[int32]NumericalFunc
SkillCostByEnhanceId map[int32]NumericalFunc
AbilityGroupsByGroupId map[int32][]WeaponAbilityGroupRow
AbilityEnhanceMats map[[2]int32][]WeaponAbilityEnhanceMaterialRow // key: [enhancementMaterialId, abilityLevel]
AbilityMaxLevelByEnhanceId map[int32]NumericalFunc
AbilityCostByEnhanceId map[int32]NumericalFunc
EnhanceCostByWeaponByEnhanceId map[int32]NumericalFunc
LimitBreakCostByWeaponByEnhanceId map[int32]NumericalFunc
LimitBreakCostByMaterialByEnhanceId map[int32]NumericalFunc
BaseExpByEnhanceId map[int32]int32
ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow
}
func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) {
weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon table: %w", err)
}
enhanceRows, err := utils.ReadJSON[weaponSpecificEnhanceRow]("EntityMWeaponSpecificEnhanceTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon specific enhance table: %w", err)
}
rarityEnhanceRows, err := utils.ReadJSON[weaponRarityEnhanceRow]("EntityMWeaponRarityTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon rarity table: %w", err)
}
paramMapRows, err := LoadParameterMap()
if err != nil {
return nil, err
}
funcResolver, err := LoadFunctionResolver()
if err != nil {
return nil, fmt.Errorf("load function resolver: %w", err)
}
exchangeRows, err := utils.ReadJSON[weaponConsumeExchangeRow]("EntityMWeaponConsumeExchangeConsumableItemGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon consume exchange table: %w", err)
}
evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon evolution group table: %w", err)
}
evoMatRows, err := utils.ReadJSON[WeaponEvolutionMaterialRow]("EntityMWeaponEvolutionMaterialGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon evolution material group table: %w", err)
}
abilityGroupRows, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon ability group table: %w", err)
}
skillGroupRows, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon skill group table: %w", err)
}
skillMatRows, err := utils.ReadJSON[WeaponSkillEnhanceMaterialRow]("EntityMWeaponSkillEnhancementMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon skill enhancement material table: %w", err)
}
abilityMatRows, err := utils.ReadJSON[WeaponAbilityEnhanceMaterialRow]("EntityMWeaponAbilityEnhancementMaterialTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon ability enhancement material table: %w", err)
}
releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load weapon story release condition table: %w", err)
}
catalog := &WeaponCatalog{
Weapons: make(map[int32]WeaponMasterRow, len(weapons)),
Materials: matCatalog.ByType[model.MaterialTypeWeaponEnhancement],
ExpByEnhanceId: make(map[int32][]int32, len(enhanceRows)),
GoldCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
MaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
SellPriceByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
MedalsByWeaponId: make(map[int32]map[int32]int32),
EvolutionNextWeaponId: make(map[int32]int32),
EvolutionOrder: make(map[int32]int32),
EvolutionMaterials: make(map[int32][]WeaponEvolutionMaterialRow),
EvolutionCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
AbilitySlots: make(map[int32][]int32),
SkillGroupsByGroupId: make(map[int32][]WeaponSkillGroupRow),
SkillEnhanceMats: make(map[[2]int32][]WeaponSkillEnhanceMaterialRow),
SkillMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
SkillCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
AbilityGroupsByGroupId: make(map[int32][]WeaponAbilityGroupRow),
AbilityEnhanceMats: make(map[[2]int32][]WeaponAbilityEnhanceMaterialRow),
AbilityMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
AbilityCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
EnhanceCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
LimitBreakCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
LimitBreakCostByMaterialByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)),
ReleaseConditionsByGroupId: make(map[int32][]WeaponStoryReleaseConditionRow),
}
for _, w := range weapons {
catalog.Weapons[w.WeaponId] = w
}
for _, r := range enhanceRows {
if _, ok := catalog.ExpByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
catalog.ExpByEnhanceId[r.WeaponSpecificEnhanceId] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
}
if _, ok := catalog.GoldCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
catalog.GoldCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.MaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
catalog.MaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.SellPriceByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.SellPriceNumericalFunctionId); found {
catalog.SellPriceByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.EvolutionCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.EvolutionCostNumericalFunctionId); found {
catalog.EvolutionCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.SkillMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.MaxSkillLevelNumericalFunctionId); found {
catalog.SkillMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.SkillCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.SkillEnhancementCostNumericalFunctionId); found {
catalog.SkillCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.AbilityMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.MaxAbilityLevelNumericalFunctionId); found {
catalog.AbilityMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.AbilityCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.AbilityEnhancementCostNumericalFunctionId); found {
catalog.AbilityCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.EnhanceCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.EnhancementCostByWeaponNumericalFunctionId); found {
catalog.EnhanceCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.LimitBreakCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.LimitBreakCostByWeaponNumericalFunctionId); found {
catalog.LimitBreakCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.LimitBreakCostByMaterialByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
if f, found := funcResolver.Resolve(r.LimitBreakCostByMaterialNumericalFunctionId); found {
catalog.LimitBreakCostByMaterialByEnhanceId[r.WeaponSpecificEnhanceId] = f
}
}
if _, ok := catalog.BaseExpByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
catalog.BaseExpByEnhanceId[r.WeaponSpecificEnhanceId] = r.BaseEnhancementObtainedExp
}
}
for _, ex := range exchangeRows {
if catalog.MedalsByWeaponId[ex.WeaponId] == nil {
catalog.MedalsByWeaponId[ex.WeaponId] = make(map[int32]int32)
}
catalog.MedalsByWeaponId[ex.WeaponId][ex.ConsumableItemId] = ex.Count
}
grouped := make(map[int32][]WeaponEvolutionGroupRow)
for _, row := range evoGroupRows {
grouped[row.WeaponEvolutionGroupId] = append(grouped[row.WeaponEvolutionGroupId], row)
}
for _, rows := range grouped {
sort.Slice(rows, func(i, j int) bool {
return rows[i].EvolutionOrder < rows[j].EvolutionOrder
})
for i, row := range rows {
catalog.EvolutionOrder[row.WeaponId] = int32(i)
if i < len(rows)-1 {
catalog.EvolutionNextWeaponId[row.WeaponId] = rows[i+1].WeaponId
}
}
}
for _, row := range evoMatRows {
catalog.EvolutionMaterials[row.WeaponEvolutionMaterialGroupId] = append(
catalog.EvolutionMaterials[row.WeaponEvolutionMaterialGroupId], row)
}
for _, row := range abilityGroupRows {
catalog.AbilitySlots[row.WeaponAbilityGroupId] = append(
catalog.AbilitySlots[row.WeaponAbilityGroupId], row.SlotNumber)
}
for _, row := range skillGroupRows {
catalog.SkillGroupsByGroupId[row.WeaponSkillGroupId] = append(
catalog.SkillGroupsByGroupId[row.WeaponSkillGroupId], row)
}
for _, row := range skillMatRows {
key := [2]int32{row.WeaponSkillEnhancementMaterialId, row.SkillLevel}
catalog.SkillEnhanceMats[key] = append(catalog.SkillEnhanceMats[key], row)
}
for _, row := range abilityGroupRows {
catalog.AbilityGroupsByGroupId[row.WeaponAbilityGroupId] = append(
catalog.AbilityGroupsByGroupId[row.WeaponAbilityGroupId], row)
}
for _, row := range abilityMatRows {
key := [2]int32{row.WeaponAbilityEnhancementMaterialId, row.AbilityLevel}
catalog.AbilityEnhanceMats[key] = append(catalog.AbilityEnhanceMats[key], row)
}
for _, c := range releaseConditions {
catalog.ReleaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append(
catalog.ReleaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c)
}
// Rarity-based enhancement fallback: for weapons with WeaponSpecificEnhanceId == 0,
// use EntityMWeaponRarityTable curves via synthetic enhance IDs (-RarityType).
rarityByType := make(map[int32]weaponRarityEnhanceRow, len(rarityEnhanceRows))
for _, r := range rarityEnhanceRows {
rarityByType[r.RarityType] = r
}
registeredRarity := make(map[int32]bool, len(rarityEnhanceRows))
fallbackCount := 0
for wid, w := range catalog.Weapons {
if w.WeaponSpecificEnhanceId != 0 {
continue
}
syntheticId := -w.RarityType
if !registeredRarity[w.RarityType] {
r, ok := rarityByType[w.RarityType]
if !ok {
continue
}
catalog.ExpByEnhanceId[syntheticId] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
catalog.GoldCostByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
catalog.MaxLevelByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.SellPriceNumericalFunctionId); found {
catalog.SellPriceByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.EvolutionCostNumericalFunctionId); found {
catalog.EvolutionCostByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.MaxSkillLevelNumericalFunctionId); found {
catalog.SkillMaxLevelByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.SkillEnhancementCostNumericalFunctionId); found {
catalog.SkillCostByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.MaxAbilityLevelNumericalFunctionId); found {
catalog.AbilityMaxLevelByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.AbilityEnhancementCostNumericalFunctionId); found {
catalog.AbilityCostByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.EnhancementCostByWeaponNumericalFunctionId); found {
catalog.EnhanceCostByWeaponByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.LimitBreakCostByWeaponNumericalFunctionId); found {
catalog.LimitBreakCostByWeaponByEnhanceId[syntheticId] = f
}
if f, found := funcResolver.Resolve(r.LimitBreakCostByMaterialNumericalFunctionId); found {
catalog.LimitBreakCostByMaterialByEnhanceId[syntheticId] = f
}
catalog.BaseExpByEnhanceId[syntheticId] = r.BaseEnhancementObtainedExp
registeredRarity[w.RarityType] = true
}
w.WeaponSpecificEnhanceId = syntheticId
catalog.Weapons[wid] = w
fallbackCount++
}
log.Printf("[WeaponCatalog] rarity fallback: assigned synthetic enhance IDs to %d weapons", fallbackCount)
return catalog, nil
}