Repository: genshinsim/gcsim
Pull request #345 (2022-02-26 23:37:04)

Predator Initial
* Add Predator + import


Patch:
diff --git a/.github/workflows/cmd-build.yaml b/.github/workflows/cmd-build.yaml
index a04b69674..b820e3884 100644
--- a/.github/workflows/cmd-build.yaml
+++ b/.github/workflows/cmd-build.yaml
@@ -28,11 +28,9 @@ jobs:
         working-directory: ./
       # - run: ls
       #   working-directory: ./scripts
-      - uses: "marvinpinto/action-automatic-releases@latest"
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        if: startsWith(github.ref, 'refs/tags/')
         with:
-          repo_token: "${{ secrets.GITHUB_TOKEN }}"
-          prerelease: false
           files: |
-            release-notes.txt
-            LICENSE
             gcsim.exe
diff --git a/cmd/gcsim/.gitignore b/cmd/gcsim/.gitignore
index cf4a0f198..409005293 100644
--- a/cmd/gcsim/.gitignore
+++ b/cmd/gcsim/.gitignore
@@ -2,4 +2,5 @@
 *.json
 *.gz
 gcsim
-gcsim.exe
\ No newline at end of file
+gcsim.exe
+config.txt
\ No newline at end of file
diff --git a/cmd/gcsim/config.txt b/cmd/gcsim/config.txt
deleted file mode 100644
index 33fb8c03d..000000000
--- a/cmd/gcsim/config.txt
+++ /dev/null
@@ -1,49 +0,0 @@
-options debug=true iteration=1000 duration=92 workers=60 mode=sl;
-
-xiao char lvl=90/90 cons=0 talent=9,9,9;
-xiao add weapon="favoniuslance" refine=5 lvl=90/90;
-xiao add set="viridescentvenerer" count=4;
-xiao add stats hp=4780 atk=311 atk%=0.466 anemo%=0.466 cr=0.311; #main
-xiao add stats def%=0.124 def=39.36 hp=507.88 hp%=0.0992 atk=33.08 atk%=0.1984 er=0.1102 em=39.64 cr=0.331 cd=0.7944;
-
-xiangling char lvl=90/90 cons=6 talent=9,9,9;
-xiangling add weapon="thecatch" refine=5 lvl=90/90;
-xiangling add set="emblemofseveredfate" count=4;
-xiangling add stats hp=4780 atk=311 er=0.518 pyro%=0.466 cr=0.311; #main
-xiangling add stats def%=0.124 def=39.36 hp=507.88 hp%=0.0992 atk=33.08 atk%=0.0992 er=0.4408 em=39.64 cr=0.2648 cd=0.662;
-
-xingqiu char lvl=90/90 cons=6 talent=9,9,9; 
-xingqiu add weapon="favoniussword" refine=1 lvl=90/90;
-xingqiu add set="emblemofseveredfate" count=4;
-xingqiu add stats hp=4780 atk=311 atk%=0.466 hydro%=0.466 cr=0.311; #main
-xingqiu add stats def%=0.124 def=39.36 hp=507.88 hp%=0.0992 atk=33.08 atk%=0.1984 er=0.1102 em=39.64 cr=0.331 cd=0.7944;
-
-bennett char lvl=90/90 cons=5 talent=9,9,9;
-bennett add weapon="prototyperancour" refine=1 lvl=90/90;
-bennett add set="noblesseoblige" count=4;
-bennett add stats hp=4780 atk=311 er=0.518 pyro%=0.466 cr=0.311; #main
-bennett add stats def%=0.124 def=39.36 hp=507.88 hp%=0.0992 atk=33.08 atk%=0.0992 er=0.1102 em=237.84 cr=0.2317 cd=0.4634;
-
-
-##Default Enemy
-target lvl=100 resist=0.1;
-
-## HP Particles
-energy every interval=480,720 amount=1;
-
-##Actions List
-active xingqiu;
-
-xingqiu skill,burst,attack;
-
-bennett skill,attack,burst;
-wait 20;
-
-xiangling attack,burst,skill;
-
-xiao attack,skill,skill,burst,
-attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],
-attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],attack,high_plunge[plunge_hits=1],
-attack,high_plunge[plunge_hits=1];
-
-restart;
\ No newline at end of file
diff --git a/internal/artifacts/exile/exile.go b/internal/artifacts/exile/exile.go
new file mode 100644
index 000000000..1ccc2e861
--- /dev/null
+++ b/internal/artifacts/exile/exile.go
@@ -0,0 +1,53 @@
+package exile
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterSetFunc("exile", New)
+	core.RegisterSetFunc("theexile", New)
+}
+
+// 2-Piece Bonus: Energy Recharge +20%.
+// 4-Piece Bonus: Using an Elemental Burst regenerates 2 Energy for all party members (excluding the wearer) every 2s for 6s. This effect cannot stack.
+func New(c core.Character, s *core.Core, count int, params map[string]int) {
+	if count >= 2 {
+		m := make([]float64, core.EndStatType)
+		m[core.ER] = .20
+		c.AddMod(core.CharStatMod{
+			Key:    "exile-2pc",
+			Expiry: -1,
+			Amount: func() ([]float64, bool) {
+				return m, true
+			},
+		})
+	}
+	if count >= 4 {
+		// TODO: does multiple exile holders extend the duration?
+		s.Events.Subscribe(core.PreBurst, func(args ...interface{}) bool {
+			if s.ActiveChar != c.CharIndex() {
+				return false
+			}
+			if s.Status.Duration("exile") > 0 {
+				return false
+			}
+			s.Status.AddStatus("exile", 360)
+
+			for _, char := range s.Chars {
+				this := char
+				if c.CharIndex() == this.CharIndex() {
+					continue
+				}
+				// 3 ticks
+				for i := 120; i <= 360; i += 120 {
+					this.AddTask(func() { this.AddEnergy("exile-4pc", 2) }, "exile-energy", i)
+				}
+			}
+
+			return false
+		}, fmt.Sprintf("exile-4pc-%v", c.Name()))
+	}
+}
diff --git a/internal/artifacts/reminiscence/reminiscence.go b/internal/artifacts/reminiscence/reminiscence.go
index 1f3682d6f..55e1aec3d 100644
--- a/internal/artifacts/reminiscence/reminiscence.go
+++ b/internal/artifacts/reminiscence/reminiscence.go
@@ -17,11 +17,11 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 		m := make([]float64, core.EndStatType)
 		m[core.ATKP] = 0.18
 		c.AddMod(core.CharStatMod{
-			Key: "shim-2pc",
+			Key:    "shim-2pc",
+			Expiry: -1,
 			Amount: func() ([]float64, bool) {
 				return m, true
 			},
-			Expiry: -1,
 		})
 	}
 	//11:51 AM] Episodde｜ShimenawaChildePeddler: Basically I found out that the fox set energy tax have around a 10 frame delay.
@@ -31,26 +31,33 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 	if count >= 4 {
 		m := make([]float64, core.EndStatType)
 		m[core.DmgP] = 0.50
+		cd := -1
 		s.Events.Subscribe(core.PreSkill, func(args ...interface{}) bool {
 			if s.ActiveChar != c.CharIndex() {
 				return false
 			}
-			if c.CurrentEnergy() > 15 {
-				//consume 15 energy, increased normal/charge/plunge dmg by 50%
-				s.Tasks.Add(func() {
-					c.AddEnergy("shim-4pc", -15)
-				}, 10)
-				c.AddPreDamageMod(core.PreDamageMod{
-					Key: "shim-4pc",
-					Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
-						return m, atk.Info.AttackTag == core.AttackTagNormal || atk.Info.AttackTag == core.AttackTagExtra || atk.Info.AttackTag == core.AttackTagPlunge
-					},
-					Expiry: s.F + 600,
-				})
+			if c.CurrentEnergy() < 15 {
+				return false
 			}
+			if s.F < cd {
+				return false
+			}
+			cd = s.F + 60*10
+
+			//consume 15 energy, increased normal/charge/plunge dmg by 50%
+			s.Tasks.Add(func() {
+				c.AddEnergy("shim-4pc", -15)
+			}, 10)
+			c.AddPreDamageMod(core.PreDamageMod{
+				Key:    "shim-4pc",
+				Expiry: s.F + 60*10,
+				Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+					return m, atk.Info.AttackTag == core.AttackTagNormal || atk.Info.AttackTag == core.AttackTagExtra || atk.Info.AttackTag == core.AttackTagPlunge
+				},
+			})
+
 			return false
 		}, fmt.Sprintf("shim-4pc-%v", c.Name()))
 
 	}
-	//add flat stat to char
 }
diff --git a/internal/characters/albedo/albedo.go b/internal/characters/albedo/albedo.go
index 03f6f4f41..5486e74d7 100644
--- a/internal/characters/albedo/albedo.go
+++ b/internal/characters/albedo/albedo.go
@@ -27,7 +27,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
diff --git a/internal/characters/aloy/aloy.go b/internal/characters/aloy/aloy.go
index de3ff1228..e6cd1e61d 100644
--- a/internal/characters/aloy/aloy.go
+++ b/internal/characters/aloy/aloy.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 4
diff --git a/internal/characters/amber/amber.go b/internal/characters/amber/amber.go
index 962686c33..2a6046f59 100644
--- a/internal/characters/amber/amber.go
+++ b/internal/characters/amber/amber.go
@@ -26,7 +26,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 5
diff --git a/internal/characters/ayaka/ayaka.go b/internal/characters/ayaka/ayaka.go
index abe274090..a882afb05 100644
--- a/internal/characters/ayaka/ayaka.go
+++ b/internal/characters/ayaka/ayaka.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSword
 	c.CharZone = core.ZoneInazuma
diff --git a/internal/characters/barbara/abil.go b/internal/characters/barbara/abil.go
index fb57d40e0..f2ceb94fd 100644
--- a/internal/characters/barbara/abil.go
+++ b/internal/characters/barbara/abil.go
@@ -29,8 +29,13 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		//check for healing
 		if c.Core.Status.Duration("barbskill") > 0 {
 			//heal target
-			heal := (prochpp[c.TalentLvlSkill()]*c.MaxHP() + prochp[c.TalentLvlSkill()])
-			c.Core.Health.HealAll(c.Index, heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  -1,
+				Message: "Melody Loop (Normal Attack)",
+				Src:     prochpp[c.TalentLvlSkill()]*c.MaxHP() + prochp[c.TalentLvlSkill()],
+				Bonus:   c.Stat(core.Heal),
+			})
 			done = true
 		}
 
@@ -67,8 +72,13 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		//check for healing
 		if c.Core.Status.Duration("barbskill") > 0 {
 			//heal target
-			heal := (prochpp[c.TalentLvlSkill()]*c.MaxHP() + prochp[c.TalentLvlSkill()])
-			c.Core.Health.HealAll(c.Index, 4*heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  -1,
+				Message: "Melody Loop (Charged Attack)",
+				Src:     4 * (prochpp[c.TalentLvlSkill()]*c.MaxHP() + prochp[c.TalentLvlSkill()]),
+				Bonus:   c.Stat(core.Heal),
+			})
 			done = true
 		}
 
@@ -120,18 +130,18 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	stats, _ := c.SnapshotStats()
 	hpplus := stats[core.Heal]
-	heal := (skillhp[c.TalentLvlSkill()] + skillhpp[c.TalentLvlSkill()]*c.MaxHP()) * (1 + hpplus)
+	heal := skillhp[c.TalentLvlSkill()] + skillhpp[c.TalentLvlSkill()]*c.MaxHP()
 	//apply right away
 
 	c.skillInitF = c.Core.F
 	c.onSkillStackCount(c.Core.F)
 	//add 1 tick each 5s
 	//first tick starts at 0
-	c.barbaraHealTick(heal, c.Core.F)()
+	c.barbaraHealTick(heal, hpplus, c.Core.F)()
 	ai.Abil = "Let the Show Begin♪ Wet Tick"
+	ai.AttackTag = core.AttackTagNone
 	ai.Mult = 0
 	c.barbaraWet(ai, c.Core.F)()
-	c.Energy = 0
 	if c.Base.Cons >= 2 {
 		c.SetCD(core.ActionSkill, 32*60*0.85)
 	} else {
@@ -140,7 +150,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	return f, a //todo fix field cast time
 }
 
-func (c *char) barbaraHealTick(healAmt float64, skillInitF int) func() {
+func (c *char) barbaraHealTick(healAmt float64, hpplus float64, skillInitF int) func() {
 	return func() {
 		//make sure it's not overwritten
 		if c.skillInitF != skillInitF {
@@ -150,11 +160,17 @@ func (c *char) barbaraHealTick(healAmt float64, skillInitF int) func() {
 		if c.Core.Status.Duration("barbskill") == 0 {
 			return
 		}
-		c.Core.Log.NewEvent("barbara heal ticking", core.LogCharacterEvent, c.Index)
-		c.Core.Health.HealActive(c.Index, healAmt)
+		// c.Core.Log.NewEvent("barbara heal ticking", core.LogCharacterEvent, c.Index)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  c.Core.ActiveChar,
+			Message: "Melody Loop (Tick)",
+			Src:     healAmt,
+			Bonus:   hpplus,
+		})
 
 		// tick per 5 seconds
-		c.AddTask(c.barbaraHealTick(healAmt, skillInitF), "barbara-heal-tick", 5*60)
+		c.AddTask(c.barbaraHealTick(healAmt, hpplus, skillInitF), "barbara-heal-tick", 5*60)
 	}
 }
 
@@ -183,11 +199,15 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	stats, _ := c.SnapshotStats()
 
-	hpplus := stats[core.Heal]
-	heal := (bursthp[c.TalentLvlBurst()] + bursthpp[c.TalentLvlBurst()]*c.MaxHP()) * (1 + hpplus)
-	c.Core.Health.HealAll(c.Index, heal)
+	c.Core.Health.Heal(core.HealInfo{
+		Caller:  c.Index,
+		Target:  -1,
+		Message: "Shining Miracle♪",
+		Src:     bursthp[c.TalentLvlBurst()] + bursthpp[c.TalentLvlBurst()]*c.MaxHP(),
+		Bonus:   stats[core.Heal],
+	})
 
-	c.Energy = 0
+	c.ConsumeEnergy(8)
 	c.SetCD(core.ActionBurst, 20*60)
 	return f, a //todo fix field cast time
 }
diff --git a/internal/characters/barbara/barbara.go b/internal/characters/barbara/barbara.go
index 14daa75ad..1c813617a 100644
--- a/internal/characters/barbara/barbara.go
+++ b/internal/characters/barbara/barbara.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 		return nil, err
 	}
 	c.Tmpl = t
-	c.Energy = 80
+	c.Base.Element = core.Hydro
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.BurstCon = 3
diff --git a/internal/characters/beidou/abil.go b/internal/characters/beidou/abil.go
index 9c52283b9..e5f7fb185 100644
--- a/internal/characters/beidou/abil.go
+++ b/internal/characters/beidou/abil.go
@@ -143,6 +143,10 @@ func (c *char) burstProc() {
 		if ae.Info.AttackTag != core.AttackTagNormal && ae.Info.AttackTag != core.AttackTagExtra {
 			return false
 		}
+		//make sure the person triggering the attack is on field still
+		if ae.Info.ActorIndex != c.Core.ActiveChar {
+			return false
+		}
 		if c.Core.Status.Duration("beidouburst") == 0 {
 			return false
 		}
diff --git a/internal/characters/beidou/beidou.go b/internal/characters/beidou/beidou.go
index e264f7aab..0625086c8 100644
--- a/internal/characters/beidou/beidou.go
+++ b/internal/characters/beidou/beidou.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 5
diff --git a/internal/characters/bennett/bennett.go b/internal/characters/bennett/bennett.go
index 5bcfcab57..c1b2dd8b5 100644
--- a/internal/characters/bennett/bennett.go
+++ b/internal/characters/bennett/bennett.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 		return nil, err
 	}
 	c.Tmpl = t
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
@@ -263,7 +268,7 @@ const bennettSelfInfusionDurationInFrames = 126
 
 func (c *char) applyBennettField(stats [core.EndStatType]float64) func() {
 	hpplus := stats[core.Heal]
-	heal := (bursthp[c.TalentLvlBurst()] + bursthpp[c.TalentLvlBurst()]*c.MaxHP()) * (1 + hpplus)
+	heal := bursthp[c.TalentLvlBurst()] + bursthpp[c.TalentLvlBurst()]*c.MaxHP()
 	pc := burstatk[c.TalentLvlBurst()]
 	if c.Base.Cons >= 1 {
 		pc += 0.2
@@ -282,7 +287,13 @@ func (c *char) applyBennettField(stats [core.EndStatType]float64) func() {
 		active := c.Core.Chars[c.Core.ActiveChar]
 		//heal if under 70%
 		if active.HP()/active.MaxHP() < .7 {
-			c.Core.Health.HealActive(c.Index, heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  c.Core.ActiveChar,
+				Message: "Inspiration Field",
+				Src:     heal,
+				Bonus:   hpplus,
+			})
 		}
 
 		//add attack if over 70%
diff --git a/internal/characters/chongyun/chongyun.go b/internal/characters/chongyun/chongyun.go
index 693200293..2d56dcfaf 100644
--- a/internal/characters/chongyun/chongyun.go
+++ b/internal/characters/chongyun/chongyun.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 4
diff --git a/internal/characters/diluc/diluc.go b/internal/characters/diluc/diluc.go
index 93b4aefd9..c96ebf175 100644
--- a/internal/characters/diluc/diluc.go
+++ b/internal/characters/diluc/diluc.go
@@ -25,7 +25,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 4
diff --git a/internal/characters/diona/abil.go b/internal/characters/diona/abil.go
index 0a2126bb2..7a5ca1b03 100644
--- a/internal/characters/diona/abil.go
+++ b/internal/characters/diona/abil.go
@@ -88,7 +88,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		AttackTag:  core.AttackTagElementalArt,
 		ICDTag:     core.ICDTagElementalArt,
 		ICDGroup:   core.ICDGroupDefault,
-		StrikeType: core.StrikeTypePierce,
+		StrikeType: core.StrikeTypeDefault,
 		Element:    core.Cryo,
 		Durability: 25,
 		Mult:       paw[c.TalentLvlSkill()],
@@ -144,14 +144,20 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	snap := c.Snapshot(&ai)
 	hpplus := snap.Stats[core.Heal]
 	maxhp := c.MaxHP()
-	heal := (burstHealPer[c.TalentLvlBurst()]*maxhp + burstHealFlat[c.TalentLvlBurst()]) * (1 + hpplus)
+	heal := burstHealPer[c.TalentLvlBurst()]*maxhp + burstHealFlat[c.TalentLvlBurst()]
 
 	//ticks every 2s, first tick at t=1s, then t=3,5,7,9,11, lasts for 12.5
 	for i := 0; i < 6; i++ {
 		c.AddTask(func() {
 			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(5, false, core.TargettableEnemy), 0)
-			c.Core.Log.NewEvent("diona healing", core.LogCharacterEvent, c.Index, "+heal", hpplus, "max hp", maxhp, "heal amount", heal)
-			c.Core.Health.HealActive(c.Index, heal)
+			// c.Core.Log.NewEvent("diona healing", core.LogCharacterEvent, c.Index, "+heal", hpplus, "max hp", maxhp, "heal amount", heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  c.Core.ActiveChar,
+				Message: "Drunken Mist",
+				Src:     heal,
+				Bonus:   hpplus,
+			})
 		}, "Diona Burst (DOT)", 60+i*120)
 	}
 
diff --git a/internal/characters/diona/diona.go b/internal/characters/diona/diona.go
index 89fdbb109..2333a978c 100644
--- a/internal/characters/diona/diona.go
+++ b/internal/characters/diona/diona.go
@@ -21,7 +21,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 5
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/characters/eula/eula.go b/internal/characters/eula/eula.go
index c8c5ca4aa..c73463c8f 100644
--- a/internal/characters/eula/eula.go
+++ b/internal/characters/eula/eula.go
@@ -25,7 +25,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 5
diff --git a/internal/characters/fischl/fischl.go b/internal/characters/fischl/fischl.go
index bef042ea2..117828f8f 100644
--- a/internal/characters/fischl/fischl.go
+++ b/internal/characters/fischl/fischl.go
@@ -28,7 +28,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 5
@@ -240,7 +245,13 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		//heal at end of animation
 		heal := c.MaxHP() * 0.2
 		c.AddTask(func() {
-			c.Core.Health.HealActive(c.Index, heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  c.Index,
+				Message: "Her Pilgrimage of Bleak",
+				Src:     heal,
+				Bonus:   c.Stat(core.Heal),
+			})
 		}, "c4heal", f-1)
 
 	}
diff --git a/internal/characters/ganyu/abil.go b/internal/characters/ganyu/abil.go
index b5d8d02a4..43dc66024 100644
--- a/internal/characters/ganyu/abil.go
+++ b/internal/characters/ganyu/abil.go
@@ -153,6 +153,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	}
 	snap := c.Snapshot(&ai)
 
+	c.Core.Status.AddStatus("ganyuburst", 15*60)
+
 	rad, ok := p["radius"]
 	if !ok {
 		rad = 1
diff --git a/internal/characters/ganyu/ganyu.go b/internal/characters/ganyu/ganyu.go
index 75b361e36..19c6a0952 100644
--- a/internal/characters/ganyu/ganyu.go
+++ b/internal/characters/ganyu/ganyu.go
@@ -22,7 +22,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 6
diff --git a/internal/characters/gorou/abil.go b/internal/characters/gorou/abil.go
index ef6c1e4ed..dce23323e 100644
--- a/internal/characters/gorou/abil.go
+++ b/internal/characters/gorou/abil.go
@@ -176,9 +176,14 @@ func (c *char) gorouSkillHealField(src int, stats []float64) func() {
 		}
 		//When General's Glory is in the "Impregnable" or "Crunch" states, it will also heal active characters
 		//within its AoE by 50% of Gorou's own DEF every 1.5s.
-		//TODO: healing bonus
 		amt := c.Base.Def*(1+stats[core.DEFP]) + stats[core.DEF]
-		c.Core.Health.HealActive(c.Index, 0.5*amt)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  c.Core.ActiveChar,
+			Message: "Lapping Hound: Warm as Water",
+			Src:     0.5 * amt,
+			Bonus:   c.Stat(core.Heal),
+		})
 
 		//tick every 1.5s
 		c.Core.Tasks.Add(c.gorouSkillBuffField(src), 90)
diff --git a/internal/characters/gorou/gorou.go b/internal/characters/gorou/gorou.go
index e6f49923f..418877a19 100644
--- a/internal/characters/gorou/gorou.go
+++ b/internal/characters/gorou/gorou.go
@@ -38,7 +38,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 4
@@ -68,8 +73,8 @@ func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	}
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 
 	/**
 	Provides up to 3 buffs to active characters within the skill's AoE based on the number of Geo characters in the party at the time of casting:
diff --git a/internal/characters/hutao/abil.go b/internal/characters/hutao/abil.go
index 325c82cd4..164089347 100644
--- a/internal/characters/hutao/abil.go
+++ b/internal/characters/hutao/abil.go
@@ -195,7 +195,13 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	if count > 5 {
 		count = 5
 	}
-	c.HPCurrent += c.HPMax * float64(count) * regen
+	c.Core.Health.Heal(core.HealInfo{
+		Caller:  c.Index,
+		Target:  c.Index,
+		Message: "Spirit Soother",
+		Src:     c.HPMax * float64(count) * regen,
+		Bonus:   c.Stat(core.Heal),
+	})
 
 	f, a := c.ActionFrames(core.ActionBurst, p)
 
diff --git a/internal/characters/hutao/hutao.go b/internal/characters/hutao/hutao.go
index 8d8f78b91..333ccba4a 100644
--- a/internal/characters/hutao/hutao.go
+++ b/internal/characters/hutao/hutao.go
@@ -27,7 +27,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSpear
 	c.NormalHitNum = 6
diff --git a/internal/characters/itto/itto.go b/internal/characters/itto/itto.go
index 49b1c2e35..f062f94e9 100644
--- a/internal/characters/itto/itto.go
+++ b/internal/characters/itto/itto.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 70
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 70
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 70
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 4
diff --git a/internal/characters/jean/jean.go b/internal/characters/jean/jean.go
index aa612f8cc..ed07ebcdc 100644
--- a/internal/characters/jean/jean.go
+++ b/internal/characters/jean/jean.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Anemo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
@@ -97,7 +102,13 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		//check for healing
 		if c.Core.Rand.Float64() < 0.5 {
 			heal := 0.15 * (snap.BaseAtk*(1+snap.Stats[core.ATKP]) + snap.Stats[core.ATK])
-			c.Core.Health.HealAll(c.Index, heal)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  -1,
+				Message: "Wind Companion",
+				Src:     heal,
+				Bonus:   c.Stat(core.Heal),
+			})
 		}
 	}, "jean-na", f-1)
 
@@ -205,18 +216,30 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	//heal on cast
 	hpplus := snap.Stats[core.Heal]
 	atk := snap.BaseAtk*(1+snap.Stats[core.ATKP]) + snap.Stats[core.ATK]
-	heal := (burstInitialHealFlat[c.TalentLvlBurst()] + atk*burstInitialHealPer[c.TalentLvlBurst()]) * (1 + hpplus)
-	healDot := (burstDotHealFlat[c.TalentLvlBurst()] + atk*burstDotHealPer[c.TalentLvlBurst()]) * (1 + hpplus)
+	heal := burstInitialHealFlat[c.TalentLvlBurst()] + atk*burstInitialHealPer[c.TalentLvlBurst()]
+	healDot := burstDotHealFlat[c.TalentLvlBurst()] + atk*burstDotHealPer[c.TalentLvlBurst()]
 
 	c.AddTask(func() {
-		c.Core.Health.HealAll(c.Index, heal)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  -1,
+			Message: "Dandelion Breeze",
+			Src:     heal,
+			Bonus:   hpplus,
+		})
 	}, "Jean Heal Initial", f)
 
 	//duration is 10.5s
 	for i := 60; i < 630; i++ {
 		c.AddTask(func() {
-			c.Core.Log.NewEvent("jean q healing", core.LogCharacterEvent, c.Index, "+heal", hpplus, "atk", atk, "heal amount", healDot)
-			c.Core.Health.HealActive(c.Index, heal)
+			// c.Core.Log.NewEvent("jean q healing", core.LogCharacterEvent, c.Index, "+heal", hpplus, "atk", atk, "heal amount", healDot)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  c.Core.ActiveChar,
+				Message: "Dandelion Field",
+				Src:     healDot,
+				Bonus:   hpplus,
+			})
 		}, "Jean Tick", i)
 	}
 
diff --git a/internal/characters/kaeya/kaeya.go b/internal/characters/kaeya/kaeya.go
index bb9f2ac18..29fb08dc2 100644
--- a/internal/characters/kaeya/kaeya.go
+++ b/internal/characters/kaeya/kaeya.go
@@ -25,7 +25,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
@@ -116,7 +121,13 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	a4count := 0
 	cb := func(a core.AttackCB) {
 		heal := .15 * (a.AttackEvent.Snapshot.BaseAtk*(1+a.AttackEvent.Snapshot.Stats[core.ATKP]) + a.AttackEvent.Snapshot.Stats[core.ATK])
-		c.Core.Health.HealActive(c.Index, heal)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  c.Core.ActiveChar,
+			Message: "Cold-Blooded Strike",
+			Src:     heal,
+			Bonus:   c.Stat(core.Heal),
+		})
 		//if target is frozen after hit then drop additional energy;
 		if a4count == 2 {
 			return
diff --git a/internal/characters/kazuha/abil.go b/internal/characters/kazuha/abil.go
index 468729ba5..ddb5e20e0 100644
--- a/internal/characters/kazuha/abil.go
+++ b/internal/characters/kazuha/abil.go
@@ -6,6 +6,14 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{
+	{12},         //n1
+	{11},         //n2
+	{16, 25},     //n3
+	{15},         //n4
+	{15, 23, 31}, //n5
+}
+
 func (c *char) Attack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionAttack, p)
@@ -23,7 +31,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 
 	for i, mult := range attack[c.NormalCounter] {
 		ai.Mult = mult[c.TalentLvlAttack()]
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.3, false, core.TargettableEnemy), f-2+i, f-2+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.3, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -48,7 +56,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 	for i, mult := range charge {
 		ai.Mult = mult[c.TalentLvlAttack()]
 		ai.Abil = fmt.Sprintf("Charge %v", i)
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f-len(charge)+i, f-len(charge)+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 20+i, 20+i)
 	}
 
 	return f, a
@@ -74,7 +82,7 @@ func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
 			Mult:           plunge[c.TalentLvlAttack()],
 			IgnoreInfusion: true,
 		}
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.3, false, core.TargettableEnemy), f-10, f-10)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.3, false, core.TargettableEnemy), f, f)
 	}
 
 	//aoe dmg
@@ -91,7 +99,7 @@ func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
 		IgnoreInfusion: true,
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), f-8, f-8)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), f, f)
 
 	// a2 if applies
 	if c.a2Ele != core.NoElement {
@@ -108,7 +116,7 @@ func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
 			IgnoreInfusion: true,
 		}
 
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 10, 10)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), f-1, f-1)
 		c.a2Ele = core.NoElement
 	}
 
@@ -137,11 +145,11 @@ func (c *char) skillPress(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       skill[c.TalentLvlSkill()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 13)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, f)
 
 	c.QueueParticle("kazuha", 3, core.Anemo, 100)
 
-	c.AddTask(c.absorbCheckA2(c.Core.F, 0, int(f/18)), "kaz-a2-absorb-check", 1)
+	c.AddTask(c.absorbCheckA2(c.Core.F, 0, int(f/6)), "kaz-a2-absorb-check", 1)
 
 	cd := 360
 	if c.Base.Cons > 0 {
@@ -169,11 +177,11 @@ func (c *char) skillHold(p map[string]int) (int, int) {
 		Mult:       skillHold[c.TalentLvlSkill()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 34)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, f)
 
 	c.QueueParticle("kazuha", 4, core.Anemo, 100)
 
-	c.AddTask(c.absorbCheckA2(c.Core.F, 0, int(f/18)), "kaz-a2-absorb-check", 1)
+	c.AddTask(c.absorbCheckA2(c.Core.F, 0, int(f/6)), "kaz-a2-absorb-check", 1)
 	cd := 540
 	if c.Base.Cons > 0 {
 		cd = 486
@@ -303,6 +311,6 @@ func (c *char) absorbCheckA2(src, count, max int) func() {
 			return
 		}
 		//otherwise queue up
-		c.AddTask(c.absorbCheckA2(src, count+1, max), "kaz-a2-absorb-check", 18)
+		c.AddTask(c.absorbCheckA2(src, count+1, max), "kaz-a2-absorb-check", 6)
 	}
 }
diff --git a/internal/characters/kazuha/frames.go b/internal/characters/kazuha/frames.go
index 2131fe430..0d4a3a2b9 100644
--- a/internal/characters/kazuha/frames.go
+++ b/internal/characters/kazuha/frames.go
@@ -8,48 +8,147 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
 		f := 0
+		a := 0
 		switch c.NormalCounter {
 		//TODO: need to add atkspd mod
 		case 0:
-			//add frames if last action is also attack
-			if c.Core.LastAction.Target == core.Kazuha && c.Core.LastAction.Typ == core.ActionAttack {
-				f += 60
-			}
-			f = 14
+			f = 12
+			a = 18
 		case 1:
-			f = 34 - 14
+			f = 11
+			a = 25
 		case 2:
-			f = 70 - 34 //hit at 60, 70
+			f = 25 //hit mark 16, 25
+			a = 35
 		case 3:
-			f = 97 - 70
+			f = 15 //hit mark
+			a = 40
 		case 4:
-			f = 140 - 97
+			f = 31 //hit mark 15, 23, 31
+			a = 71
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionCharge:
-		return 44, 44 // kqm lib
+		return 21, 55 // kqm lib
 	case core.ActionHighPlunge:
 		c.Core.Log.NewEvent("plunge skill check", core.LogCharacterEvent, c.Index, "previous", c.Core.LastAction)
 		if c.Core.LastAction.Target == core.Kazuha && c.Core.LastAction.Typ == core.ActionSkill {
-			_, ok := c.Core.LastAction.Param["hold"]
-			if ok {
-				return 41, 41
+			h := c.Core.LastAction.Param["hold"]
+			if h > 0 {
+				return 41, 60
 			}
-			return 36, 36
+			return 36, 55
 		}
 		c.Core.Log.NewEvent("invalid plunge (missing skill use)", core.LogActionEvent, c.Index, "action", a)
 		return 0, 0
 	case core.ActionSkill:
-		_, ok := p["hold"]
-		if ok {
-			return 58, 58
+		h := p["hold"]
+		if h > 0 {
+			return 34, 180
 		}
-		return 27, 27
+		return 14, 88
 	case core.ActionBurst:
-		return 95, 95
+		return 95, 100
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+	//normal cancels
+	c.SetNormalCancelFrames(0, core.ActionAttack, 16-12) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 16-12) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 20-11) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 25-11) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 30-25) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 35-25) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 40-15) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 36-15) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 71-31) //n5 -> next attack (n1)
+	// c.SetNormalCancelFrames(4, core.ActionCharge, 36-15) //n5 -> charge, missing this one
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 55-21) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 34-21)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 33-21)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 32-21)   //charge -> swap
+
+	// c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 55-21) //burst -> n1
+	// c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 34-21)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 100-95) //burst -> burst
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 100-95) //burst -> burst
+	// c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 32-21) //burst -> swap
+
+	//skill press frames, dmg at 14
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 85-14)     //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 85-14)      //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 85-14)       //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 85-14)       //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 80-14)       //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionHighPlunge, 27-14) //58 frames before you can start plunge
+
+	//skill hold frames
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionAttack, 177-34)    //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionBurst, 177-34)     //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionDash, 177-34)      //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionJump, 177-34)      //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionSwap, 177-34)      //85 frames to float down
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionHighPlunge, 58-34) //58 frames before you can start plunge
+
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	//provide override for skill and plunge cause it depends on hold vs press
+	switch next {
+	case core.ActionSkill:
+		//have to check for press vs hold
+		h := p["hold"]
+		if h > 0 {
+			return c.Tmpl.ActionInterruptableDelay(core.ActionSkillHoldFramesOnly, p)
+		}
+		return c.Tmpl.ActionInterruptableDelay(next, p)
+	case core.ActionHighPlunge:
+		//depends on if it was hold or press before so this has to be custom
+		prev := c.Core.LastAction
+		if prev.Typ != core.ActionSkill {
+			//this should not happen
+			c.Core.Log.NewEvent("ERROR: plunge used without skill use on kazuha!!", core.LogActionEvent, c.Index)
+			return 0
+		}
+		//check if hold
+		h := prev.Param["hold"]
+		if h > 0 {
+			return plungeHoldSkillFrames(next)
+		}
+		return plungePressSkillFrames(next)
+	default:
+		return c.Tmpl.ActionInterruptableDelay(next, p)
+	}
+}
+
+func plungePressSkillFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 55 - 36
+	case core.ActionBurst:
+		return 55 - 36
+	default:
+		return 47 - 36
+	}
+}
+
+func plungeHoldSkillFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 60 - 42
+	case core.ActionBurst:
+		return 60 - 42
+	default:
+		return 50 - 42
+	}
+}
diff --git a/internal/characters/kazuha/kazuha.go b/internal/characters/kazuha/kazuha.go
index 0f749eb46..eb0273653 100644
--- a/internal/characters/kazuha/kazuha.go
+++ b/internal/characters/kazuha/kazuha.go
@@ -25,7 +25,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Anemo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSword
 	c.BurstCon = 5
@@ -33,11 +38,13 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.NormalHitNum = 5
 	c.CharZone = core.ZoneInazuma
 
+	c.InitCancelFrames()
+
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 	c.a4()
 
 }
diff --git a/internal/characters/keqing/keqing.go b/internal/characters/keqing/keqing.go
index cfbeb6607..d39b1d3a0 100644
--- a/internal/characters/keqing/keqing.go
+++ b/internal/characters/keqing/keqing.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
diff --git a/internal/characters/klee/klee.go b/internal/characters/klee/klee.go
index 5ac1c3b6e..fe7410b1a 100644
--- a/internal/characters/klee/klee.go
+++ b/internal/characters/klee/klee.go
@@ -26,7 +26,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 3
diff --git a/internal/characters/kokomi/abil.go b/internal/characters/kokomi/abil.go
index 87310c8f7..5fd5a5fea 100644
--- a/internal/characters/kokomi/abil.go
+++ b/internal/characters/kokomi/abil.go
@@ -103,7 +103,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	c.AddTask(c.skillTickTask(d, c.Core.F), "kokomi-e-ticks", 120)
 
-	c.skillLastUsed = c.Core.F - 1
+	c.skillLastUsed = c.Core.F
 	c.SetCD(core.ActionSkill, 20*60)
 
 	return f, a
@@ -136,8 +136,6 @@ func (c *char) createSkillSnapshot() *core.AttackEvent {
 // Helper function that handles damage, healing, and particle components of every tick of her E
 func (c *char) skillTick(d *core.AttackEvent) {
 
-	hpplus := 1 + c.Stat(core.Heal)
-
 	// check if skill has burst bonus snapshot
 	// max swap frame should be 40 frame before 2nd tick
 	if c.swapEarlyF > c.skillLastUsed && c.swapEarlyF < (c.skillLastUsed+120-40) {
@@ -147,7 +145,13 @@ func (c *char) skillTick(d *core.AttackEvent) {
 	}
 
 	c.Core.Combat.QueueAttackEvent(d, 0)
-	c.Core.Health.HealActive(c.Index, (skillHealPct[c.TalentLvlSkill()]*c.HPMax+skillHealFlat[c.TalentLvlSkill()])*hpplus)
+	c.Core.Health.Heal(core.HealInfo{
+		Caller:  c.Index,
+		Target:  c.Core.ActiveChar,
+		Message: "Bake-Kurage",
+		Src:     skillHealPct[c.TalentLvlSkill()]*c.HPMax + skillHealFlat[c.TalentLvlSkill()],
+		Bonus:   d.Snapshot.Stats[core.Heal],
+	})
 
 	// Particles are 0~1 (1:2) on every damage instance
 	if c.Core.Rand.Float64() < .6667 {
@@ -160,7 +164,13 @@ func (c *char) skillTick(d *core.AttackEvent) {
 	if c.Base.Cons >= 2 {
 		active := c.Core.Chars[c.Core.ActiveChar]
 		if active.HP()/active.MaxHP() <= .5 {
-			c.Core.Health.HealActive(c.Index, 0.045*c.HPMax*hpplus)
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  c.Core.ActiveChar,
+				Message: "The Clouds Like Waves Rippling",
+				Src:     0.045 * c.HPMax,
+				Bonus:   c.Stat(core.Heal),
+			})
 		}
 	}
 }
diff --git a/internal/characters/kokomi/kokomi.go b/internal/characters/kokomi/kokomi.go
index b27d60432..603b038d2 100644
--- a/internal/characters/kokomi/kokomi.go
+++ b/internal/characters/kokomi/kokomi.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Hydro
-	c.Energy = 70
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 70
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 70
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 3
@@ -97,8 +102,13 @@ func (c *char) burstActiveHook() {
 			return false
 		}
 
-		hpplus := 1 + c.Stat(core.Heal)
-		c.Core.Health.HealAll(c.Index, (burstHealPct[c.TalentLvlBurst()]*c.HPMax+burstHealFlat[c.TalentLvlBurst()])*hpplus)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  -1,
+			Message: "Ceremonial Garment",
+			Src:     burstHealPct[c.TalentLvlBurst()]*c.HPMax + burstHealFlat[c.TalentLvlBurst()],
+			Bonus:   c.Stat(core.Heal),
+		})
 
 		// C2 handling
 		// Sangonomiya Kokomi gains the following Healing Bonuses with regard to characters with 50% or less HP via the following methods:
@@ -108,7 +118,13 @@ func (c *char) burstActiveHook() {
 				if char.HP()/char.MaxHP() > .5 {
 					continue
 				}
-				c.Core.Health.HealIndex(c.Index, i, 0.006*c.HPMax*hpplus)
+				c.Core.Health.Heal(core.HealInfo{
+					Caller:  c.Index,
+					Target:  i,
+					Message: "The Clouds Like Waves Rippling",
+					Src:     0.006 * c.HPMax,
+					Bonus:   c.Stat(core.Heal),
+				})
 			}
 		}
 
diff --git a/internal/characters/lisa/lisa.go b/internal/characters/lisa/lisa.go
index 81e93ebc3..a903ce10c 100644
--- a/internal/characters/lisa/lisa.go
+++ b/internal/characters/lisa/lisa.go
@@ -22,7 +22,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 4
diff --git a/internal/characters/mona/mona.go b/internal/characters/mona/mona.go
index 9d0e8b66a..4c4ccd0df 100644
--- a/internal/characters/mona/mona.go
+++ b/internal/characters/mona/mona.go
@@ -28,7 +28,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Hydro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 4
@@ -56,8 +61,8 @@ func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 	//add damage mod for omen
 	//add E hook
 	val := make([]float64, core.EndStatType)
diff --git a/internal/characters/ningguang/ningguang.go b/internal/characters/ningguang/ningguang.go
index 5b357e561..36130f4ab 100644
--- a/internal/characters/ningguang/ningguang.go
+++ b/internal/characters/ningguang/ningguang.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 1
diff --git a/internal/characters/noelle/abil.go b/internal/characters/noelle/abil.go
index f837c01e5..1c235b961 100644
--- a/internal/characters/noelle/abil.go
+++ b/internal/characters/noelle/abil.go
@@ -41,8 +41,14 @@ func (c *char) Attack(p map[string]int) (int, int) {
 			if c.Core.Rand.Float64() < prob {
 				//heal target
 				x := a.AttackEvent.Snapshot.BaseDef*(1+a.AttackEvent.Snapshot.Stats[core.DEFP]) + a.AttackEvent.Snapshot.Stats[core.DEF]
-				heal := (shieldHeal[c.TalentLvlSkill()]*x + shieldHealFlat[c.TalentLvlSkill()]) * (1 + a.AttackEvent.Snapshot.Stats[core.Heal])
-				c.Core.Health.HealAll(c.Index, heal)
+				heal := shieldHeal[c.TalentLvlSkill()]*x + shieldHealFlat[c.TalentLvlSkill()]
+				c.Core.Health.Heal(core.HealInfo{
+					Caller:  c.Index,
+					Target:  -1,
+					Message: "Breastplate (Attack)",
+					Src:     heal,
+					Bonus:   a.AttackEvent.Snapshot.Stats[core.Heal],
+				})
 				done = true
 			}
 		}
diff --git a/internal/characters/noelle/noelle.go b/internal/characters/noelle/noelle.go
index 031db0bd3..dae5f103d 100644
--- a/internal/characters/noelle/noelle.go
+++ b/internal/characters/noelle/noelle.go
@@ -24,7 +24,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 4
@@ -40,8 +45,6 @@ a2: shielding if fall below hp threshold, not implemented
 
 a4: every 4 hit decrease breastplate cd by 1; implement as hook
 
-c1: 100% healing, not implemented
-
 c2: decrease stam consumption, to be implemented
 
 c4: explodes for 400% when expired or destroyed; how to implement expired?
diff --git a/internal/characters/qiqi/abil.go b/internal/characters/qiqi/abil.go
index ecc75b2c8..c67ddc2f6 100644
--- a/internal/characters/qiqi/abil.go
+++ b/internal/characters/qiqi/abil.go
@@ -81,7 +81,13 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		snap := c.Snapshot(&ai)
 
 		// One healing proc happens immediately on cast
-		c.Core.Health.HealActive(c.Index, c.healSnapshot(&snap, skillHealContPer, skillHealContFlat, c.TalentLvlSkill()))
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  c.Core.ActiveChar,
+			Message: "Herald of Frost (Tick)",
+			Src:     c.healSnapshot(&snap, skillHealContPer, skillHealContFlat, c.TalentLvlSkill()),
+			Bonus:   snap.Stats[core.Heal],
+		})
 
 		// Healing and damage instances are snapshot
 		// Separately cloned snapshots are fed into each function to ensure nothing interferes with each other
@@ -176,7 +182,13 @@ func (c *char) skillHealTickTask(src int) func() {
 			return
 		}
 
-		c.Core.Health.HealActive(c.Index, c.healSnapshot(&c.skillHealSnapshot, skillHealContPer, skillHealContFlat, c.TalentLvlSkill()))
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  c.Core.ActiveChar,
+			Message: "Herald of Frost (Tick)",
+			Src:     c.healSnapshot(&c.skillHealSnapshot, skillHealContPer, skillHealContFlat, c.TalentLvlSkill()),
+			Bonus:   c.skillHealSnapshot.Stats[core.Heal],
+		})
 
 		// Queue next instance
 		c.AddTask(c.skillHealTickTask(src), "qiqi-skill-heal-tick", 4.5*60)
@@ -210,13 +222,13 @@ func (c *char) Burst(p map[string]int) (int, int) {
 // Helper function to calculate healing amount dynamically using current character stats, which has all mods applied
 func (c *char) healDynamic(healScalePer []float64, healScaleFlat []float64, talentLevel int) float64 {
 	atk := c.Base.Atk + c.Weapon.Atk*(1+c.Stat(core.ATKP)) + c.Stat(core.ATK)
-	heal := (healScaleFlat[talentLevel] + atk*healScalePer[talentLevel]) * (1 + c.Stat(core.Heal))
+	heal := healScaleFlat[talentLevel] + atk*healScalePer[talentLevel]
 	return heal
 }
 
 // Helper function to calculate healing amount from a snapshot instance
 func (c *char) healSnapshot(d *core.Snapshot, healScalePer []float64, healScaleFlat []float64, talentLevel int) float64 {
 	atk := d.BaseAtk*(1+d.Stats[core.ATKP]) + d.Stats[core.ATK]
-	heal := (healScaleFlat[talentLevel] + atk*healScalePer[talentLevel]) * (1 + d.Stats[core.Heal])
+	heal := healScaleFlat[talentLevel] + atk*healScalePer[talentLevel]
 	return heal
 }
diff --git a/internal/characters/qiqi/qiqi.go b/internal/characters/qiqi/qiqi.go
index 0fb8b12ca..6a1739ef4 100644
--- a/internal/characters/qiqi/qiqi.go
+++ b/internal/characters/qiqi/qiqi.go
@@ -33,7 +33,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
@@ -54,8 +59,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 }
 
 // Ensures the set of targets are initialized properly
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 
 	// c.talismanExpiry = make([]int, len(c.Core.Targets))
 	// c.talismanICDExpiry = make([]int, len(c.Core.Targets))
@@ -95,19 +100,25 @@ func (c *char) talismanHealHook() {
 		atk := args[1].(*core.AttackEvent)
 
 		healAmt := c.healDynamic(burstHealPer, burstHealFlat, c.TalentLvlBurst())
-		c.Core.Health.HealIndex(c.Index, atk.Info.ActorIndex, healAmt)
+		c.Core.Health.Heal(core.HealInfo{
+			Caller:  c.Index,
+			Target:  atk.Info.ActorIndex,
+			Message: "Fortune-Preserving Talisman",
+			Src:     healAmt,
+			Bonus:   c.Stat(core.Heal),
+		})
 		t.SetTag(talismanICDKey, c.Core.F+60)
 
-		c.Core.Log.NewEvent(
-			"Qiqi Talisman Healing",
-			core.LogCharacterEvent,
-			c.Index,
-			"target", t.Index(),
-			"healed_char", atk.Info.ActorIndex,
-			"talisman_expiry", t.GetTag(talismanKey),
-			"talisman_healing_icd", t.GetTag(talismanICDKey),
-			"healed_amt", healAmt,
-		)
+		// c.Core.Log.NewEvent(
+		// 	"Qiqi Talisman Healing",
+		// 	core.LogCharacterEvent,
+		// 	c.Index,
+		// 	"target", t.Index(),
+		// 	"healed_char", atk.Info.ActorIndex,
+		// 	"talisman_expiry", t.GetTag(talismanKey),
+		// 	"talisman_healing_icd", t.GetTag(talismanICDKey),
+		// 	"healed_amt", healAmt,
+		// )
 
 		return false
 	}, "talisman-heal-hook")
@@ -157,7 +168,13 @@ func (c *char) onNACAHitHook() {
 
 		// Qiqi NA/CA healing proc in skill duration
 		if c.Core.Status.Duration("qiqiskill") > 0 {
-			c.Core.Health.HealAll(c.Index, c.healSnapshot(&c.skillHealSnapshot, skillHealOnHitPer, skillHealOnHitFlat, c.TalentLvlSkill()))
+			c.Core.Health.Heal(core.HealInfo{
+				Caller:  c.Index,
+				Target:  -1,
+				Message: "Herald of Frost (Attack)",
+				Src:     c.healSnapshot(&c.skillHealSnapshot, skillHealOnHitPer, skillHealOnHitFlat, c.TalentLvlSkill()),
+				Bonus:   c.skillHealSnapshot.Stats[core.Heal],
+			})
 		}
 
 		return false
diff --git a/internal/characters/raiden/raiden.go b/internal/characters/raiden/raiden.go
index ae3a8d192..c7c149fc6 100644
--- a/internal/characters/raiden/raiden.go
+++ b/internal/characters/raiden/raiden.go
@@ -28,7 +28,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 90
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 90
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 90
 	c.Weapon.Class = core.WeaponClassSpear
 	c.BurstCon = 3
@@ -47,10 +52,6 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
-}
-
 func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	switch a {
 	case core.ActionDash:
diff --git a/internal/characters/rosaria/rosaria.go b/internal/characters/rosaria/rosaria.go
index 26d9eaeaf..35239528f 100644
--- a/internal/characters/rosaria/rosaria.go
+++ b/internal/characters/rosaria/rosaria.go
@@ -21,7 +21,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Cryo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSpear
 	c.BurstCon = 5
diff --git a/internal/characters/sara/sara.go b/internal/characters/sara/sara.go
index 1d43d367e..58b945a4b 100644
--- a/internal/characters/sara/sara.go
+++ b/internal/characters/sara/sara.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 5
@@ -33,10 +38,6 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
-}
-
 func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	switch a {
 	case core.ActionDash:
diff --git a/internal/characters/shenhe/shenhe.go b/internal/characters/shenhe/shenhe.go
index 99ebd3467..0959d5889 100644
--- a/internal/characters/shenhe/shenhe.go
+++ b/internal/characters/shenhe/shenhe.go
@@ -39,7 +39,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 		return nil, err
 	}
 	c.Tmpl = t
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSpear
 	c.NormalHitNum = 5
@@ -78,8 +83,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 	// if c.Base.Cons >= 6 {
 	// 	c.c6()
 	// }
diff --git a/internal/characters/sucrose/sucrose.go b/internal/characters/sucrose/sucrose.go
index b39c18653..346a44d4f 100644
--- a/internal/characters/sucrose/sucrose.go
+++ b/internal/characters/sucrose/sucrose.go
@@ -31,7 +31,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Anemo
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 4
@@ -46,8 +51,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 	c.a2()
 	c.a4()
 }
diff --git a/internal/characters/tartaglia/callbacks.go b/internal/characters/tartaglia/callbacks.go
index 55b73818d..46ed66ad3 100644
--- a/internal/characters/tartaglia/callbacks.go
+++ b/internal/characters/tartaglia/callbacks.go
@@ -1,30 +1,20 @@
 package tartaglia
 
-import "github.com/genshinsim/gcsim/pkg/core"
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+)
 
 //While aiming, the power of Hydro will accumulate on the arrowhead.
 //A arrow fully charged with the torrent will deal Hydro DMG and apply the Riptide status.
 func (c *char) aimedApplyRiptide(a core.AttackCB) {
-	a.Target.SetTag(riptideKey, c.Core.F+riptideDuration)
-	c.Core.Log.NewEvent(
-		"riptide applied (CA)",
-		core.LogCharacterEvent,
-		c.Index,
-		"target", a.Target.Index(),
-		"expiry", c.Core.F+riptideDuration,
-	)
+	c.applyRiptide("aimed shot", a.Target)
 }
 
 //Swiftly fires a Hydro-imbued magic arrow, dealing AoE Hydro DMG and applying the Riptide status.
 func (c *char) rangedBurstApplyRiptide(a core.AttackCB) {
-	a.Target.SetTag(riptideKey, c.Core.F+riptideDuration)
-	c.Core.Log.NewEvent(
-		"riptide applied (ranged burst)",
-		core.LogCharacterEvent,
-		c.Index,
-		"target", a.Target.Index(),
-		"expiry", c.Core.F+riptideDuration,
-	)
+	c.applyRiptide("ranged burst", a.Target)
 }
 
 //When Tartaglia is in Foul Legacy: Raging Tide's Melee Stance, on dealing a CRIT hit,
@@ -32,17 +22,25 @@ func (c *char) rangedBurstApplyRiptide(a core.AttackCB) {
 func (c *char) meleeApplyRiptide(a core.AttackCB) {
 	//only applies if is crit
 	if a.IsCrit {
-		a.Target.SetTag(riptideKey, c.Core.F+riptideDuration)
-		c.Core.Log.NewEvent(
-			"riptide applied (melee)",
-			core.LogCharacterEvent,
-			c.Index,
-			"target", a.Target.Index(),
-			"expiry", c.Core.F+riptideDuration,
-		)
+		c.applyRiptide("melee", a.Target)
 	}
 }
 
+func (c *char) applyRiptide(src string, t core.Target) {
+	if c.Base.Cons >= 4 && t.GetTag(riptideKey) < c.Core.F {
+		c.AddTask(func() { c.c4(t) }, "tartaglia-c4", 60*4)
+	}
+
+	t.SetTag(riptideKey, c.Core.F+riptideDuration)
+	c.Core.Log.NewEvent(
+		fmt.Sprintf("riptide applied (%v)", src),
+		core.LogCharacterEvent,
+		c.Index,
+		"target", t.Index(),
+		"expiry", c.Core.F+riptideDuration,
+	)
+}
+
 // Riptide Flash: A fully-charged Aimed Shot that hits an opponent affected
 // by Riptide deals consecutive bouts of AoE DMG. Can occur once every 0.7s.
 func (c *char) rtFlashCallback(a core.AttackCB) {
@@ -56,6 +54,11 @@ func (c *char) rtFlashCallback(a core.AttackCB) {
 	}
 	//add 0.7s icd
 	a.Target.SetTag(riptideFlashICDKey, c.Core.F+42)
+
+	c.rtFlashTick(a.Target)
+}
+
+func (c *char) rtFlashTick(t core.Target) {
 	//queue damage
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -66,21 +69,21 @@ func (c *char) rtFlashCallback(a core.AttackCB) {
 		StrikeType: core.StrikeTypeDefault,
 		Element:    core.Hydro,
 		Durability: 25,
-		Mult:       rtFlash[0][c.TalentLvlAttack()],
+		Mult:       rtFlash[c.TalentLvlAttack()],
 	}
-
 	//proc 3 hits
 	for i := 1; i <= 3; i++ {
-		c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), 1, 1)
+		c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(t.Index(), core.TargettableEnemy), 1, 1)
 	}
+
 	c.Core.Log.NewEvent(
 		"riptide flash triggered",
 		core.LogCharacterEvent,
 		c.Index,
 		"dur", c.Core.Status.Duration("tartagliamelee"),
-		"target", a.Target.Index(),
-		"riptide_flash_icd", a.Target.GetTag(riptideFlashICDKey),
-		"riptide_expiry", a.Target.GetTag(riptideKey),
+		"target", t.Index(),
+		"riptide_flash_icd", t.GetTag(riptideFlashICDKey),
+		"riptide_expiry", t.GetTag(riptideKey),
 	)
 
 	//queue particles
@@ -104,6 +107,10 @@ func (c *char) rtSlashCallback(a core.AttackCB) {
 	//add 1.5s icd
 	a.Target.SetTag(riptideSlashICDKey, c.Core.F+90)
 
+	c.rtSlashTick(a.Target)
+}
+
+func (c *char) rtSlashTick(t core.Target) {
 	//trigger attack
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -117,16 +124,17 @@ func (c *char) rtSlashCallback(a core.AttackCB) {
 		Mult:       rtSlash[c.TalentLvlSkill()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 1, 1)
+	x, y := t.Shape().Pos()
+	c.Core.Combat.QueueAttack(ai, core.NewCircleHit(x, y, 2, false, core.TargettableEnemy), 1, 1)
 
 	c.Core.Log.NewEvent(
-		"Riptide Slash ticked",
+		"riptide slash ticked",
 		core.LogCharacterEvent,
 		c.Index,
 		"dur", c.Core.Status.Duration("tartagliamelee"),
-		"target", a.Target.Index(),
-		"riptide_slash_icd", a.Target.GetTag(riptideSlashICDKey),
-		"riptide_expiry", a.Target.GetTag(riptideKey),
+		"target", t.Index(),
+		"riptide_slash_icd", t.GetTag(riptideSlashICDKey),
+		"riptide_expiry", t.GetTag(riptideKey),
 	)
 
 	//queue particle if not on icd
diff --git a/internal/characters/tartaglia/stats.go b/internal/characters/tartaglia/stats.go
index 938e4ac92..09bcdd895 100644
--- a/internal/characters/tartaglia/stats.go
+++ b/internal/characters/tartaglia/stats.go
@@ -122,58 +122,22 @@ var (
 		2.79,
 		2.945,
 	}
-	rtFlash = [][]float64{
-		{
-			0.124,
-			0.1333,
-			0.1426,
-			0.155,
-			0.1643,
-			0.1736,
-			0.186,
-			0.1984,
-			0.2108,
-			0.2232,
-			0.2356,
-			0.248,
-			0.2635,
-			0.279,
-			0.2945,
-		},
-		{
-			0.124,
-			0.1333,
-			0.1426,
-			0.155,
-			0.1643,
-			0.1736,
-			0.186,
-			0.1984,
-			0.2108,
-			0.2232,
-			0.2356,
-			0.248,
-			0.2635,
-			0.279,
-			0.2945,
-		},
-		{
-			0.124,
-			0.1333,
-			0.1426,
-			0.155,
-			0.1643,
-			0.1736,
-			0.186,
-			0.1984,
-			0.2108,
-			0.2232,
-			0.2356,
-			0.248,
-			0.2635,
-			0.279,
-			0.2945,
-		},
+	rtFlash = []float64{
+		0.124,
+		0.1333,
+		0.1426,
+		0.155,
+		0.1643,
+		0.1736,
+		0.186,
+		0.1984,
+		0.2108,
+		0.2232,
+		0.2356,
+		0.248,
+		0.2635,
+		0.279,
+		0.2945,
 	}
 	rtBurst = []float64{
 		0.62,
diff --git a/internal/characters/tartaglia/tartaglia.go b/internal/characters/tartaglia/tartaglia.go
index 8556c8008..ef208ec1c 100644
--- a/internal/characters/tartaglia/tartaglia.go
+++ b/internal/characters/tartaglia/tartaglia.go
@@ -16,10 +16,7 @@ type char struct {
 	*character.Tmpl
 	eCast         int // the frame tartaglia casts E to enter melee stance
 	rtParticleICD int
-	// rtFlashICD    []int
-	// rtSlashICD    []int
-	// rtExpiry      []int
-	mlBurstUsed bool // used for c6
+	mlBurstUsed   bool // used for c6
 }
 
 //constants for tags
@@ -38,7 +35,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Hydro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassBow
 	c.SkillCon = 3
@@ -50,9 +52,6 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 
 	c.rtParticleICD = 0
-	// c.rtFlashICD = make([]int, len(c.Core.Targets))
-	// c.rtSlashICD = make([]int, len(c.Core.Targets))
-	// c.rtExpiry = make([]int, len(c.Core.Targets))
 
 	c.Core.Flags.ChildeActive = true
 	c.onExitField()
@@ -117,39 +116,16 @@ func (c *char) onDefeatTargets() {
 	}, "tartaglia-on-enemy-death")
 }
 
-//apply riptide status to enemy hit
-// func (c *char) applyRT() {
-// 	c.Core.Events.Subscribe(core.OnDamage, func(args ...interface{}) bool {
-// 		atk := args[1].(*core.AttackEvent)
-// 		t := args[0].(core.Target)
-// 		crit := args[3].(bool)
-
-// 		if c.Core.Status.Duration("tartagliamelee") > 0 {
-// 			if atk.Info.AttackTag != core.AttackTagNormal && atk.Info.AttackTag != core.AttackTagExtra {
-// 				return false
-// 			}
-// 			if !crit {
-// 				return false
-// 			}
-
-// 			//dont log if it just refreshes riptide status
-// 			if c.rtExpiry[t.Index()] <= c.Core.F {
-// 				c.Core.Log.NewEvent("Tartaglia applied riptide", core.LogCharacterEvent, c.Index, "target", t.Index(), "rtExpiry", c.Core.F+riptideDuration)
-// 			}
-// 			c.rtExpiry[t.Index()] = c.Core.F + riptideDuration
-// 		} else {
-// 			if atk.Info.AttackTag != core.AttackTagElementalBurst && atk.Info.AttackTag != core.AttackTagExtra {
-// 				return false
-// 			}
-
-// 			//ranged burst or aim mode
-// 			//dont log if it just refreshes riptide status
-// 			if c.rtExpiry[t.Index()] <= c.Core.F {
-// 				c.Core.Log.NewEvent("Tartaglia applied riptide", core.LogCharacterEvent, c.Index, "target", t.Index(), "rtExpiry", c.Core.F+riptideDuration)
-// 			}
-// 			c.rtExpiry[t.Index()] = c.Core.F + riptideDuration
-// 		}
-
-// 		return false
-// 	}, "tartaglia-apply-riptide")
-// }
+func (c *char) c4(t core.Target) {
+	if t.GetTag(riptideKey) < c.Core.F {
+		return
+	}
+
+	if c.Core.Status.Duration("tartagliamelee") > 0 {
+		c.rtSlashTick(t)
+	} else {
+		c.rtFlashTick(t)
+	}
+
+	c.AddTask(func() { c.c4(t) }, "tartaglia-c4", 60*4)
+}
diff --git a/internal/characters/travelerelectro/travelerelectro.go b/internal/characters/travelerelectro/travelerelectro.go
index 46a7d4b94..20ab67c6b 100644
--- a/internal/characters/travelerelectro/travelerelectro.go
+++ b/internal/characters/travelerelectro/travelerelectro.go
@@ -21,7 +21,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSword
 	c.BurstCon = 3
diff --git a/internal/characters/travelergeo/travelergeo.go b/internal/characters/travelergeo/travelergeo.go
index 0b6205a63..6a367bcd0 100644
--- a/internal/characters/travelergeo/travelergeo.go
+++ b/internal/characters/travelergeo/travelergeo.go
@@ -21,7 +21,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSword
 	c.BurstCon = 3
@@ -31,8 +36,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 
 	if c.Base.Cons > 0 {
 		c.c1()
diff --git a/internal/characters/venti/abil.go b/internal/characters/venti/abil.go
index 6831832c9..adf7e65f7 100644
--- a/internal/characters/venti/abil.go
+++ b/internal/characters/venti/abil.go
@@ -2,6 +2,7 @@ package venti
 
 import (
 	"fmt"
+
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
@@ -98,7 +99,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(4, false, core.TargettableEnemy), 0, f-1, cb)
 
-	c.QueueParticle("venti", 4, core.Anemo, f+100)
+	c.QueueParticle("venti", 3, core.Anemo, f+100)
 
 	c.SetCD(core.ActionSkill, cd)
 	return f, a
diff --git a/internal/characters/venti/venti.go b/internal/characters/venti/venti.go
index 0d8bc0c35..122b4337c 100644
--- a/internal/characters/venti/venti.go
+++ b/internal/characters/venti/venti.go
@@ -22,7 +22,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Anemo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 6
diff --git a/internal/characters/xiangling/xiangling.go b/internal/characters/xiangling/xiangling.go
index 539006853..fef577130 100644
--- a/internal/characters/xiangling/xiangling.go
+++ b/internal/characters/xiangling/xiangling.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 		return nil, err
 	}
 	c.Tmpl = t
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSpear
 	c.NormalHitNum = 5
@@ -35,8 +40,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 	if c.Base.Cons >= 6 {
 		c.c6()
 	}
diff --git a/internal/characters/xiao/xiao.go b/internal/characters/xiao/xiao.go
index 427a24d32..efc0a51f0 100644
--- a/internal/characters/xiao/xiao.go
+++ b/internal/characters/xiao/xiao.go
@@ -32,7 +32,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Anemo
-	c.Energy = 70
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 70
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 70
 	c.Weapon.Class = core.WeaponClassSpear
 	c.BurstCon = 5
diff --git a/internal/characters/xingqiu/xingqiu.go b/internal/characters/xingqiu/xingqiu.go
index c48fc3f64..cbf08be84 100644
--- a/internal/characters/xingqiu/xingqiu.go
+++ b/internal/characters/xingqiu/xingqiu.go
@@ -30,7 +30,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Hydro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassSword
 	c.BurstCon = 3
diff --git a/internal/characters/yaemiko/yaemiko.go b/internal/characters/yaemiko/yaemiko.go
index 976d4d42b..fdcd10a8d 100644
--- a/internal/characters/yaemiko/yaemiko.go
+++ b/internal/characters/yaemiko/yaemiko.go
@@ -16,8 +16,6 @@ type char struct {
 	cdQueue                [][]int
 	availableCDCharge      []int
 	additionalCDCharge     []int
-	a2skillTimer           int
-	a2burstTimer           int
 	totemParticleICD       int
 }
 
@@ -34,7 +32,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Electro
-	c.Energy = 90
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 90
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 90
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.NormalHitNum = 3
@@ -56,15 +59,14 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 	c.additionalCDCharge[core.ActionSkill] = 2
 	c.availableCDCharge[core.ActionSkill] += 2
-	c.a2burstTimer = 0
-	c.a2skillTimer = 0
 	c.Tags["eCharge"] = c.availableCDCharge[core.ActionSkill]
+
 	return &c, nil
 }
 
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
-	// c.a2()
+
+func (c *char) Init() {
+	c.Tmpl.Init()
 	c.a4()
 	if c.Base.Cons >= 4 {
 		c.c4()
@@ -82,59 +84,18 @@ func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	}
 }
 
-// func (c *char) a2() {
-// 	//Other nearby party members can decrease the CD of Yae Miko's Yakan Evocation: Sesshou Sakura:
-// 	// • Hitting opponents with Elemental Skill DMG decreases it by 1s and can occur once every 1.8s.
-// 	// • Hitting opponents with Elemental Burst DMG decreases it by 1s and can occur once every 1.8s.
-
-// 	c.Core.Events.Subscribe(core.OnAttackWillLand, func(args ...interface{}) bool {
-// 		atk := args[1].(*core.AttackEvent)
-// 		if c.Index == atk.Info.ActorIndex {
-// 			// do not trigger for yae attacks
-// 			return false
-// 		}
-// 		switch atk.Info.AttackTag {
-// 		case core.AttackTagElementalBurst:
-// 			if c.Core.F < c.a2burstTimer+1.8*60 {
-// 				return false
-// 			} else {
-// 				c.ReduceActionCooldown(core.ActionSkill, 60)
-// 				c.a2burstTimer = c.Core.F
-// 			}
-// 		case core.AttackTagElementalArt:
-// 			if c.Core.F < c.a2skillTimer+1.8*60 {
-// 				return false
-// 			} else {
-// 				c.ReduceActionCooldown(core.ActionSkill, 60)
-// 				c.a2skillTimer = c.Core.F
-// 			}
-// 		case core.AttackTagElementalArtHold:
-// 			if c.Core.F < c.a2skillTimer+1.8*60 {
-// 				return false
-// 			} else {
-// 				c.ReduceActionCooldown(core.ActionSkill, 60)
-// 				c.a2skillTimer = c.Core.F
-// 			}
-// 		default:
-// 			return false
-// 		}
-// 		return false
-// 	}, "yaemiko-a2")
-
-// }
-
 func (c *char) a4() {
+	m := make([]float64, core.EndStatType)
 	c.AddPreDamageMod(core.PreDamageMod{
-		Expiry: -1,
 		Key:    "yaemiko-a2",
+		Expiry: -1,
 		Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+			// only trigger on elemental art damage
 			if atk.Info.AttackTag != core.AttackTagElementalArt {
-				// only trigger on elemental art damage
 				return nil, false
 			}
-			val := make([]float64, core.EndStatType)
-			val[core.DmgP] = c.Stats[core.EM] * 0.0015
-			return val, true
+			m[core.DmgP] = c.Stat(core.EM) * 0.0015
+			return m, true
 		},
 	})
 }
diff --git a/internal/characters/yanfei/yanfei.go b/internal/characters/yanfei/yanfei.go
index 87a72aedf..65ca20b02 100644
--- a/internal/characters/yanfei/yanfei.go
+++ b/internal/characters/yanfei/yanfei.go
@@ -25,7 +25,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 80
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 80
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 80
 	c.Weapon.Class = core.WeaponClassCatalyst
 	c.BurstCon = 5
diff --git a/internal/characters/yoimiya/yoimiya.go b/internal/characters/yoimiya/yoimiya.go
index 1372436f3..302377dc0 100644
--- a/internal/characters/yoimiya/yoimiya.go
+++ b/internal/characters/yoimiya/yoimiya.go
@@ -23,7 +23,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Pyro
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSword
 	c.NormalHitNum = 5
diff --git a/internal/characters/yunjin/yunjin.go b/internal/characters/yunjin/yunjin.go
index 116a7b059..f6bd99bbe 100644
--- a/internal/characters/yunjin/yunjin.go
+++ b/internal/characters/yunjin/yunjin.go
@@ -26,7 +26,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 60
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 60
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 60
 	c.Weapon.Class = core.WeaponClassSpear
 	c.NormalHitNum = 5
@@ -47,8 +52,8 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 }
 
 // Occurs after all characters are loaded, so getPartyElementalTypeCounts works properly
-func (c *char) Init(index int) {
-	c.Tmpl.Init(index)
+func (c *char) Init() {
+	c.Tmpl.Init()
 
 	c.partyElementalTypes = 0
 	c.getPartyElementalTypeCounts()
diff --git a/internal/characters/zhongli/shield.go b/internal/characters/zhongli/shield.go
index 4f96838c2..fcef2c82d 100644
--- a/internal/characters/zhongli/shield.go
+++ b/internal/characters/zhongli/shield.go
@@ -77,14 +77,18 @@ func (s *shd) OnDamage(dmg float64, ele core.EleType, bonus float64) (float64, b
 		//40% of dmg is converted into healing, but cannot exceed 8% of each char max hp
 		//so we have to go through each char one at a time....
 
-		for i, c := range s.c.Core.Chars {
-			heal := 0.4 * dmg
-			if heal > 0.08*c.MaxHP() {
-				heal = 0.08 * c.MaxHP()
-			}
-			s.c.Core.Health.HealIndex(s.c.Index, i, heal)
-			s.c.Core.Log.NewEvent("zhongli c6 healing char", core.LogHurtEvent, c.CharIndex(), "amount", heal, "final", c.HP())
+		c := s.c.Core.Chars[s.c.Core.ActiveChar]
+		heal := 0.4 * dmg
+		if heal > 0.08*c.MaxHP() {
+			heal = 0.08 * c.MaxHP()
 		}
+		s.c.Core.Health.Heal(core.HealInfo{
+			Caller:  s.c.Index,
+			Target:  s.c.Core.ActiveChar,
+			Message: "Chrysos, Bounty of Dominator",
+			Src:     heal,
+			Bonus:   c.Stat(core.Heal),
+		})
 	}
 	if !ok {
 		s.c.removeJadeShield()
diff --git a/internal/characters/zhongli/zhongli.go b/internal/characters/zhongli/zhongli.go
index ad450ca13..fd2cc5022 100644
--- a/internal/characters/zhongli/zhongli.go
+++ b/internal/characters/zhongli/zhongli.go
@@ -28,7 +28,12 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Tmpl = t
 	c.Base.Element = core.Geo
-	c.Energy = 40
+
+	e, ok := p.Params["start_energy"]
+	if !ok {
+		e = 40
+	}
+	c.Energy = float64(e)
 	c.EnergyMax = 40
 	c.Weapon.Class = core.WeaponClassSpear
 	c.BurstCon = 5
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
 }
diff --git a/internal/substatoptimizer/substatoptimizer.go b/internal/substatoptimizer/substatoptimizer.go
index 2af020b84..cffd13fe3 100644
--- a/internal/substatoptimizer/substatoptimizer.go
+++ b/internal/substatoptimizer/substatoptimizer.go
@@ -34,7 +34,9 @@ var (
 
 	// TODO: Will need to update this once artifact keys are introduced, and if more 4* artifact sets are implemented
 	artifactSets4Star = []string{
+		"exile",
 		"instructor",
+		"theexile",
 	}
 
 	substatValues  = make([]float64, core.EndStatType)
diff --git a/internal/tmpl/action/action.go b/internal/tmpl/action/action.go
index 94b971001..643855112 100644
--- a/internal/tmpl/action/action.go
+++ b/internal/tmpl/action/action.go
@@ -228,7 +228,7 @@ func (a *Ctrl) execAction(n *core.ActionItem) (int, bool, error) {
 		"swap_cd_post", a.core.SwapCD,
 		"stam_post", a.core.Stam,
 		"animation", f,
-	)
+	).SetEnded(a.core.F + f)
 
 	a.core.LastAction = *n
 
diff --git a/internal/tmpl/character/actions.go b/internal/tmpl/character/actions.go
index c89991e4c..8e49baa80 100644
--- a/internal/tmpl/character/actions.go
+++ b/internal/tmpl/character/actions.go
@@ -46,11 +46,6 @@ func (c *Tmpl) ActionStam(a core.ActionType, p map[string]int) float64 {
 	}
 }
 
-func (c *Tmpl) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
-	c.Core.Log.NewEvent("ActionFrames not implemented", core.LogActionEvent, c.Index)
-	return 0, 0
-}
-
 func (c *Tmpl) ActionReady(a core.ActionType, p map[string]int) bool {
 	switch a {
 	case core.ActionBurst:
@@ -65,10 +60,6 @@ func (c *Tmpl) ActionReady(a core.ActionType, p map[string]int) bool {
 	return true
 }
 
-func (c *Tmpl) ActionInterruptableDelay(next core.ActionType) int {
-	return 0
-}
-
 func (c *Tmpl) AddCDAdjustFunc(rd core.CDAdjust) {
 	ind := -1
 	for i, v := range c.CDReductionFuncs {
diff --git a/internal/tmpl/character/frames.go b/internal/tmpl/character/frames.go
new file mode 100644
index 000000000..5102dc6dd
--- /dev/null
+++ b/internal/tmpl/character/frames.go
@@ -0,0 +1,52 @@
+package character
+
+import "github.com/genshinsim/gcsim/pkg/core"
+
+func (t *Tmpl) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
+	t.Core.Log.NewEvent("ActionFrames not implemented", core.LogActionEvent, t.Index)
+	return 0, 0
+}
+
+func (t *Tmpl) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	switch t.Core.LastAction.Typ {
+	case core.ActionSwap:
+		//if not same character then there should be no delay
+		return 0
+	case core.ActionAttack:
+		//check our hit counter; should be hit counter - 1
+		lastHit := t.NormalCounter - 1
+		if lastHit < 0 {
+			lastHit = t.NormalHitNum - 1
+		}
+		n, ok := t.normalCancelFrames[lastHit]
+		if !ok {
+			//sanity check just in case no frames are set
+			return 0
+		}
+		return n[next]
+	default:
+		n, ok := t.cancelFrames[t.Core.LastAction.Typ]
+		if !ok {
+			return 0
+		}
+		return n[next]
+	}
+}
+
+func (t *Tmpl) SetNormalCancelFrames(normalHitNum int, nextAbil core.ActionType, frames int) {
+
+	if _, ok := t.normalCancelFrames[normalHitNum]; !ok {
+		t.normalCancelFrames[normalHitNum] = make(map[core.ActionType]int)
+	}
+
+	t.normalCancelFrames[normalHitNum][nextAbil] = frames
+}
+
+func (t *Tmpl) SetAbilCancelFrames(prevAbil core.ActionType, nextAbil core.ActionType, frames int) {
+
+	if _, ok := t.cancelFrames[prevAbil]; !ok {
+		t.cancelFrames[prevAbil] = make(map[core.ActionType]int)
+	}
+
+	t.cancelFrames[prevAbil][nextAbil] = frames
+}
diff --git a/internal/tmpl/character/tmpl.go b/internal/tmpl/character/tmpl.go
index 09fb262f2..1df00e6a3 100644
--- a/internal/tmpl/character/tmpl.go
+++ b/internal/tmpl/character/tmpl.go
@@ -41,49 +41,59 @@ type Tmpl struct {
 	NormalHitNum  int //how many hits in a normal combo
 	NormalCounter int
 
+	//map to track frames
+	normalCancelFrames map[int]map[core.ActionType]int             //this maps normal hit number into
+	cancelFrames       map[core.ActionType]map[core.ActionType]int //this maps all other skills
+
 	//infusion
 	Infusion core.WeaponInfusion //TODO currently just overides the old; disregarding any existing
 }
 
 func NewTemplateChar(x *core.Core, p core.CharacterProfile) (*Tmpl, error) {
-	c := Tmpl{}
-	c.Core = x
-	c.Rand = x.Rand
-
-	c.ActionCD = make([]int, core.EndActionType)
-	c.Mods = make([]core.CharStatMod, 0, 10)
-	c.Tags = make(map[string]int)
-	c.CDReductionFuncs = make([]core.CDAdjust, 0, 5)
-	c.Base = p.Base
-	c.Weapon = p.Weapon
-	c.Talents = p.Talents
-	c.SkillCon = 3
-	c.BurstCon = 5
-	if c.Talents.Attack < 1 || c.Talents.Attack > 15 {
-		return nil, fmt.Errorf("invalid talent lvl: attack - %v", c.Talents.Attack)
+	t := Tmpl{}
+	t.Core = x
+	t.Rand = x.Rand
+
+	t.ActionCD = make([]int, core.EndActionType)
+	t.Mods = make([]core.CharStatMod, 0, 10)
+	t.Tags = make(map[string]int)
+	t.CDReductionFuncs = make([]core.CDAdjust, 0, 5)
+	t.Base = p.Base
+	t.Weapon = p.Weapon
+	t.Talents = p.Talents
+	t.SkillCon = 3
+	t.BurstCon = 5
+	if t.Talents.Attack < 1 || t.Talents.Attack > 15 {
+		return nil, fmt.Errorf("invalid talent lvl: attack - %v", t.Talents.Attack)
 	}
-	if c.Talents.Attack < 1 || c.Talents.Attack > 12 {
-		return nil, fmt.Errorf("invalid talent lvl: skill - %v", c.Talents.Skill)
+	if t.Talents.Attack < 1 || t.Talents.Attack > 12 {
+		return nil, fmt.Errorf("invalid talent lvl: skill - %v", t.Talents.Skill)
 	}
-	if c.Talents.Attack < 1 || c.Talents.Attack > 12 {
-		return nil, fmt.Errorf("invalid talent lvl: burst - %v", c.Talents.Burst)
+	if t.Talents.Attack < 1 || t.Talents.Attack > 12 {
+		return nil, fmt.Errorf("invalid talent lvl: burst - %v", t.Talents.Burst)
 	}
 	for i, v := range p.Stats {
-		c.Stats[i] = v
+		t.Stats[i] = v
 	}
 	if p.Base.StartHP > -1 {
-		c.Core.Log.NewEvent("setting starting hp", core.LogCharacterEvent, c.Index, "character", p.Base.Key.String(), "hp", p.Base.StartHP)
-		c.HPCurrent = p.Base.StartHP
+		t.Core.Log.NewEvent("setting starting hp", core.LogCharacterEvent, t.Index, "character", p.Base.Key.String(), "hp", p.Base.StartHP)
+		t.HPCurrent = p.Base.StartHP
 	} else {
-		c.HPCurrent = math.MaxInt64
+		t.HPCurrent = math.MaxInt64
 	}
 
-	return &c, nil
+	t.normalCancelFrames = make(map[int]map[core.ActionType]int)
+	t.cancelFrames = make(map[core.ActionType]map[core.ActionType]int)
+
+	return &t, nil
 }
 
-// Character initialization function. Occurs AFTER all char/weapons are initially loaded
-func (t *Tmpl) Init(index int) {
+func (t *Tmpl) SetIndex(index int) {
 	t.Index = index
+}
+
+// Character initialization function. Occurs AFTER all char/weapons are initially loaded
+func (t *Tmpl) Init() {
 	hpp := t.Stats[core.HPP]
 	hp := t.Stats[core.HP]
 
@@ -225,6 +235,17 @@ func (c *Tmpl) ReactBonusModIsActive(key string) bool {
 	return true
 }
 
+func (c *Tmpl) WeaponInfuseIsActive(key string) bool {
+	if c.Infusion.Key != key {
+		return false
+	}
+	//check expiry
+	if c.Infusion.Expiry < c.Core.F && c.Infusion.Expiry > -1 {
+		return false
+	}
+	return true
+}
+
 func (t *Tmpl) AddReactBonusMod(mod core.ReactionBonusMod) {
 	ind := -1
 	for i, v := range t.ReactMod {
diff --git a/internal/tmpl/construct/construct.go b/internal/tmpl/construct/construct.go
index b38399c58..fe7f674d0 100644
--- a/internal/tmpl/construct/construct.go
+++ b/internal/tmpl/construct/construct.go
@@ -1,6 +1,8 @@
 package construct
 
-import "github.com/genshinsim/gcsim/pkg/core"
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
 
 type ConstructCtrl struct {
 	constructs  []core.Construct
@@ -170,6 +172,36 @@ func (s *ConstructCtrl) Has(key int) bool {
 	return false
 }
 
+func (s *ConstructCtrl) Expiry(t core.GeoConstructType) int {
+	expiry := -1
+	for _, v := range s.constructs {
+		if v.Type() == t {
+			if expiry == -1 {
+				expiry = v.Expiry()
+			} else if expiry > v.Expiry() {
+				expiry = v.Expiry()
+			}
+		}
+	}
+	for _, v := range s.consNoLimit {
+		if v.Type() == t {
+			if expiry == -1 {
+				expiry = v.Expiry()
+			} else if expiry > v.Expiry() {
+				expiry = v.Expiry()
+			}
+		}
+	}
+
+	expiry = expiry - s.core.F
+
+	if expiry < 0 {
+		return 0
+	}
+
+	return expiry
+}
+
 //destroy key if exist, return true if destroyed
 func (s *ConstructCtrl) Destroy(key int) bool {
 	ok := false
diff --git a/internal/tmpl/health/health.go b/internal/tmpl/health/health.go
index aa9745d84..c1fb618a6 100644
--- a/internal/tmpl/health/health.go
+++ b/internal/tmpl/health/health.go
@@ -16,37 +16,32 @@ func NewCtrl(c *core.Core) *HealthCtrl {
 	}
 }
 
-func (h *HealthCtrl) HealActive(caller int, hp float64) {
-	heal := h.healBonusMult(h.core.ActiveChar) * hp
-	h.core.Chars[h.core.ActiveChar].ModifyHP(heal)
-	h.core.Events.Emit(core.OnHeal, caller, h.core.ActiveChar, heal)
-	h.core.Log.NewEvent("healing", core.LogHealEvent, h.core.ActiveChar, "amount", hp, "bonus", h.healBonusMult(h.core.ActiveChar), "final", h.core.Chars[h.core.ActiveChar].HP())
-}
+func (h *HealthCtrl) HealIndex(hi *core.HealInfo, index int) {
+	c := h.core.Chars[index]
 
-func (h *HealthCtrl) HealAll(caller int, hp float64) {
-	for i, c := range h.core.Chars {
-		heal := h.healBonusMult(i) * hp
-		c.ModifyHP(heal)
-		h.core.Events.Emit(core.OnHeal, caller, i, heal)
-		h.core.Log.NewEvent("healing (all)", core.LogHealEvent, i, "amount", hp, "bonus", h.healBonusMult(i), "final", h.core.Chars[h.core.ActiveChar].HP())
+	bonus := h.healBonusMult(index) + hi.Bonus
+	hp := .0
+	switch hi.Type {
+	case core.HealTypeAbsolute:
+		hp = hi.Src
+	case core.HealTypePercent:
+		hp = c.MaxHP() * hi.Src
 	}
-}
+	heal := hp * bonus
 
-func (h *HealthCtrl) HealAllPercent(caller int, percent float64) {
-	for i, c := range h.core.Chars {
-		hp := c.MaxHP() * percent
-		heal := h.healBonusMult(i) * hp
-		c.ModifyHP(heal)
-		h.core.Events.Emit(core.OnHeal, caller, i, heal)
-		h.core.Log.NewEvent("healing (all)", core.LogHealEvent, i, "amount", hp, "bonus", h.healBonusMult(i), "final", h.core.Chars[h.core.ActiveChar].HP())
-	}
+	c.ModifyHP(heal)
+	h.core.Events.Emit(core.OnHeal, hi.Caller, index, heal)
+	h.core.Log.NewEvent(hi.Message, core.LogHealEvent, index, "amount", hp, "bonus", bonus, "final", c.HP())
 }
 
-func (h *HealthCtrl) HealIndex(caller int, index int, hp float64) {
-	heal := h.healBonusMult(index) * hp
-	h.core.Chars[index].ModifyHP(heal)
-	h.core.Events.Emit(core.OnHeal, caller, index, heal)
-	h.core.Log.NewEvent("healing", core.LogHealEvent, index, "amount", hp, "bonus", h.healBonusMult(index), "final", h.core.Chars[h.core.ActiveChar].HP())
+func (h *HealthCtrl) Heal(hi core.HealInfo) {
+	if hi.Target == -1 { // all
+		for i := range h.core.Chars {
+			h.HealIndex(&hi, i)
+		}
+	} else {
+		h.HealIndex(&hi, hi.Target)
+	}
 }
 
 func (h *HealthCtrl) healBonusMult(healedCharIndex int) float64 {
diff --git a/internal/tmpl/queue/conditions.go b/internal/tmpl/queue/conditions.go
index b00c042e6..002681ad0 100644
--- a/internal/tmpl/queue/conditions.go
+++ b/internal/tmpl/queue/conditions.go
@@ -58,6 +58,10 @@ func (q *Queuer) evalCond(cond core.Condition) (bool, error) {
 		return q.evalAbilReady(cond)
 	case ".mods":
 		return q.evalCharMods(cond)
+	case ".infusion":
+		return q.evalInfusion(cond)
+	case ".construct":
+		return q.evalConstruct(cond)
 	}
 	return false, nil
 }
diff --git a/internal/tmpl/queue/construct.go b/internal/tmpl/queue/construct.go
new file mode 100644
index 000000000..6b20a8b59
--- /dev/null
+++ b/internal/tmpl/queue/construct.go
@@ -0,0 +1,44 @@
+package queue
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func (q *Queuer) evalConstruct(cond core.Condition) (bool, error) {
+	if len(cond.Fields) < 3 {
+		return false, errors.New("eval construct: unexpected short field, expected at least 3")
+	}
+	switch cond.Fields[1] {
+	case ".duration":
+		return q.evalConstructDuration(cond)
+	case ".count":
+		return q.evalConstructCount(cond)
+	default:
+		return false, errors.New("eval construct: invalid field: " + cond.Fields[1])
+	}
+}
+
+func (q *Queuer) evalConstructDuration(cond core.Condition) (bool, error) {
+	//.construct.duration.<name>
+	s := strings.TrimPrefix(cond.Fields[2], ".")
+	key := core.ConstructNameToKey[s]
+
+	//grab construct
+	exp := q.core.Constructs.Expiry(key)
+
+	return compInt(cond.Op, exp, cond.Value), nil
+}
+
+func (q *Queuer) evalConstructCount(cond core.Condition) (bool, error) {
+	//.construct.count.<name>
+	s := strings.TrimPrefix(cond.Fields[2], ".")
+	key := core.ConstructNameToKey[s]
+
+	//grab construct
+	count := q.core.Constructs.CountByType(key)
+
+	return compInt(cond.Op, count, cond.Value), nil
+}
diff --git a/internal/tmpl/queue/infusion.go b/internal/tmpl/queue/infusion.go
new file mode 100644
index 000000000..86e38ad9b
--- /dev/null
+++ b/internal/tmpl/queue/infusion.go
@@ -0,0 +1,28 @@
+package queue
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func (q *Queuer) evalInfusion(cond core.Condition) (bool, error) {
+	if len(cond.Fields) < 3 {
+		return false, errors.New("eval infusion: unexpected short field, expected at least 3")
+	}
+	name := strings.TrimPrefix(cond.Fields[1], ".")
+	key := core.CharNameToKey[name]
+	char, ok := q.core.CharByName(key)
+	if !ok {
+		return false, errors.New("eval infusion: invalid char in condition")
+	}
+
+	active := 0
+	inf := strings.TrimPrefix(cond.Fields[2], ".")
+	if char.WeaponInfuseIsActive(inf) {
+		active = 1
+	}
+
+	return compInt(cond.Op, active, cond.Value), nil
+}
diff --git a/internal/weapons/bow/mitternachtswaltz/mitternachtswaltz.go b/internal/weapons/bow/mitternachtswaltz/mitternachtswaltz.go
new file mode 100644
index 000000000..2ce9e3772
--- /dev/null
+++ b/internal/weapons/bow/mitternachtswaltz/mitternachtswaltz.go
@@ -0,0 +1,81 @@
+package mitternachtswaltz
+
+import (
+	"fmt"
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterWeaponFunc("mitternachtswaltz", weapon)
+	core.RegisterWeaponFunc("mitternachts", weapon)
+	core.RegisterWeaponFunc("mitternacht", weapon)
+}
+
+/*
+ * Normal Attack hits on opponents increase Elemental Skill DMG by 20/25/30/35/40% for 5s.
+ * Elemental Skill hits on opponents increase Normal Attack DMG by 20/25/30/35/40% for 5s.
+ */
+func weapon(char core.Character, c *core.Core, r int, param map[string]int) string {
+	m := make([]float64, core.EndStatType)
+
+	buffAmount := .15 + .05*float64(r) // same amount in either context
+	buffExpiry := 300                  // 5s
+	buffIcd := 0                       // Add a 1-frame ICD to prevent buffs from being applied too quickly for the sim
+
+	c.Events.Subscribe(core.OnDamage, func(args ...interface{}) bool {
+		atk := args[1].(*core.AttackEvent)
+
+		// Attack belongs to the equipped character
+		if atk.Info.ActorIndex != char.CharIndex() {
+			return false
+		}
+
+		// Active character has weapon equipped
+		if c.ActiveChar != char.CharIndex() {
+			return false
+		}
+
+		// Add 1-frame ICD to prevent too many buffs from being applied the sim simultaneously
+		if c.F <= buffIcd {
+			return false
+		}
+
+		buffIcd = c.F + 1
+
+		// only apply elemental skill buff on normal attacks
+		if atk.Info.AttackTag == core.AttackTagNormal {
+			char.AddPreDamageMod(core.PreDamageMod{
+				Key: "mitternachtswaltz-ele",
+				Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+					if (atk.Info.AttackTag == core.AttackTagElementalArt) || (atk.Info.AttackTag == core.AttackTagElementalArtHold) {
+						m[core.DmgP] = buffAmount
+						return m, true
+					}
+
+					return nil, false
+				},
+				Expiry: c.F + buffExpiry,
+			})
+		}
+
+		// only apply normal attack buff on elemental skill
+		if (atk.Info.AttackTag == core.AttackTagElementalArt) || (atk.Info.AttackTag == core.AttackTagElementalArtHold) {
+			char.AddPreDamageMod(core.PreDamageMod{
+				Key: "mitternachtswaltz-na",
+				Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+					if atk.Info.AttackTag == core.AttackTagNormal {
+						m[core.DmgP] = buffAmount
+						return m, true
+					}
+
+					return nil, false
+				},
+				Expiry: c.F + buffExpiry,
+			})
+		}
+
+		return false
+	}, fmt.Sprintf("mitternachtswaltz-%v", char.Name()))
+
+	return "mitternachtswaltz"
+}
diff --git a/internal/weapons/bow/predator/predator.go b/internal/weapons/bow/predator/predator.go
new file mode 100644
index 000000000..298b14155
--- /dev/null
+++ b/internal/weapons/bow/predator/predator.go
@@ -0,0 +1,93 @@
+package predator
+
+import (
+	"fmt"
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterWeaponFunc("predator", weapon)
+}
+
+/*
+ * Dealing Cryo DMG to opponents increases this character's Normal and Charged Attack DMG by 10% for 6s.
+ * This effect can have a maximum of 2 stacks.
+ * Additionally, when Aloy equips Predator, ATK is increased by 66.
+ * (Refines do not change the weapon effect)
+ */
+func weapon(char core.Character, c *core.Core, r int, param map[string]int) string {
+	mATK, mDMG := make([]float64, core.EndStatType), make([]float64, core.EndStatType)
+
+	if char.Key() == core.Aloy {
+		char.AddMod(core.CharStatMod{
+			Key:    "predator",
+			Expiry: -1,
+			Amount: func() ([]float64, bool) {
+				mATK[core.ATK] = 66
+				return mATK, true
+			},
+		})
+	}
+
+	buffDmgP := .10
+
+	stacks := 0
+	stackExpiry := 0
+	maxStacks := 2
+	stackDuration := 360 // 6s
+
+	c.Events.Subscribe(core.OnDamage, func(args ...interface{}) bool {
+		atk := args[1].(*core.AttackEvent)
+
+		// Attack belongs to the equipped character
+		if atk.Info.ActorIndex != char.CharIndex() {
+			return false
+		}
+
+		// Active character has weapon equipped
+		if c.ActiveChar != char.CharIndex() {
+			return false
+		}
+
+		// Only apply when damage element is cryo
+		if atk.Info.Element != core.Cryo {
+			return false
+		}
+
+		// Reset stacks if they've expired
+		if c.F > stackExpiry {
+			stacks = 0
+		}
+
+		// Checks done, proc weapon passive
+		// Increment stack count
+		if stacks < maxStacks {
+			stacks++
+		}
+
+		stackExpiry = c.F + stackDuration
+
+		char.AddPreDamageMod(core.PreDamageMod{
+			Key: "predator",
+			Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+				// Reset stacks if they've expired
+				if c.F > stackExpiry {
+					stacks = 0
+				}
+
+				// Only apply to normal or charged attacks
+				if (atk.Info.AttackTag == core.AttackTagNormal) || (atk.Info.AttackTag == core.AttackTagExtra) {
+					mDMG[core.DmgP] = buffDmgP * float64(stacks)
+					return mDMG, true
+				}
+
+				return nil, false
+			},
+			Expiry: stackExpiry,
+		})
+
+		return false
+	}, fmt.Sprintf("predator-%v", char.Name()))
+
+	return "predator"
+}
diff --git a/internal/weapons/catalyst/prototype/prototype.go b/internal/weapons/catalyst/prototype/prototype.go
index 787adf3bc..fbcbd934b 100644
--- a/internal/weapons/catalyst/prototype/prototype.go
+++ b/internal/weapons/catalyst/prototype/prototype.go
@@ -23,10 +23,16 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 		}
 
 		for i := 120; i <= 360; i += 120 {
-
 			char.AddTask(func() {
 				char.AddEnergy("prototype-amber", e)
-				c.Health.HealAllPercent(char.CharIndex(), e/100.0*(1+char.Stat(core.Heal)))
+				c.Health.Heal(core.HealInfo{
+					Caller:  char.CharIndex(),
+					Target:  -1,
+					Type:    core.HealTypePercent,
+					Message: "Prototype Amber",
+					Src:     e / 100.0,
+					Bonus:   char.Stat(core.Heal),
+				})
 			}, "recharge", i)
 		}
 
diff --git a/internal/weapons/catalyst/thrilling/thrilling.go b/internal/weapons/catalyst/thrilling/thrilling.go
index f705c7469..21658c07d 100644
--- a/internal/weapons/catalyst/thrilling/thrilling.go
+++ b/internal/weapons/catalyst/thrilling/thrilling.go
@@ -12,7 +12,7 @@ func init() {
 }
 
 func weapon(char core.Character, c *core.Core, r int, param map[string]int) string {
-	last := 0
+	cd := -1
 	isActive := false
 
 	c.Events.Subscribe(core.OnInitialize, func(args ...interface{}) bool {
@@ -35,13 +35,12 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 			isActive = false
 
 			//do nothing if off cd
-			if last != 0 && c.F-last < 1200 {
+			if c.F < cd {
 				return false
 			}
 			//trigger buff if not on cd
-
-			last = c.F
-			expiry := c.F + 600
+			cd = c.F + 60*20
+			expiry := c.F + 60*10
 
 			active := c.Chars[c.ActiveChar]
 			active.AddMod(core.CharStatMod{
diff --git a/internal/weapons/spear/whitetassel/whitetassel.go b/internal/weapons/spear/whitetassel/whitetassel.go
new file mode 100644
index 000000000..1696f0eba
--- /dev/null
+++ b/internal/weapons/spear/whitetassel/whitetassel.go
@@ -0,0 +1,25 @@
+package whitetassel
+
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterWeaponFunc("whitetassel", weapon)
+}
+
+// Increases Normal Attack DMG by 24/30/36/42/48%.
+func weapon(char core.Character, c *core.Core, r int, param map[string]int) string {
+	m := make([]float64, core.EndStatType)
+	m[core.DmgP] = 0.18 + 0.06*float64(r)
+
+	char.AddPreDamageMod(core.PreDamageMod{
+		Key:    "whitetassel",
+		Expiry: -1,
+		Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+			return m, atk.Info.AttackTag == core.AttackTagNormal
+		},
+	})
+
+	return "whitetassel"
+}
diff --git a/internal/weapons/sword/aquila/aquila.go b/internal/weapons/sword/aquila/aquila.go
index 99b4e8667..2768bbb2d 100644
--- a/internal/weapons/sword/aquila/aquila.go
+++ b/internal/weapons/sword/aquila/aquila.go
@@ -50,8 +50,14 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 
 		atk := snap.BaseAtk*(1+snap.Stats[core.ATKP]) + snap.Stats[core.ATK]
 
-		c.Log.NewEvent("acquila heal triggered", core.LogWeaponEvent, char.CharIndex(), "atk", atk, "heal amount", atk*heal)
-		c.Health.HealActive(char.CharIndex(), atk*heal)
+		// c.Log.NewEvent("acquila heal triggered", core.LogWeaponEvent, char.CharIndex(), "atk", atk, "heal amount", atk*heal)
+		c.Health.Heal(core.HealInfo{
+			Caller:  char.CharIndex(),
+			Target:  c.ActiveChar,
+			Message: "Aquila Favonia",
+			Src:     atk * heal,
+			Bonus:   char.Stat(core.Heal),
+		})
 		return false
 	}, fmt.Sprintf("aquila-%v", char.Name()))
 	return "aquilafavonia"
diff --git a/internal/weapons/sword/black/black.go b/internal/weapons/sword/black/black.go
index ce189f961..3af636cc3 100644
--- a/internal/weapons/sword/black/black.go
+++ b/internal/weapons/sword/black/black.go
@@ -46,7 +46,13 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 			return false
 		}
 		if crit && (c.F-last >= 300 || last == 0) {
-			c.Health.HealActive(char.CharIndex(), heal*(atk.Snapshot.BaseAtk*(1+atk.Snapshot.Stats[core.ATKP])+atk.Snapshot.Stats[core.ATK]))
+			c.Health.Heal(core.HealInfo{
+				Caller:  char.CharIndex(),
+				Target:  c.ActiveChar,
+				Message: "The Black Sword",
+				Src:     heal * (atk.Snapshot.BaseAtk*(1+atk.Snapshot.Stats[core.ATKP]) + atk.Snapshot.Stats[core.ATK]),
+				Bonus:   char.Stat(core.Heal),
+			})
 			//trigger cd
 			last = c.F
 		}
diff --git a/pkg/core/action.go b/pkg/core/action.go
index 1de82ee5c..32d3c7699 100644
--- a/pkg/core/action.go
+++ b/pkg/core/action.go
@@ -111,6 +111,8 @@ const (
 	ActionSwap
 	ActionWalk
 	EndActionType
+	//these are only used for frames purposes and that's why it's after end
+	ActionSkillHoldFramesOnly
 )
 
 var astr = []string{
diff --git a/pkg/core/cfg.go b/pkg/core/cfg.go
index bd8643732..c32657fd9 100644
--- a/pkg/core/cfg.go
+++ b/pkg/core/cfg.go
@@ -39,7 +39,7 @@ func (c *SimulationConfig) Clone() SimulationConfig {
 type SimulatorSettings struct {
 	Duration   int
 	DamageMode bool
-	SwapDelay  int
+	Delays     Delays
 
 	//modes
 	QueueMode  SimulationQueueMode
diff --git a/pkg/core/character.go b/pkg/core/character.go
index 49be63a4b..f0e5e4736 100644
--- a/pkg/core/character.go
+++ b/pkg/core/character.go
@@ -1,8 +1,9 @@
 package core
 
 type Character interface {
-	Init(index int) //to be called when everything including weapon and artifacts has been loaded
-	Tick()          //function to be called every frame
+	SetIndex(index int) //to be called to set the index
+	Init()              //to be called when everything including weapon and artifacts has been loaded
+	Tick()              //function to be called every frame
 
 	//information functions
 	Key() CharKey
@@ -45,13 +46,14 @@ type Character interface {
 	// ActionFrames(a ActionType, p map[string]int) int
 	//return the number of frames the current action must wait before it can be
 	//executed;
-	ActionInterruptableDelay(next ActionType) int
+	ActionInterruptableDelay(next ActionType, p map[string]int) int
 
 	//char stat mods
 	AddMod(mod CharStatMod)
 	ModIsActive(key string) bool
 	AddPreDamageMod(mod PreDamageMod)
 	AddWeaponInfuse(inf WeaponInfusion)
+	WeaponInfuseIsActive(key string) bool
 	AddReactBonusMod(mod ReactionBonusMod)
 	ReactBonus(AttackInfo) float64
 
diff --git a/pkg/core/construct.go b/pkg/core/construct.go
index 5b1ae3ce9..b9ccb9aef 100644
--- a/pkg/core/construct.go
+++ b/pkg/core/construct.go
@@ -3,7 +3,8 @@ package core
 type GeoConstructType int
 
 const (
-	GeoConstructNingSkill GeoConstructType = iota
+	GeoConstructInvalid GeoConstructType = iota
+	GeoConstructNingSkill
 	GeoConstructZhongliSkill
 	GeoConstructTravellerSkill
 	GeoConstructTravellerBurst
@@ -13,6 +14,7 @@ const (
 )
 
 var ConstructString = [...]string{
+	"Invalid",
 	"NingSkill",
 	"ZhongliSkill",
 	"TravellerSkill",
@@ -21,6 +23,15 @@ var ConstructString = [...]string{
 	"IttoSkill",
 }
 
+var ConstructNameToKey = map[string]GeoConstructType{
+	"ningguang":      GeoConstructNingSkill,
+	"zhongli":        GeoConstructZhongliSkill,
+	"traveler_skill": GeoConstructTravellerSkill,
+	"traveler_burst": GeoConstructTravellerBurst,
+	"albedo":         GeoConstructAlbedoSkill,
+	"itto":           GeoConstructIttoSkill,
+}
+
 func (c GeoConstructType) String() string {
 	return ConstructString[c]
 }
diff --git a/pkg/core/core.go b/pkg/core/core.go
index 511261042..fcabae387 100644
--- a/pkg/core/core.go
+++ b/pkg/core/core.go
@@ -12,7 +12,6 @@ const (
 	JumpFrames         = 33
 	DashFrames         = 24
 	WalkFrames         = 1
-	SwapFrames         = 1
 	SwapCDFrames       = 60
 	MaxTeamPlayerCount = 4
 	DefaultTargetIndex = 1
@@ -23,7 +22,7 @@ type Flags struct {
 	EnergyCalcMode bool // Allows Burst Action when not at full Energy, logs current Energy when using Burst
 	LogDebug       bool // Used to determine logging level
 	ChildeActive   bool // Used for Childe +1 NA talent passive
-	SwapFrames     int
+	Delays         Delays
 	// AmpReactionDidOccur bool
 	// AmpReactionType     ReactionType
 	// NextAttackMVMult    float64 // melt vape multiplier
@@ -31,6 +30,17 @@ type Flags struct {
 	Custom map[string]int
 }
 
+type Delays struct {
+	Skill  int
+	Burst  int
+	Attack int
+	Charge int
+	Aim    int
+	Dash   int
+	Jump   int
+	Swap   int
+}
+
 type Core struct {
 	//control
 	F     int   // current frame
@@ -87,62 +97,13 @@ func New() *Core {
 	c.stamModifier = make([]stamMod, 0, 10)
 	//make a default nil writer
 	c.Log = &NilLogger{}
-	c.Flags.SwapFrames = SwapFrames
-	// c.queue = make([]Command, 0, 20)
-
-	// for _, f := range cfg {
-	// 	err := f(c)
-	// 	if err != nil {
-	// 		return nil, err
-	// 	}
-	// }
 
-	// if c.Log == nil {
-	// 	c.Log, err = NewDefaultLogger(c, false, false, nil)
-	// 	if err != nil {
-	// 		return nil, err
-	// 	}
-	// }
-
-	// if c.Rand == nil {
-	// 	c.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
-	// }
-	// if c.Events == nil {
-	// 	c.Events = NewEventCtrl(c)
-	// }
-	// if c.Status == nil {
-	// 	c.Status = NewStatusCtrl(c)
-	// }
-	// if c.Energy == nil {
-	// 	c.Energy = NewEnergyCtrl(c)
-	// }
-	// if c.Combat == nil {
-	// 	c.Combat = NewCombatCtrl(c)
-	// }
-	// if c.Tasks == nil {
-	// 	c.Tasks = NewTaskCtrl(&c.F)
-	// }
-	// if c.Constructs == nil {
-	// 	c.Constructs = NewConstructCtrl(c)
-	// }
-	// if c.Shields == nil {
-	// 	c.Shields = NewShieldCtrl(c)
-	// }
-	// if c.Health == nil {
-	// 	c.Health = NewHealthCtrl(c)
-	// }
-	// if c.Queue == nil {
-	// 	c.Queue = NewQueuer(c)
-	// }
-
-	//check handlers
 	return c
 }
 
 func (c *Core) Init() {
-
-	for i, char := range c.Chars {
-		char.Init(i)
+	for _, char := range c.Chars {
+		char.Init()
 	}
 
 	c.Events.Emit(OnInitialize)
@@ -158,7 +119,9 @@ func (c *Core) AddChar(v CharacterProfile) (Character, error) {
 		return nil, err
 	}
 	c.Chars = append(c.Chars, char)
-	c.CharPos[v.Base.Key] = len(c.Chars) - 1
+	i := len(c.Chars) - 1
+	c.CharPos[v.Base.Key] = i
+	char.SetIndex(i)
 
 	wf, ok := weaponMap[v.Weapon.Name]
 	if !ok {
@@ -175,7 +138,7 @@ func (c *Core) AddChar(v CharacterProfile) (Character, error) {
 		if ok {
 			f(char, c, count, v.SetParams[key])
 		} else {
-			c.Log.NewEvent(fmt.Sprintf("character %v has unrecognized set %v", v.Base.Key, key), LogArtifactEvent, -1)
+			return nil, fmt.Errorf("character %v has unrecognized artifact: %v", v.Base.Name, key)
 		}
 	}
 	if total > 5 {
@@ -203,10 +166,10 @@ func (c *Core) Swap(next CharKey) int {
 	c.Events.Emit(OnCharacterSwap, prev, c.ActiveChar)
 	//this duration reset needs to be after the hook for spine to behave properly
 	c.ActiveDuration = 0
-	return c.Flags.SwapFrames
+	return 1
 }
 
-func (c *Core) AnimationCancelDelay(next ActionType) int {
+func (c *Core) AnimationCancelDelay(next ActionType, p map[string]int) int {
 	//if last action is jump, dash, swap,
 	switch c.LastAction.Typ {
 	case ActionSwap:
@@ -217,11 +180,30 @@ func (c *Core) AnimationCancelDelay(next ActionType) int {
 		return 0
 	}
 	//other wise check with the current character
-	return c.Chars[c.ActiveChar].ActionInterruptableDelay(next)
+	return c.Chars[c.ActiveChar].ActionInterruptableDelay(next, p)
 }
 
 func (c *Core) UserCustomDelay() int {
-	return c.LastAction.Param["delay"]
+	d := 0
+	switch c.LastAction.Typ {
+	case ActionSkill:
+		d = c.Flags.Delays.Skill
+	case ActionBurst:
+		d = c.Flags.Delays.Burst
+	case ActionAttack:
+		d = c.Flags.Delays.Attack
+	case ActionCharge:
+		d = c.Flags.Delays.Charge
+	case ActionDash:
+		d = c.Flags.Delays.Dash
+	case ActionJump:
+		d = c.Flags.Delays.Jump
+	case ActionSwap:
+		d = c.Flags.Delays.Swap
+	case ActionAim:
+		d = c.Flags.Delays.Aim
+	}
+	return c.LastAction.Param["delay"] + d
 }
 
 func (c *Core) ResetAllNormalCounter() {
diff --git a/pkg/core/handlers.go b/pkg/core/handlers.go
index c0e91ed72..29ec726d9 100644
--- a/pkg/core/handlers.go
+++ b/pkg/core/handlers.go
@@ -45,14 +45,12 @@ type ConstructHandler interface {
 	CountByType(t GeoConstructType) int
 	Destroy(key int) bool
 	Has(key int) bool
+	Expiry(t GeoConstructType) int
 	Tick()
 }
 
 type HealthHandler interface {
-	HealIndex(caller int, index int, amt float64)
-	HealActive(caller int, amt float64)
-	HealAll(caller int, amt float64)
-	HealAllPercent(caller int, percent float64)
+	Heal(hi HealInfo)
 	AddIncHealBonus(f func(healedCharIndex int) float64)
 
 	AddDamageReduction(f func() (float64, bool))
diff --git a/pkg/core/health.go b/pkg/core/health.go
new file mode 100644
index 000000000..8d91c45c1
--- /dev/null
+++ b/pkg/core/health.go
@@ -0,0 +1,17 @@
+package core
+
+type HealInfo struct {
+	Caller  int // index of healing character
+	Target  int // index of char receiving the healing. use -1 to heal all characters
+	Type    HealType
+	Message string
+	Src     float64 // depends on the type
+	Bonus   float64
+}
+
+type HealType int
+
+const (
+	HealTypeAbsolute HealType = iota // regular number
+	HealTypePercent                  // percent of the target's max hp
+)
diff --git a/pkg/core/target.go b/pkg/core/target.go
index d7394c943..e03fa6959 100644
--- a/pkg/core/target.go
+++ b/pkg/core/target.go
@@ -195,7 +195,6 @@ func (c *Core) RandomEnemyTarget() int {
 		}
 	}
 	panic("no random target found?? should not happen")
-	return -1
 }
 
 func (c *Core) RandomTargetIndex(typ TargettableType) int {
@@ -220,5 +219,4 @@ func (c *Core) RandomTargetIndex(typ TargettableType) int {
 		}
 	}
 	panic("no random target found?? should not happen")
-	return -1
 }
diff --git a/pkg/parse/parseCharLine.go b/pkg/parse/parseCharLine.go
index 819e63017..5aff6212a 100644
--- a/pkg/parse/parseCharLine.go
+++ b/pkg/parse/parseCharLine.go
@@ -165,6 +165,8 @@ func parseCharAddSet(p *Parser) (parseFn, error) {
 		case itemTerminateLine:
 			c.Sets[label] = count
 			return parseRows, nil
+		default:
+			return nil, fmt.Errorf("unexpected token after in parsing sets: %v at line %v", n, p.tokens)
 		}
 		if err != nil {
 			return nil, err
diff --git a/pkg/parse/parseOptions.go b/pkg/parse/parseOptions.go
index 62169680f..eb5818048 100644
--- a/pkg/parse/parseOptions.go
+++ b/pkg/parse/parseOptions.go
@@ -47,7 +47,59 @@ func parseOptions(p *Parser) (parseFn, error) {
 			case "swap_delay":
 				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
 				if err == nil {
-					p.cfg.Settings.SwapDelay, err = itemNumberToInt(n)
+					p.cfg.Settings.Delays.Swap, err = itemNumberToInt(n)
+				}
+			case "attack_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Attack, err = itemNumberToInt(n)
+				}
+			case "charge_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Charge, err = itemNumberToInt(n)
+				}
+			case "skill_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Skill, err = itemNumberToInt(n)
+				}
+			case "burst_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Burst, err = itemNumberToInt(n)
+				}
+			case "jump_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Jump, err = itemNumberToInt(n)
+				}
+			case "dash_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Dash, err = itemNumberToInt(n)
+				}
+			case "aim_delay":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err == nil {
+					p.cfg.Settings.Delays.Aim, err = itemNumberToInt(n)
+				}
+			case "frame_defaults":
+				n, err = p.acceptSeqReturnLast(itemEqual, itemIdentifier)
+				if err == nil {
+					switch n.val {
+					case "human":
+						p.cfg.Settings.Delays.Swap = 8
+						p.cfg.Settings.Delays.Attack = 5
+						p.cfg.Settings.Delays.Charge = 5
+						p.cfg.Settings.Delays.Skill = 5
+						p.cfg.Settings.Delays.Burst = 5
+						p.cfg.Settings.Delays.Dash = 5
+						p.cfg.Settings.Delays.Jump = 5
+						p.cfg.Settings.Delays.Aim = 5
+					default:
+						return nil, fmt.Errorf("unrecognized option for frame_defaults specified: %v", n.val)
+					}
 				}
 			case "er_calc":
 				//does nothing thus far...
diff --git a/pkg/simulation/core.go b/pkg/simulation/core.go
index 7ca3389fb..fb171457c 100644
--- a/pkg/simulation/core.go
+++ b/pkg/simulation/core.go
@@ -50,8 +50,9 @@ func NewCore(seed int64, debug bool, cfg core.SimulatorSettings) (*core.Core, er
 		return nil, errors.New("no action mode set - please add either mode=sl or mode=apl to the options")
 	}
 
-	if cfg.SwapDelay > 0 {
-		c.Flags.SwapFrames = cfg.SwapDelay
+	c.Flags.Delays = cfg.Delays
+	if c.Flags.Delays.Swap == 0 {
+		c.Flags.Delays.Swap = 1 //default 1 frame if none set
 	}
 
 	return c, nil
diff --git a/pkg/simulation/imports.go b/pkg/simulation/imports.go
index 133d234f6..bf83f82fc 100644
--- a/pkg/simulation/imports.go
+++ b/pkg/simulation/imports.go
@@ -7,6 +7,7 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/artifacts/bloodstained"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/bolide"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/crimson"
+	_ "github.com/genshinsim/gcsim/internal/artifacts/exile"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/gladiator"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/heartofdepth"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/huskofopulentdreams"
@@ -79,8 +80,10 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/favonius"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/hamayumi"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/huntersbow"
+	_ "github.com/genshinsim/gcsim/internal/weapons/bow/mitternachtswaltz"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/mouunsmoon"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/polarstar"
+	_ "github.com/genshinsim/gcsim/internal/weapons/bow/predator"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/prototype"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/royal"
 	_ "github.com/genshinsim/gcsim/internal/weapons/bow/rust"
@@ -154,6 +157,7 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/weapons/spear/skyward"
 	_ "github.com/genshinsim/gcsim/internal/weapons/spear/vortex"
 	_ "github.com/genshinsim/gcsim/internal/weapons/spear/wavebreaker"
+	_ "github.com/genshinsim/gcsim/internal/weapons/spear/whitetassel"
 	_ "github.com/genshinsim/gcsim/internal/weapons/sword/alley"
 	_ "github.com/genshinsim/gcsim/internal/weapons/sword/amenoma"
 	_ "github.com/genshinsim/gcsim/internal/weapons/sword/aquila"
diff --git a/pkg/simulation/resonance.go b/pkg/simulation/resonance.go
index 1fcb850e3..d7ffdacdd 100644
--- a/pkg/simulation/resonance.go
+++ b/pkg/simulation/resonance.go
@@ -21,7 +21,10 @@ func (s *Simulation) initResonance(count map[core.EleType]int) {
 				}
 			case core.Hydro:
 				//heal not implemented yet
-				s.C.Log.NewEvent("adding hydro resonance (NO IMPLEMENTED)", core.LogSimEvent, -1)
+				s.C.Log.NewEvent("adding hydro resonance", core.LogSimEvent, -1)
+				s.C.Health.AddIncHealBonus(func(healedCharIndex int) float64 {
+					return 0.3
+				})
 			case core.Cryo:
 				s.C.Log.NewEvent("adding cryo resonance", core.LogSimEvent, -1)
 				val := make([]float64, core.EndStatType)
diff --git a/pkg/simulation/run.go b/pkg/simulation/run.go
index 463d727bd..d35621d81 100644
--- a/pkg/simulation/run.go
+++ b/pkg/simulation/run.go
@@ -106,55 +106,78 @@ func (s *Simulation) AdvanceFrame() error {
 		s.queue = append(s.queue, next...)
 	}
 
-	if len(s.queue) > 0 {
+	//here we need to check for delay but only if the next action is an action
+	//i.e. not a wait
 
+	act, isAction := s.queue[0].(*core.ActionItem)
+
+	//we need to check for when the previous action finished "executing"
+	//this is because sometimes the next action isn't queued for a while
+	//so we can end up with a situation where the last action was queued 100 frames ago
+	//and then we're still trying to add more delay on top of 100 frame
+	if isAction {
 		var delay int
-		//check if the current action is executable right now; if not then delay
-		act, isAction := s.queue[0].(*core.ActionItem)
-		if isAction {
-			delay = s.C.AnimationCancelDelay(act.Typ) + s.C.UserCustomDelay()
+		//check if this action is ready
+		char := s.C.Chars[s.C.ActiveChar]
+		if !(char.ActionReady(act.Typ, act.Param)) {
+			s.C.Log.NewEvent("queued action is not ready, should not happen; skipping frame", core.LogSimEvent, -1)
+			return nil
 		}
+		delay = s.C.AnimationCancelDelay(act.Typ, act.Param) + s.C.UserCustomDelay()
+		//check if we should delay
 
+		//so if current frame - when the last action is used is >= delay, then we shouldn't
+		//delay at all
+		if s.C.F-s.lastActionUsedAt >= delay {
+			delay = 0
+		}
+
+		//other wise we can add delay
 		if delay > 0 {
+			s.C.Log.NewEvent(
+				"animation delay triggered",
+				core.LogActionEvent,
+				s.C.ActiveChar,
+				"total_delay", delay,
+				"param", s.C.LastAction.Param["delay"],
+				"default_delays", s.C.Flags.Delays,
+			)
 			s.skip = delay
 			return nil
 		}
+	}
 
-		// s.C.Log.Debugw("queue check - before exec",
-		// 	"frame", s.C.F,
-		// 	core.LogQueueEvent,
-		// 	"remaining queue", s.queue,
-		// )
+	s.skip, done, err = s.C.Action.Exec(s.queue[0])
+	//last action used should then be current frame + how much we are skipping (i.e. first frame queueable)
+	s.lastActionUsedAt = s.C.F + s.skip
+	if err != nil {
+		return err
+	}
 
-		s.skip, done, err = s.C.Action.Exec(s.queue[0])
-		if err != nil {
-			return err
+	if done {
+		// if s.opts.LogDetails && isAction {
+		if isAction {
+			s.stats.AbilUsageCountByChar[s.C.ActiveChar][act.Typ.String()]++
 		}
-
-		if done {
-			// if s.opts.LogDetails && isAction {
-			if isAction {
-				s.stats.AbilUsageCountByChar[s.C.ActiveChar][act.Typ.String()]++
-			}
-			//pop queue
-			s.queue = s.queue[1:]
-		} else {
-			if s.dropQueueIfFailed {
-				//drop rest of the queue
-				s.queue = s.queue[:0]
-				//reset
-				s.dropQueueIfFailed = false
-			}
+		//pop queue
+		s.queue = s.queue[1:]
+	} else {
+		if s.dropQueueIfFailed {
+			//drop rest of the queue
+			s.queue = s.queue[:0]
+			//reset
+			s.dropQueueIfFailed = false
 		}
-		// s.C.Log.Debugw("queue check - after exec",
-		// 	"frame", s.C.F,
-		// 	core.LogQueueEvent,
-		// 	"remaining queue", s.queue,
-		// 	"skip", s.skip,
-		// 	"done", done,
-		// 	"dropIfFailed", s.dropQueueIfFailed,
-		// )
 	}
+	// s.C.Log.Debugw("queue check - after exec",
+	// 	"frame", s.C.F,
+	// 	core.LogQueueEvent,
+	// 	"remaining queue", s.queue,
+	// 	"skip", s.skip,
+	// 	"done", done,
+	// 	"dropIfFailed", s.dropQueueIfFailed,
+	// )
+
 	return nil
 }
 
diff --git a/pkg/simulation/sim.go b/pkg/simulation/sim.go
index 0f7798f21..2a7b916a7 100644
--- a/pkg/simulation/sim.go
+++ b/pkg/simulation/sim.go
@@ -18,6 +18,8 @@ type Simulation struct {
 	lastEnergyDrop int
 	//result
 	stats Result
+	//prevs action that was checked
+	lastActionUsedAt int
 }
 
 func New(cfg core.SimulationConfig, c *core.Core) (*Simulation, error) {
diff --git a/release-notes.txt b/release-notes.txt
deleted file mode 100644
index 204d2dc54..000000000
--- a/release-notes.txt
+++ /dev/null
@@ -1,564 +0,0 @@
-0.5.3  - Feb 21, 2022
----------------------
-
-- Sorry I lied. Really fixed auto build this time. Probably...
-
-0.5.2  - Feb 21, 2022
----------------------
-
-- Fix auto build cmd line tool (hopefully)
-
-0.5.1  - Feb 21, 2022
----------------------
-
-- Add initial Itto implementation, thanks to Aftermath#7658
-- Add Ayaka cons
-- Add better error message when wrong mode is used (sequential vs action list)
-- Add walk action for cancel purposes (i.e. Klee)
-- Add Slingshot, thanks to warbear#3252
-
-0.5.0  - Feb 20, 2022
----------------------
-
-- Desktop app officially retired in favour of webapp (https://gcsim.app)
-- Hakushin Ring added
-- Wine and Song added
-- Compound Bow added
-- Logging framework refactor completed
-- Bug fixes:
-    * Various Yae fixes (too many to list)
-    * Fixes #252 - Add NA counter to abil name for missing chars
-    * Fixes #251 - Snow Tombed Starsilver did not proc due to ICD check
-    * Fixes #250 - Sara A4 energy gen calc was wrong
-    * Fixes #245 - Childe ranged burst did not properly apply riptide
-    * Fixes Kazuha C2 duration
-    * Change instances of c.Stats[] to c.Stat() in char code to use mods
-    * Offset Xiao plunging hits from main plunge attack
-    * Implement Kazuha CA #224
-    * Fix Beidou C6 #240
-    * Make Kokomi A4 dynamic
-    * Add travel time to Ganyu bloom #242
-    * Fix NA/CA/Plunge snapshot timing
-    * emc skill: fix particle element
-    * gmc: a2 damage should count as normal attack dmg
-    * raiden: fix polearm CA tags
-    * skyward/prototype: change PostBurst to PreBurst
-    * Fix Kokomi skill snapshot
-    * Change order of NA/CA callbacks to fix #246
-    * Fix Pale Flame stacks
-    * Add source string to AddEnergy
-    * Add char to fav procs; amend apl particle wait for compatibility
-    * Add energy detail log tracking
-    * Fix flags not getting parsed in cmd and substat optimizer parse bug
-    * Fixes #272 and #273 - Fixes Shenhe particle timing and burst
-        - Burst ticks were incorrectly coded
-        - C2 was not implemented correctly
-        - Burst res shred duration was wrong
-    * Fixes #235 - Generated crystallize shields were always electro
-    * Fixes #284 and #285 - Add particles to Keqing E; fix Keqing Q name
-    * Fixes #286 - Substat optimizer should ignore gzip setting
-    * Fixes #288 - Updates log for energy add, random normals, and status
-        - Energy add now have a fuller set of stats
-        - Random energy on normal logs moved to sim log
-        - Status log messages aligned with mod messages
-
-
-0.4.37 - Feb 08, 2022
----------------------
-
-- Fixed Zhongli Stele not snapshotting
-- Fixed Zhongli Stele max count incorrectly capped at 1
-- Fixed Xiao snapshot timing
-- Fixed Yunjin perfect counter. Changed to skill[perfect=1]
-- Fixed Kazuha c2 not applied to self when off-field
-
-
-0.4.36 - Feb 06, 2022
----------------------
-
-- Fixes pale flame 2 pc set typo
-- Fixes Bennett burst frame timing and A4 activation method
-- Fixes Gorou C6 incorrectly snapshotting
-- Add max energy in energy log
-- Add Eula skill hold check for tenacity and pale flame
-- Fixed Eula burst timing
-- Fixed Skyward Pride proc count
-- Added Calamity Queller
-- Fix Polar Star
-- Fix Mistsplitter burst bug
-- Add Moonglow
-
-
-0.4.35 - Feb 01, 2022
----------------------
-
-- Fix Aloy coil stacks issue
-- Fix Xingqiu infinite burst
-- Fix Venti Q frames
-- Fix Kokomi not adding in heal bonus
-- Add ability to set Geo Traveller burst construct count for Zhongli
-- Fix Keqing C4 and weapon attack tag checks
-- Add Polar Star
-
-0.4.34 - Jan 27, 2022
----------------------
-
-- Overall debug improvements, moving things into consistent categories (#201)
-- A lot of bugfixes. Below list the ones that have been resolved:
-  - Bennett Q did not work properly at C0
-  - Lisa C1 was activating on her CA and not her hold E
-  - Windblume Ode did not have an onfield check
-  - Ganyu E explosion particle timing was wrong
-  - Kaeya CA was coded as being an NA tag
-  - Geo Traveller had an 80 cost burst instead of 60
-  - Geo Traveller energy refund did not work if cast at frame 0
-  - Hydro swirl was doing AoE damage
-  - Jean NA was snapshotting incorrectly
-  - Jean CA was not implemented
-  - Keqing Q was coded as snapshotted, not dynamic
-
-0.4.33 - Jan 24, 2022
----------------------
-
-- Fix substat optimizer typo
-- Fix Amber Attack Tag, ICD Tag, and ICD Group
-- Fix Eula grimheart stacks not adding
-- Fix Fischl snapshot frames to account for on damage proc timing
-- Fix Freedom Sworn - ATK did not snapshot, dmg% buff did not reset
-
-0.4.32 - Jan 24, 2022
----------------------
-
-- Implement substat optimization in cmd line
-- Fix Primordial Jade Winged-Spear stacks
-- Fix Guoba snapshot timing
-- Fix Kazuha plunge not shattering
-- Fix Chongyun infuse field timing
-- Fix swirl damage ICD so an enemy can be affected by more than 2 damage instances
-- Fix VV not proccing on AoE targets appropriately
-
-0.4.31 - Jan 22, 2022
----------------------
-
-- Fix Rosaria burst using wrong multiplier
-- Fix Diona burst framess
-
-0.4.30 - Jan 21, 2022
----------------------
-
-- Fix Albedo burst CD
-- Fix Barbara E scaling
-
-0.4.29 - Jan 20, 2022
----------------------
-
-- Fix burst CD start to match energy consumption
-- Fix Ayaka ICD on NA and Dash
-
-0.4.28 - Jan 18, 2022
----------------------
-
-- Fix Chongyun Q frames
-
-0.4.27 - Jan 15, 2022
----------------------
-
-- Minor bug fixes
-
-0.4.26 - Jan 11, 2022
----------------------
-
-- Bug fixes for Ningguang, prototype amber, and elegy
-- Disable debug mode option; always use debug mode
-- Fix missing images
-
-0.4.25 - Jan 11, 2022
----------------------
-
-- Logging cleanup
-
-0.4.24 - Jan 08, 2022
----------------------
-
-- Fixed Solar Pearl, Alley Hunter, Lost Prayer, Lisa bugs
-
-0.4.23 - Jan 08, 2022
----------------------
-
-- Fixed Ganyu Q bug
-
-0.4.22 - Jan 08, 2022
----------------------
-
-- Fixed output file name
-
-0.4.21 - Jan 08, 2022
----------------------
-
-- Disable workflow cache
-
-0.4.20 - Jan 07, 2022
----------------------
-
-- Shenhe ICD fix
-- Yunjin particles adjustment
-- Added share button
-
-
-0.4.19 - Jan 07, 2022
----------------------
-
-- Actually fix shatter this time.. (forgot to merge bugfix)
-
-0.4.18 - Jan 07, 2022
----------------------
-
-- Fix shatter bug
-- Enable mods by default in debug view
-
-0.4.17 - Jan 07, 2022
----------------------
-
-- Forgot to enable Yunjin...
-
-0.4.16 - Jan 06, 2022
----------------------
-
-- Fix Shenhe NO4 snapshotting
-- Fix Kazuha frames
-- Fix shields not overwriting properly
-
-0.4.15 - Jan 06, 2022
----------------------
-
-- Actually fix Shenhe cooldown this time...
-- Additional bug fixes for Noelle,  Hutao, Bennett, Yoimiya
-
-
-0.4.14 - Jan 05, 2022
----------------------
-
-- Fix Shenhe skill cd
-
-
-0.4.13 - Jan 05, 2022
----------------------
-
-- Add Shenhe
-- Fixed various Zhongli bugs
-- Change +try to either +try=drop or +try=wait
-
-0.4.12 - Jan 02, 2022
----------------------
-
-- Various bug fixes
-
-0.4.11 - Jan 02, 2022
----------------------
-
-- Fixed bug introduced in 0.4.10
-
-0.4.10 - Jan 02, 2022
----------------------
-
-- Fixed Kazuha burst timing
-- Fixed Beidou ascension stat
-- Fixed debug output for reaction into freeze/ec
-
-0.4.9 - Jan 02, 2022
---------------------
-
-- Added guoba swirl
-- Fixed Noelle and HT snapshots
-- Some backend clean up
-- Enabled Gorou
-
-0.4.8 - Dec 31, 2021
---------------------
-
-- Fix limit/reset_limit commands not work
-
-0.4.7 - Dec 31, 2021
---------------------
-
-- Add restart command to calc mode
-- Various bug fixes
-
-0.4.6 - Dec 31, 2021
---------------------
-
-- Add Bennett Q self-infusion
-
-0.4.5 - Dec 31, 2021
---------------------
-
-- Revert changes made in 0.4.4
-
-0.4.4 - Dec 30, 2021
---------------------
-
-- Simplified calc mode shorthand to remove commas
-
-0.4.3 - Dec 30, 2021
---------------------
-
-- Allow shorthand in calc mode (ui only)
-
-0.4.2 - Dec 30, 2021
---------------------
-
-- Syntax bug fixes
-
-0.4.1 - Dec 30, 2021
---------------------
-
-- Fixed ICD group bug
-
-0.4.0 - Dec 29, 2021
---------------------
-
-- THIS RELEASE CONTAINS BREAKING CHANGES!!!
-  - The entire action list syntax has been redesigned to be simpler.
-  - All action lists on https://www.gcsim.app have been updated to this new format. Please check for example
-  - Documentation will be added shortly to the wiki
-
-Because this is a very significant change, as always please get in touch (Discord preferred) if you run into any issues
-
-0.3.6 - Dec 28, 2021
---------------------
-
-- Fixed Riaden E restoring energy
-
-
-0.3.5 - Dec 28, 2021
---------------------
-
-- Fixed EC ticks critting
-
-0.3.4 - Dec 26, 2021
---------------------
-
-- Fixed Mona Q cost incorrectly set to 80
-
-0.3.3 - Dec 25, 2021
---------------------
-
-- More bug fixes as a Christmas present
-
-0.3.2 - Dec 24, 2021
---------------------
-
-- Fix reaction damage benefiting incorrectly from dynamic mods
-
-0.3.1 - Dec 24, 2021
---------------------
-
-- Various bug fixes (freeze, calc mode, Mona not activated)
-
-0.3.0 - Dec 24, 2021
---------------------
-
-- Added Mona
-- Geo MC and Gorou have been added as well but are not yet currently enabled due to issues with the UI
-- The entire targetting and reaction system have been rewritten. You'll notice the
-  following:
-  - There is always a target 0 that takes no damage. This is to represent the player
-  - Debug will contain a whole bunch of skipped messages. This is because targeting is
-    now based on a 2D hitbox system.
-If you notice any issues with sim results compared to previous builds, please drop an issue in GitHub
-
-
-0.2.27 - Dec 5, 2021
---------------------
-
-- Would help if I remember to enable Qiqi in UI...
-
-0.2.26 - Dec 5, 2021
---------------------
-
-- Qiqi added thanks to Tsym#2586; See https://github.com/genshinsim/gcsim/pull/79
-- Clam, Cinnabar and Husk added added
-- Fixed various bugs
-
-0.2.25 - Nov 27, 2021
----------------------
-
-- Update genshin-db to latest version
-- Added a couple generic weapons with no effects
-- Added images for new artifacts
-- Fix artifact name showing up incorrectly in edit screen
-
-0.2.24 - Nov 25, 2021
----------------------
-
-- Added ability to export full config to file
-- Fixed Fischl C6 bug
-
-0.2.23 - Nov 24, 2021
----------------------
-
-- Fix UI bug where it is adding way too much CR/CD
-- Various bug fixes; see https://github.com/genshinsim/gcsim/pull/73
-
-0.2.22 - Nov 23, 2021
----------------------
-
-- Fix a bug where debug is not hiding the right items
-
-0.2.21 - Nov 23, 2021
----------------------
-
-- Revert back to genshin-db@3.11.2; Newest version seems to crash
-
-0.2.20 - Nov 23, 2021
----------------------
-
-- Added calc mode (documentation needs to be added still)
-- Fixed Aloy not enabled (forgot again...)
-
-
-0.2.19 - Nov 22, 2021
----------------------
-
-- Aloy added thanks to Tsym#2586; See https://github.com/genshinsim/gcsim/pull/71
-- Added ability to save and load teams to JSON files in UI
-
-0.2.18 - Nov 22, 2021
----------------------
-
-- Tartaglia added thanks to vigne#5943 and Tsym#2586; See https://github.com/genshinsim/gcsim/pull/43
-
-
-0.2.17 - Nov 20, 2021
----------------------
-
-- Forgot to actually enable Kokomi and Yanfei in UI....
-
-0.2.16 - Nov 20, 2021
----------------------
-
-- Added Yanfei (thanks to Tsym#2586); See https://github.com/genshinsim/gcsim/pull/70
-- Added Kokomi (thanks to Tsym#2586); See https://github.com/genshinsim/gcsim/pull/68
-- Fixed a bug with Albedo E construct refreshing
-- Added tags/status for Bennett buff and Xiao E stacks
-- Fixed debug to lazy load/render; Should be faster now
-- Added better queue debug msgs
-
-0.2.15 - Nov 15, 2021
----------------------
-
-- Various bug fixes from; See https://github.com/genshinsim/gcsim/pull/66
-
-
-0.2.14 - Nov 8, 2021
----------------------
-
-- Rebrand app name
-
-
-0.2.13 - Nov 5, 2021
----------------------
-
-- Various bug fixes:
-  - Fix Zhongli Q multiplier
-  - Fix Ningguang C2 reset at start
-  - Fix Ningguang skill multiplier incorrect
-  - Fix Prototype Archaic not activating properly
-  - Fix Fischl Oz number of hits
-  - Add Sea-Lord and Nagamasa
-
-0.2.12 - Nov 4, 2021
----------------------
-
-- Various bug fixes:
-  - Archaic petra
-  - Diluc infusion timing/snapshot timing
-  - Fischl A4
-  - Kitain buffing wrong thing
-
-0.2.11 - Nov 4, 2021
----------------------
-
-- Fixed Ningguang charge attack
-
-
-0.2.10 - Nov 4, 2021
----------------------
-
-- Fixed an issue with rand not properly seeded and a bug introduced in 0.2.9
-
-0.2.9
------
-
-- Fixed weapons that add elemental damage incorrectly also adding to all damage:
-  - Mappa
-  - Skyward Atlas
-  - Kitain Cross
-  - Mistsplitter
-  - Widsith
-  - Lost Prayer
-
-0.2.8
------
-
-- Add VirusTotal scan to releases.
-
-0.2.7
------
-
-- Fixed Sara C6, Xingqiu orbital, VH procs, and other minor bugs (https://github.com/genshinsim/gcsim/pull/48)
-
-0.2.6
------
-
-- Fixed a bug where UI was running every simulation with R1 weapon only.
-
-0.2.5
------
-
-- Add new weapons:
-    Wavebreaker's Fin
-    Akuoumaru
-    Mouun's Moon
-
-0.2.4
------
-
-- Fixed a bug introduced in Raiden's C2 shred
-
-0.2.3
------
-
-- Fixed Raiden's c2 and cleaned up Sucrose A2 log (https://github.com/genshinsim/gcsim/commit/780753b29d8d8905a6cb587e2297e2a9972d07db)
-
-
-0.2.2
------
-
-- Merged PR#32. Fix Raiden/Xingqiu/Bennett snapshot timing
-
-
-0.2.1
------
-
-- Fixed Primordial Jade Cutter bug where it was incorrectly adding atk% instead of flat atk
-
-0.2.0
------
-
-- By default, sim will now use team from the team builder. This can be turned off in the options (advanced users).
-  The team config screen has been removed to reflect this.
-- Graphs have been added to results
-- Name changed to gcsim to reflect new website
-
-
-0.1.4
------
-
-- Updated app to include all image assets
-
-
-0.1.3
------
-
-- Added auto updater


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)
+	}
+}
diff --git a/internal/tmpl/character/curve_test.go b/internal/tmpl/character/curve_test.go
index adf17026e..9f78eda26 100644
--- a/internal/tmpl/character/curve_test.go
+++ b/internal/tmpl/character/curve_test.go
@@ -35,7 +35,7 @@ func TestBasicAbilUsage(t *testing.T) {
 		t.FailNow()
 	}
 	c.Chars = append(c.Chars, x)
-	x.Init(0)
+	x.Init()
 
 	if !floatApproxEqual(210, x.Base.Atk, 1) {
 		t.Errorf("expecting ~210 base atk, got %v", x.Base.Atk)