Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions internal/lightcone/harmony/poisedtobloom/data.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions internal/lightcone/harmony/poisedtobloom/poisedtobloom.go
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requires StatusType: model.StatusType_STATUS_BUFF,
nit: Stacking is Unique by default so can be removed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can also use CanDispel: true, now

})
}

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can use the same variable for both


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
}
}
}
})
}
1 change: 1 addition & 0 deletions pkg/key/lightcone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/simulation/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
63 changes: 63 additions & 0 deletions tests/testcase/lightcone/poised_to_bloom_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 27 additions & 0 deletions tests/testcfg/testchar/bronya.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
15 changes: 15 additions & 0 deletions tests/testcfg/testcone/dance_dance_dance.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
15 changes: 15 additions & 0 deletions tests/testcfg/testcone/poised_to_bloom.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
53 changes: 46 additions & 7 deletions tests/teststub/stub.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package teststub

import (
"context"
"encoding/json"
"fmt"
"time"
Expand Down Expand Up @@ -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() {
Expand All @@ -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.
Expand All @@ -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())
Expand All @@ -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 {
Expand Down