diff --git a/internal/lightcone/harmony/poisedtobloom/data.go b/internal/lightcone/harmony/poisedtobloom/data.go new file mode 100644 index 00000000..3ef078a0 --- /dev/null +++ b/internal/lightcone/harmony/poisedtobloom/data.go @@ -0,0 +1,71 @@ +// Code generated by "weapstat"; DO NOT EDIT. + +package poisedtobloom + +import "github.com/simimpact/srsim/pkg/engine/equip/lightcone" + +var promotions = []lightcone.PromotionData{ + { + MaxLevel: 20, + HPBase: 43.20, + HPAdd: 6.48, + ATKBase: 19.20, + ATKAdd: 2.88, + DEFBase: 18, + DEFAdd: 2.70, + }, + { + MaxLevel: 30, + HPBase: 95.04, + HPAdd: 6.48, + ATKBase: 42.24, + ATKAdd: 2.88, + DEFBase: 39.60, + DEFAdd: 2.70, + }, + { + MaxLevel: 40, + HPBase: 164.16, + HPAdd: 6.48, + ATKBase: 72.96, + ATKAdd: 2.88, + DEFBase: 68.40, + DEFAdd: 2.70, + }, + { + MaxLevel: 50, + HPBase: 233.28, + HPAdd: 6.48, + ATKBase: 103.68, + ATKAdd: 2.88, + DEFBase: 97.20, + DEFAdd: 2.70, + }, + { + MaxLevel: 60, + HPBase: 302.40, + HPAdd: 6.48, + ATKBase: 134.40, + ATKAdd: 2.88, + DEFBase: 126, + DEFAdd: 2.70, + }, + { + MaxLevel: 70, + HPBase: 371.52, + HPAdd: 6.48, + ATKBase: 165.12, + ATKAdd: 2.88, + DEFBase: 154.80, + DEFAdd: 2.70, + }, + { + MaxLevel: 80, + HPBase: 440.64, + HPAdd: 6.48, + ATKBase: 195.84, + ATKAdd: 2.88, + DEFBase: 183.60, + DEFAdd: 2.70, + }, +} \ No newline at end of file diff --git a/internal/lightcone/harmony/poisedtobloom/poisedtobloom.go b/internal/lightcone/harmony/poisedtobloom/poisedtobloom.go new file mode 100644 index 00000000..9566a994 --- /dev/null +++ b/internal/lightcone/harmony/poisedtobloom/poisedtobloom.go @@ -0,0 +1,77 @@ +package poisedtobloom + +import ( + "github.com/simimpact/srsim/pkg/engine" + "github.com/simimpact/srsim/pkg/engine/equip/lightcone" + "github.com/simimpact/srsim/pkg/engine/event" + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/modifier" + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +const ( + poised key.Modifier = "poised-to-bloom" + poisedCritDmg key.Modifier = "poised-to-bloom-crit-dmg" +) + +// Lose Not, Forget Not +// Increases the wearer's ATK by 16/20/24/28/32%. Upon entering battle, +// if two or more characters follow the same Path, +// then these characters' CRIT DMG increases by 16/20/24/28/32%. +// Abilities of the same type cannot stack. + +func init() { + lightcone.Register(key.PoisedToBloom, lightcone.Config{ + CreatePassive: Create, + Rarity: 4, + Path: model.Path_HARMONY, + Promotions: promotions, + }) + + modifier.Register(poised, modifier.Config{}) + + modifier.Register(poisedCritDmg, modifier.Config{ + Stacking: modifier.Unique, + }) +} + +func Create(engine engine.Engine, owner key.TargetID, lc info.LightCone) { + atkAmt := 0.12 + 0.04*float64(lc.Imposition) + critDmgAmt := 0.12 + 0.04*float64(lc.Imposition) + + engine.AddModifier(owner, info.Modifier{ + Name: poised, + Source: owner, + Stats: info.PropMap{prop.ATKPercent: atkAmt}, + }) + + engine.Events().BattleStart.Subscribe(func(event event.BattleStart) { + // This is probably slow, but I can't think of other good ways to iterate + // and store paths that don't involve allocating memory + // that might not be used, which I suspect would be slower + // It's still only called once per iteration though, so it should + // be fine. + + // Iterating over all the characters + for _, charA := range engine.Characters() { + charAInfo, _ := engine.CharacterInfo(charA) + // Checking for pairs with them + for _, charB := range engine.Characters() { + charBInfo, _ := engine.CharacterInfo(charB) + // If there's a pair, we apply the crit dmg buff to charA + // we could also apply it to charB, but with no way to remove + // them from the future iterations, that would actually be slower + if charB != charA && charAInfo.Path == charBInfo.Path { + engine.AddModifier(charA, info.Modifier{ + Name: poisedCritDmg, + Source: owner, + Stats: info.PropMap{prop.CritDMG: critDmgAmt}, + }) + break + } + } + } + }) +} diff --git a/pkg/key/lightcone.go b/pkg/key/lightcone.go index c0aa3ceb..1e4ac448 100644 --- a/pkg/key/lightcone.go +++ b/pkg/key/lightcone.go @@ -73,6 +73,7 @@ const ( MemoriesofthePast LightCone = "memories_of_the_past" DanceDanceDance LightCone = "dance_dance_dance" PlanetaryRendezvous LightCone = "planetary_rendezvous" + PoisedToBloom LightCone = "poised_to_bloom" ) // Preservation diff --git a/pkg/simulation/imports.go b/pkg/simulation/imports.go index dd758b1c..78124c71 100644 --- a/pkg/simulation/imports.go +++ b/pkg/simulation/imports.go @@ -71,6 +71,7 @@ import ( _ "github.com/simimpact/srsim/internal/lightcone/harmony/memoriesofthepast" _ "github.com/simimpact/srsim/internal/lightcone/harmony/meshingcogs" _ "github.com/simimpact/srsim/internal/lightcone/harmony/planetaryrendezvous" + _ "github.com/simimpact/srsim/internal/lightcone/harmony/poisedtobloom" _ "github.com/simimpact/srsim/internal/lightcone/hunt/adversarial" _ "github.com/simimpact/srsim/internal/lightcone/hunt/arrows" _ "github.com/simimpact/srsim/internal/lightcone/hunt/cruisinginthestellarsea" diff --git a/tests/testcase/lightcone/poised_to_bloom_test.go b/tests/testcase/lightcone/poised_to_bloom_test.go new file mode 100644 index 00000000..976fefbb --- /dev/null +++ b/tests/testcase/lightcone/poised_to_bloom_test.go @@ -0,0 +1,63 @@ +package lightcone + +import ( + "testing" + + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/tests/testcfg/testchar" + "github.com/simimpact/srsim/tests/testcfg/testcone" + "github.com/simimpact/srsim/tests/teststub" + "github.com/stretchr/testify/suite" +) + +type PTBTest struct { + teststub.Stub +} + +func TestPTBTest(t *testing.T) { + suite.Run(t, new(PTBTest)) +} + +// Testing that PTB indeed does add the correct ATK amount +func (t *PTBTest) Test_AtkAdd() { + bronyaModel := testchar.Bronya() + bronyaModel.LightCone = testcone.PoisedToBloom() + t.Characters.ResetCharacters() + bronya := t.Characters.AddCharacter(bronyaModel) + t.StartSimulation() + // She should have no other atk buffs from anywhere + bronya.Equal(prop.ATKPercent, 0.16) +} + +// Testing that PTB adds crit damage to only characters who share a path with another character in the party +func (t *PTBTest) Test_CritDMG() { + bronyaModel := testchar.Bronya() + bronyaModel.LightCone = testcone.PoisedToBloom() + t.Characters.ResetCharacters() + bronya := t.Characters.AddCharacter(bronyaModel) + dan1 := t.Characters.AddCharacter(testchar.DanHung()) + dan2 := t.Characters.AddCharacter(testchar.DanHung()) + t.StartSimulation() + // 0.50 from base crit damage + 0.16 from poised buff + dan1.Equal(prop.CritDMG, 0.66) + dan2.Equal(prop.CritDMG, 0.66) + // Just the 0.50 base crit damage + bronya.Equal(prop.CritDMG, 0.50) +} + +// Testing that PTB adds crit damage to only characters who share a path with another character in the party +func (t *PTBTest) Test_No_Stacking() { + // I'm a bit concerned about using the same character twice, but hopefully all should be good? + bronyaModel1 := testchar.Bronya() + bronyaModel1.LightCone = testcone.PoisedToBloom() + bronyaModel2 := testchar.Bronya() + bronyaModel2.LightCone = testcone.PoisedToBloom() + t.Characters.ResetCharacters() + bronya1 := t.Characters.AddCharacter(bronyaModel1) + bronya2 := t.Characters.AddCharacter(bronyaModel2) + + t.StartSimulation() + // 0.50 from base crit damage + 0.16 from poised buff + bronya1.Equal(prop.CritDMG, 0.66) + bronya2.Equal(prop.CritDMG, 0.66) +} diff --git a/tests/testcfg/testchar/bronya.go b/tests/testcfg/testchar/bronya.go new file mode 100644 index 00000000..0ff847f4 --- /dev/null +++ b/tests/testcfg/testchar/bronya.go @@ -0,0 +1,27 @@ +package testchar + +import ( + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" + "github.com/simimpact/srsim/tests/testcfg/testcone" +) + +func Bronya() *model.Character { + return &model.Character{ + Key: key.Bronya.String(), + Level: 80, + MaxLevel: 80, + Eidols: 0, + Traces: nil, + Abilities: &model.Abilities{ + Attack: 1, + Skill: 1, + Ult: 1, + Talent: 1, + }, + LightCone: testcone.DanceDanceDance(), + Relics: nil, + StartHp: 0, + StartEnergy: 0, + } +} diff --git a/tests/testcfg/testcone/dance_dance_dance.go b/tests/testcfg/testcone/dance_dance_dance.go new file mode 100644 index 00000000..a6dfc238 --- /dev/null +++ b/tests/testcfg/testcone/dance_dance_dance.go @@ -0,0 +1,15 @@ +package testcone + +import ( + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +func DanceDanceDance() *model.LightCone { + return &model.LightCone{ + Key: key.DanceDanceDance.String(), + Level: 80, + MaxLevel: 80, + Imposition: 1, + } +} diff --git a/tests/testcfg/testcone/poised_to_bloom.go b/tests/testcfg/testcone/poised_to_bloom.go new file mode 100644 index 00000000..bf331ced --- /dev/null +++ b/tests/testcfg/testcone/poised_to_bloom.go @@ -0,0 +1,15 @@ +package testcone + +import ( + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +func PoisedToBloom() *model.LightCone { + return &model.LightCone{ + Key: key.PoisedToBloom.String(), + Level: 80, + MaxLevel: 80, + Imposition: 1, + } +} diff --git a/tests/teststub/stub.go b/tests/teststub/stub.go index 5f837f0e..4a0dffdd 100644 --- a/tests/teststub/stub.go +++ b/tests/teststub/stub.go @@ -1,6 +1,7 @@ package teststub import ( + "context" "encoding/json" "fmt" "time" @@ -45,6 +46,9 @@ type Stub struct { // Assert wraps common assertion methods for convenience Assert assertion + + // Context to check if simulation run is completed + ctx context.Context } func (s *Stub) SetupTest() { @@ -71,13 +75,24 @@ func (s *Stub) TearDownTest() { fmt.Println("Test Finished") logging.InitLoggers() s.cfgEval = nil - select { - case <-s.eventPipe: - s.haltSignaller <- struct{}{} - default: + // hacky way to drain the sim and make sure it finishes first + for { + select { + case <-s.ctx.Done(): // wait for sim to finish + switch s.ctx.Err() { + case context.Canceled: + // finished ok; we can close down + close(s.haltSignaller) + return + default: + // sim did not end without error + panic(s.ctx.Err()) + } + case <-s.eventPipe: + case s.haltSignaller <- struct{}{}: + fmt.Println("forcing continue at end of test") + } } - close(s.eventPipe) - close(s.haltSignaller) } // StartSimulation handles the setup for starting the asynchronous sim run. @@ -99,12 +114,15 @@ func (s *Stub) StartSimulation() { s.simulator.Turn = s.Turn } s.Characters.attributes = s.simulator.Attr + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) + s.ctx = ctx go func() { itres, err := s.simulator.Run() + defer cancel() if err != nil { s.FailNow("Simulation run error", err) } - fmt.Println(itres) + fmt.Printf("test simulation run finished with damage %v\n", itres.TotalDamageDealt) }() // start sim logic, fast-forward sim to BattleStart state, so we can initialize the remaining helper stuff s.Expect(battlestart.ExpectFor()) @@ -118,6 +136,27 @@ func (s *Stub) StartSimulation() { } } +func (s *Stub) WaitForSimulationFinished() error { + // this is hacky as hell but we need to spam continue to let sim finish + // and we do this by consuming all events and spamming continue + for { + select { + case <-s.ctx.Done(): + // check if timed out + switch s.ctx.Err() { + case context.Canceled: + return nil + default: + return s.ctx.Err() + } + case e := <-s.eventPipe: + fmt.Printf("there are more events at end of test: %v\n", e) + case s.haltSignaller <- struct{}{}: + fmt.Println("forcing continue at end of test") + } + } +} + // Expect handles all sorts of checks against events. Refer to eventchecker.EventChecker for more details. func (s *Stub) Expect(checkers ...eventchecker.EventChecker) { for {