package common import ( "crypto/sha256" "encoding/hex" "fmt" "regexp" ) // Native PlayerLevelConfigRow, before run-specific modifiers are applied. type CurrencyWarsPlayerLevel struct { GeneralPropertyList []CurrencyWarsNativePropertyValue `json:"GeneralPropertyList"` Level uint32 `json:"PlayerLevel"` LevelUpExp uint32 `json:"LevelUpExp"` AvatarMaxNumber uint32 `json:"AvatarMaxNumber"` Rarity1Weight uint32 `json:"Rarity1Weight"` Rarity2Weight uint32 `json:"Rarity2Weight"` Rarity3Weight uint32 `json:"Rarity3Weight"` Rarity4Weight uint32 `json:"Rarity4Weight"` Rarity5Weight uint32 `json:"Rarity5Weight"` } // Keep native FixPoint decimal values as text rather than float64. type CurrencyWarsNativePropertyValue struct { PropertyType uint32 `json:"PropertyType"` Value string `json:"Value"` } var currencyWarsDecimal = regexp.MustCompile(`^[+-]?[0-9]+(?:\.[0-9]+)?$`) func (p CurrencyWarsPlayerLevel) RarityWeights() [5]uint32 { return [5]uint32{p.Rarity1Weight, p.Rarity2Weight, p.Rarity3Weight, p.Rarity4Weight, p.Rarity5Weight} } func (c *CurrencyWarsCatalog) validatePlayerLevels() error { c.playerLevelByID = make(map[uint32]CurrencyWarsPlayerLevel) if len(c.PlayerLevels) == 0 && c.Provenance.PlayerLevelsSHA256 == "" { return nil } digest, err := hex.DecodeString(c.Provenance.PlayerLevelsSHA256) if err != nil || len(digest) != sha256.Size { return fmt.Errorf("CW player-level capture provenance is missing") } maxLevel, err := c.IntegerRule("GridFight_DefaultPlayerLevelMax") if err != nil || maxLevel <= 0 || uint64(maxLevel) != uint64(len(c.PlayerLevels)) { return fmt.Errorf("CW player-level capture does not cover the default range") } for _, row := range c.PlayerLevels { if row.Level == 0 || uint64(row.Level) > uint64(maxLevel) || row.AvatarMaxNumber == 0 { return fmt.Errorf("invalid CW player-level row %d", row.Level) } if _, duplicate := c.playerLevelByID[row.Level]; duplicate { return fmt.Errorf("duplicate CW player level %d", row.Level) } if row.GeneralPropertyList == nil { return fmt.Errorf("CW level %d is missing property rows", row.Level) } for _, prop := range row.GeneralPropertyList { if prop.PropertyType == 0 || !currencyWarsDecimal.MatchString(prop.Value) { return fmt.Errorf("invalid CW property value at level %d", row.Level) } } var total uint64 for _, weight := range row.RarityWeights() { total += uint64(weight) } if total == 0 { return fmt.Errorf("CW shop rarity weights are empty at level %d", row.Level) } c.playerLevelByID[row.Level] = row } return nil } // Native GetLevelUpNeedExp uses a run modifier when present, otherwise this // table's LevelUpExp. Reading a row does not apply EXP or draw shop cards. func (c *CurrencyWarsCatalog) PlayerLevel(level uint32) (CurrencyWarsPlayerLevel, error) { row, exists := c.playerLevelByID[level] if !exists { return CurrencyWarsPlayerLevel{}, fmt.Errorf("CW player level %d has not been captured", level) } row.GeneralPropertyList = append([]CurrencyWarsNativePropertyValue(nil), row.GeneralPropertyList...) return row, nil }