Repository: genshinsim/gcsim (go)

Issue:
Eula Hold E with 2 Grimheart should give 2 stacks of 4pc Pale Flame on its own
https://gcsim.app/viewer/share/perm_4LJ8Ub5G1MPHdPBi3rwB1

In here, Eula uses Tap E > wait 10 seconds > Burst > Hold E > N5

The N5 should have been buffed by 2 stacks of 4 pale flame but they only had 1 stack. 

Pull request description:
fix eula icewhirl timing re pale flame, fix swirl applying too much durability in aoe
fixes #375 

Patch:
diff --git a/internal/characters/eula/abil.go b/internal/characters/eula/abil.go
index 71c1aa47e..203426712 100644
--- a/internal/characters/eula/abil.go
+++ b/internal/characters/eula/abil.go
@@ -2,6 +2,7 @@ package eula
 
 import (
 	"fmt"
+
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
@@ -131,7 +132,7 @@ func (c *char) holdE() {
 	}
 	for i := 0; i < v; i++ {
 		//spacing it out for stacks
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 90+i*7, shredCB)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 92+i*7, shredCB)
 	}
 
 	//A2
diff --git a/internal/reactable/swirl.go b/internal/reactable/swirl.go
index 0bc478678..a6d863de4 100644
--- a/internal/reactable/swirl.go
+++ b/internal/reactable/swirl.go
@@ -1,10 +1,12 @@
 package reactable
 
-import "github.com/genshinsim/gcsim/pkg/core"
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
 
 func calcSwirlAtkDurability(consumed, src core.Durability) core.Durability {
-	if consumed < 0.5*src {
-		return 1.25*(consumed-1) + 25
+	if consumed < src {
+		return 1.25*(0.5*consumed-1) + 25
 	}
 	return 1.25*(src-1) + 25
 }


Test patch:
diff --git a/internal/reactable/swirl_test.go b/internal/reactable/swirl_test.go
new file mode 100644
index 000000000..95ef537b5
--- /dev/null
+++ b/internal/reactable/swirl_test.go
@@ -0,0 +1,403 @@
+package reactable
+
+import (
+	"fmt"
+	"math"
+	"testing"
+
+	"github.com/genshinsim/gcsim/internal/tmpl/character"
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func TestSwirl50to25(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 50 applied to ~20")
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 25,
+		},
+	})
+	//1 tick
+	c.Tick()
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 50,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := dur*1.25 + 23.75
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	//no durability
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}
+
+func TestSwirl25to25(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 25 applied to ~20")
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 25,
+		},
+	})
+	//1 tick
+	c.Tick()
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 25,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := core.Durability(25)*1.25 + 23.75
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	//no durability
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}
+
+func TestSwirl25to50(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 25 applied to ~40")
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 50,
+		},
+	})
+	//1 tick
+	c.Tick()
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 25,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := core.Durability(25)*1.25 + 23.75
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	//no durability
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}
+
+func TestSwirl50to50(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 50 applied to ~40")
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 50,
+		},
+	})
+	//1 tick
+	c.Tick()
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 50,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := core.Durability(50)*1.25 + 23.75
+
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+	//no durability
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}
+
+func TestSwirl25to10(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 25 applied to ~10")
+
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 25,
+		},
+	})
+	//tick 285
+	for i := 0; i < 285; i++ {
+		c.Tick()
+	}
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 25,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := dur*1.25 + 23.75
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	//no durability
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}
+
+func TestSwirl50to10(t *testing.T) {
+	fmt.Println("------------------------------\ntesting swirl 50 applied to ~10")
+
+	c := testCore()
+
+	char, err := character.NewTemplateChar(c, testChar)
+	if err != nil {
+		t.Error(err)
+		t.FailNow()
+	}
+	c.Chars = append(c.Chars, char)
+	trg := &testTarget{src: 1}
+	trg.Reactable = &Reactable{}
+	trg.Init(trg, c)
+	c.Targets = append(c.Targets, trg)
+
+	trg2 := &testTarget{src: 1}
+	trg2.Reactable = &Reactable{}
+	trg2.Init(trg, c)
+	c.Targets = append(c.Targets, trg2)
+
+	c.Init()
+
+	var src *core.AttackEvent
+	trg2.onDmgCallBack = func(atk *core.AttackEvent) (float64, bool) {
+		src = atk
+		// log.Println(atk.Info.Abil)
+		// log.Println(atk.Info.Element)
+		// log.Println(atk)
+		return 0, false
+	}
+
+	//apply 25 pyro first
+	trg.React(&core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Pyro,
+			Durability: 25,
+		},
+	})
+	//tick 285
+	for i := 0; i < 285; i++ {
+		c.Tick()
+	}
+	//check durability after 1 tick
+	dur := trg.Durability[core.Pyro]
+	fmt.Printf("pyro left: %v\n", dur)
+	next := &core.AttackEvent{
+		Info: core.AttackInfo{
+			Element:    core.Anemo,
+			Durability: 25,
+		},
+	}
+	trg.React(next)
+	//dmg should trigger next tick
+	//i'm expecting an aoe swirl with durability = dur * 1.25 + 23.75
+	expected := dur*1.25 + 23.75
+	c.Tick()
+	if src == nil || src.Info.Abil != "swirl-pyro (aoe)" {
+		t.Errorf("expecting swirl, got %v", src)
+	}
+	//no durability
+	fmt.Printf("expected durability to be %v, got %v\n", expected, src.Info.Durability)
+
+	if math.Abs(float64(src.Info.Durability-expected)) > float64(ZeroDur) {
+		t.Errorf("expected durability to be %v, got %v", expected, src.Info.Durability)
+	}
+}