diff --git a/pkg/core/info/character.go b/pkg/core/info/character.go index 85ea2aa6a0..d0e30e1dcd 100644 --- a/pkg/core/info/character.go +++ b/pkg/core/info/character.go @@ -1,19 +1,87 @@ package info import ( + "errors" + "fmt" + "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/keys" ) type CharacterProfile struct { - Base CharacterBase `json:"base"` - Weapon WeaponProfile `json:"weapon"` - Talents TalentProfile `json:"talents"` - Stats []float64 `json:"stats"` - StatsByLabel map[string][]float64 `json:"stats_by_label"` - Sets Sets `json:"sets"` - SetParams map[keys.Set]map[string]int `json:"-"` - Params map[string]int `json:"-"` + Base CharacterBase `json:"base"` + Weapon WeaponProfile `json:"weapon"` + Talents TalentProfile `json:"talents"` + Stats []float64 `json:"stats"` + StatsByLabel map[string][]float64 `json:"stats_by_label"` + RandomSubstats *RandomSubstats `json:"random_substats"` + Sets Sets `json:"sets"` + SetParams map[keys.Set]map[string]int `json:"-"` + Params map[string]int `json:"-"` +} + +type RandomSubstats struct { + Rarity int `json:"rarity"` + Sand attributes.Stat + Goblet attributes.Stat + Circlet attributes.Stat +} + +func (r RandomSubstats) Validate() error { + //TODO: support more than just 5 stars + if r.Rarity != 5 { + return fmt.Errorf("unsupported rarity: %v", r.Rarity) + } + if r.Sand == attributes.NoStat { + return errors.New("sand main stat not specified") + } + if r.Goblet == attributes.NoStat { + return errors.New("goblet main stat not specified") + } + if r.Circlet == attributes.NoStat { + return errors.New("circlet main stat not specified") + } + // main stat have to be valid + switch r.Sand { + case attributes.HPP: + case attributes.ATKP: + case attributes.DEFP: + case attributes.EM: + case attributes.ER: + default: + return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String()) + } + + switch r.Goblet { + case attributes.HPP: + case attributes.ATKP: + case attributes.DEFP: + case attributes.EM: + case attributes.PyroP: + case attributes.HydroP: + case attributes.CryoP: + case attributes.ElectroP: + case attributes.AnemoP: + case attributes.GeoP: + case attributes.DendroP: + case attributes.PhyP: + default: + return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String()) + } + + switch r.Circlet { + case attributes.HPP: + case attributes.ATKP: + case attributes.DEFP: + case attributes.EM: + case attributes.CR: + case attributes.CD: + case attributes.Heal: + default: + return fmt.Errorf("%v is not a valid main stat for sand", r.Sand.String()) + } + + return nil } func (c *CharacterProfile) Clone() CharacterProfile { diff --git a/pkg/gcs/ast/parseCharacter.go b/pkg/gcs/ast/parseCharacter.go index 4dd5ff0a54..9fba690af9 100644 --- a/pkg/gcs/ast/parseCharacter.go +++ b/pkg/gcs/ast/parseCharacter.go @@ -283,6 +283,11 @@ func parseCharAddStats(p *Parser) (parseFn, error) { } c.StatsByLabel[key] = m return parseRows, nil + case itemIdentifier: + if n.Val == "random" { + return parseCharAddRandomStats(p) + } + fallthrough default: return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats: %v", n.line, n) } @@ -290,6 +295,64 @@ func parseCharAddStats(p *Parser) (parseFn, error) { return nil, errors.New("unexpected end of line while parsing character add stats") } +func parseCharAddRandomStats(p *Parser) (parseFn, error) { + // xiangling add stats random rarity=5 sand=hp% goblet=pyro% circlet=cr + + // note that plume/flower not specified and will be ignored + rs := &info.RandomSubstats{ + Rarity: 5, // default to 5 star + } + + for n := p.next(); n.Typ != itemEOF; n = p.next() { + switch n.Typ { + case itemTerminateLine: + // check to make sure all values are valid + err := rs.Validate() + if err != nil { + return nil, fmt.Errorf("ln%v: %w", n.line, err) + } + c := p.chars[p.currentCharKey] + c.RandomSubstats = rs + return parseRows, nil + case itemIdentifier: + switch n.Val { + case "rarity": + x, err := p.acceptSeqReturnLast(itemAssign, itemNumber) + if err != nil { + return nil, err + } + rs.Rarity, err = itemNumberToInt(x) + if err != nil { + return nil, err + } + case "sand": + x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey) + if err != nil { + return nil, err + } + rs.Sand = statKeys[x.Val] + case "goblet": + x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey) + if err != nil { + return nil, err + } + rs.Goblet = statKeys[x.Val] + case "circlet": + x, err := p.acceptSeqReturnLast(itemAssign, itemStatKey) + if err != nil { + return nil, err + } + rs.Circlet = statKeys[x.Val] + default: + return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats random: %v", n.line, n) + } + default: + return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats random: %v", n.line, n) + } + } + return nil, errors.New("unexpected end of line while parsing character add stats (with random subs)") +} + func (p *Parser) acceptLevelReturnBaseMax() (int, int, error) { base := 0 max := 0 diff --git a/pkg/simulation/randstats.go b/pkg/simulation/randstats.go new file mode 100644 index 0000000000..8f0375f977 --- /dev/null +++ b/pkg/simulation/randstats.go @@ -0,0 +1,123 @@ +package simulation + +import ( + "errors" + "log" + "math/rand" + + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/info" +) + +var subDist [attributes.DelimBaseStat]float64 +var subUpgrade [attributes.DelimBaseStat][4]float64 + +// mainstat at lvl 20 +var mainStat = map[attributes.Stat]float64{ + attributes.HP: 4780, + attributes.ATK: 311, + attributes.HPP: 0.466, + attributes.ATKP: 0.466, + attributes.DEFP: 0.583, + attributes.PyroP: 0.466, + attributes.HydroP: 0.466, + attributes.CryoP: 0.466, + attributes.ElectroP: 0.466, + attributes.AnemoP: 0.466, + attributes.GeoP: 0.466, + attributes.DendroP: 0.466, + attributes.PhyP: 0.466, + attributes.EM: 186.5, + attributes.ER: 0.518, + attributes.CD: 0.622, + attributes.CR: 0.311, + attributes.Heal: 0.359, +} + +func init() { + subDist[attributes.HP] = 6 + subDist[attributes.ATK] = 6 + subDist[attributes.DEF] = 6 + subDist[attributes.HPP] = 4 + subDist[attributes.ATKP] = 4 + subDist[attributes.DEFP] = 4 + subDist[attributes.ER] = 4 + subDist[attributes.EM] = 4 + subDist[attributes.CR] = 3 + subDist[attributes.CD] = 3 + + subUpgrade[attributes.HP] = [4]float64{209, 239, 269, 299} + subUpgrade[attributes.DEF] = [4]float64{16, 19, 21, 23} + subUpgrade[attributes.ATK] = [4]float64{14, 16, 18, 19} + subUpgrade[attributes.HPP] = [4]float64{0.041, 0.047, 0.053, 0.058} + subUpgrade[attributes.DEFP] = [4]float64{0.051, 0.058, 0.066, 0.073} + subUpgrade[attributes.ATKP] = [4]float64{0.041, 0.047, 0.053, 0.058} + subUpgrade[attributes.EM] = [4]float64{16, 19, 21, 23} + subUpgrade[attributes.ER] = [4]float64{0.045, 0.052, 0.058, 0.065} + subUpgrade[attributes.CR] = [4]float64{0.027, 0.031, 0.035, 0.039} + subUpgrade[attributes.CD] = [4]float64{0.054, 0.062, 0.07, 0.078} +} + +func generateRandSubs(r *info.RandomSubstats, rng *rand.Rand) ([]float64, error) { + if r.Rarity != 5 { + return nil, errors.New("sorry only 5 star artifacts supported currently") + } + stats := make([]float64, attributes.EndStatType) + // main stats first + stats[attributes.ATK] = mainStat[attributes.ATK] + stats[attributes.HP] = mainStat[attributes.HP] + stats[r.Sand] += mainStat[r.Sand] + stats[r.Goblet] += mainStat[r.Goblet] + stats[r.Circlet] += mainStat[r.Circlet] + + mains := [5]attributes.Stat{attributes.ATK, attributes.HP, r.Sand, r.Goblet, r.Circlet} + + for _, m := range mains { + // weights + var weight [attributes.DelimBaseStat]float64 + var picked [4]attributes.Stat + copy(weight[:], subDist[:]) + weight[m] = 0 + + //TODO: option to use boss + upgrades := 4 + if rng.Float64() <= 0.2 { + upgrades = 5 + } + for i := 0; i < 4; i++ { + // pick stat from weight + s := randSub(weight, rng) + if s == attributes.NoStat { + log.Println("weights no good?") + log.Println(subDist) + log.Println(weight) + return nil, errors.New("unexpected error picking random sub; none found") + } + weight[s] = 0 + stats[s] += subUpgrade[s][rng.Intn(4)] + picked[i] = s + } + for i := 0; i < upgrades; i++ { + // pick one out of 4 stats + s := picked[rng.Intn(4)] + stats[s] += subUpgrade[s][rng.Intn(4)] + } + } + return stats, nil +} + +func randSub(weights [attributes.DelimBaseStat]float64, rng *rand.Rand) attributes.Stat { + var sumWeights float64 + for _, v := range weights { + sumWeights += v + } + pick := rng.Float64() * sumWeights + var cum float64 + for i, v := range weights { + cum += v + if pick <= cum { + return attributes.Stat(i) + } + } + return attributes.NoStat +} diff --git a/pkg/simulation/setup.go b/pkg/simulation/setup.go index bd896ff6bc..5da2e5d1a4 100644 --- a/pkg/simulation/setup.go +++ b/pkg/simulation/setup.go @@ -64,6 +64,15 @@ func SetupCharactersInCore(core *core.Core, chars []info.CharacterProfile, initi active := -1 for i := range chars { + // if using random stats, ignore all stats except main + if chars[i].RandomSubstats != nil { + stats, err := generateRandSubs(chars[i].RandomSubstats, core.Rand) + if err != nil { + return err + } + chars[i].Stats = stats + clear(chars[i].StatsByLabel) + } i, err := core.AddChar(chars[i]) if err != nil { return err