Repository: genshinsim/gcsim
Pull request #484 (2022-04-17 19:40:12)

sucrose can cancel her dash cancel
Barbara and Diona can too, I will add them when I do their frame updates.


Patch:
diff --git a/.github/workflows/nightly-build.yaml b/.github/workflows/nightly-build.yaml
index daef75deb..acacc2ee5 100644
--- a/.github/workflows/nightly-build.yaml
+++ b/.github/workflows/nightly-build.yaml
@@ -22,18 +22,11 @@ jobs:
 
           # run the script
           "${GITHUB_WORKSPACE}/scripts/build.sh"
-      # - run: ls
-      #   working-directory: ./cmd/gcsim
-      - run: ls
-        working-directory: ./
-      # - run: ls
-      #   working-directory: ./scripts
       - uses: "marvinpinto/action-automatic-releases@latest"
         with:
           repo_token: "${{ secrets.GITHUB_TOKEN }}"
-          prerelease: false
+          prerelease: true
           automatic_release_tag: nightly
           files: |
-            release-notes.txt
             LICENSE
             gcsim.exe
diff --git a/internal/artifacts/blizzard/blizzard.go b/internal/artifacts/blizzard/blizzard.go
index b147706e1..2e0dd7b9f 100644
--- a/internal/artifacts/blizzard/blizzard.go
+++ b/internal/artifacts/blizzard/blizzard.go
@@ -22,11 +22,11 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 		})
 	}
 	if count >= 4 {
+		m := make([]float64, core.EndStatType)
 		c.AddPreDamageMod(core.PreDamageMod{
 			Key:    "4bs",
 			Expiry: -1,
 			Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
-				m := make([]float64, core.EndStatType)
 				//frozen check first so we don't mistaken coexisting cryo
 				if t.AuraContains(core.Frozen) {
 					m[core.CR] = 0.4
@@ -39,7 +39,5 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 				return nil, false
 			},
 		})
-
 	}
-	//add flat stat to char
 }
diff --git a/internal/artifacts/gambler/gambler.go b/internal/artifacts/gambler/gambler.go
new file mode 100644
index 000000000..77ebb9b70
--- /dev/null
+++ b/internal/artifacts/gambler/gambler.go
@@ -0,0 +1,26 @@
+package gambler
+
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterSetFunc("gambler", New)
+}
+
+// 2-Piece Bonus: Elemental Skill Dmg +20%
+// 4-Piece Bonus: Resets Skill CD after defeating an enemy - not yet relevent to the sim
+func New(c core.Character, s *core.Core, count int, params map[string]int) {
+	if count >= 2 {
+		m := make([]float64, core.EndStatType)
+		m[core.DmgP] = 0.2
+		c.AddPreDamageMod(core.PreDamageMod{
+			Key: "gambler-2pc",
+			Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+				return m, (atk.Info.AttackTag == core.AttackTagElementalArt ||
+					atk.Info.AttackTag == core.AttackTagElementalArtHold)
+			},
+			Expiry: -1,
+		})
+	}
+}
diff --git a/internal/artifacts/huskofopulentdreams/huskofopulentdreams.go b/internal/artifacts/huskofopulentdreams/huskofopulentdreams.go
index f432430f9..8ef3625c4 100644
--- a/internal/artifacts/huskofopulentdreams/huskofopulentdreams.go
+++ b/internal/artifacts/huskofopulentdreams/huskofopulentdreams.go
@@ -42,13 +42,18 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 
 		// Helper function to check for stack loss
 		// called after every stack gain
-		checkStackLoss := func() {
-			if (lastStackGain + 360) >= s.F {
+		var checkStackLoss func()
+		checkStackLoss = func() {
+			if (lastStackGain + 360) > s.F {
 				return
 			}
 			stacks--
 			s.Log.NewEvent("Husk lost stack", core.LogArtifactEvent, c.CharIndex(), "stacks", stacks, "last_swap", lastSwap, "last_stack_change", lastStackGain)
 
+			// queue up again if we still have stacks
+			if stacks > 0 {
+				c.AddTask(checkStackLoss, "husk-4pc-stack-loss-check", 360)
+			}
 		}
 
 		var gainStackOfffield func(src int) func()
diff --git a/internal/artifacts/scholar/scholar.go b/internal/artifacts/scholar/scholar.go
new file mode 100644
index 000000000..880a9497c
--- /dev/null
+++ b/internal/artifacts/scholar/scholar.go
@@ -0,0 +1,51 @@
+package scholar
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+func init() {
+	core.RegisterSetFunc("scholar", New)
+
+}
+
+// 2-Piece Bonus: Energy Recharge +20%.
+// 4-Piece Bonus: Gaining Elemental Particles or Orbs gives 3 Energy to all party members who have a bow or a catalyst equipped. Can only occur once every 3s.
+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:    "scholar-2pc",
+			Expiry: -1,
+			Amount: func() ([]float64, bool) {
+				return m, true
+			},
+		})
+	}
+	if count >= 4 {
+		// TODO: test lmao
+		s.Events.Subscribe(core.OnParticleReceived, func(args ...interface{}) bool {
+			if s.ActiveChar != c.CharIndex() {
+				return false
+			}
+			if s.Status.Duration("scholar") > 0 {
+				return false
+			}
+			s.Status.AddStatus("scholar", 180)
+
+			for _, char := range s.Chars {
+				this := char
+
+				// only for bow and catalyst
+				if this.WeaponClass()==core.WeaponClassBow ||this.WeaponClass()==core.WeaponClassCatalyst{
+					this.AddEnergy("scholar-4pc", 3)
+				}
+			}
+
+			return false
+		}, fmt.Sprintf("scholar-4pc-%v", c.Name()))
+	}
+}
diff --git a/internal/artifacts/tenacity/tenacity.go b/internal/artifacts/tenacity/tenacity.go
index afbee87fb..ff4d66c67 100644
--- a/internal/artifacts/tenacity/tenacity.go
+++ b/internal/artifacts/tenacity/tenacity.go
@@ -25,6 +25,8 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 		})
 	}
 	if count >= 4 {
+		m := make([]float64, core.EndStatType)
+		m[core.ATKP] = 0.2
 		icd := 0
 
 		s.Events.Subscribe(core.OnDamage, func(args ...interface{}) bool {
@@ -43,23 +45,15 @@ func New(c core.Character, s *core.Core, count int, params map[string]int) {
 
 			for _, char := range s.Chars {
 				char.AddMod(core.CharStatMod{
-					Key: "tom-4pc",
+					Key:    "tom-4pc",
+					Expiry: s.F + 180,
 					Amount: func() ([]float64, bool) {
-						m := make([]float64, core.EndStatType)
-						m[core.ATKP] = 0.2
-						if s.Status.Duration("tom-proc") == 0 {
-							return nil, false
-						}
 						return m, true
 					},
-					Expiry: s.F + 180,
 				})
-
 			}
 			s.Log.NewEvent("tom 4pc proc", core.LogArtifactEvent, c.CharIndex(), "expiry", s.F+180, "icd", s.F+30)
 			return false
 		}, fmt.Sprintf("tom4-%v", c.Name()))
-
 	}
-	//add flat stat to char
 }
diff --git a/internal/characters/ayaka/abil.go b/internal/characters/ayaka/abil.go
index 8723c5376..45876d8c8 100644
--- a/internal/characters/ayaka/abil.go
+++ b/internal/characters/ayaka/abil.go
@@ -143,7 +143,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Key:    "ayaka-a1",
 		Expiry: c.Core.F + 360,
 		Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
-			return val, atk.Info.AttackTag == core.AttackTagNormal || ai.AttackTag == core.AttackTagExtra
+			return val, atk.Info.AttackTag == core.AttackTagNormal || atk.Info.AttackTag == core.AttackTagExtra
 		},
 	})
 
diff --git a/internal/characters/ayato/abil.go b/internal/characters/ayato/abil.go
index 33f1583bd..3a25b3c0d 100644
--- a/internal/characters/ayato/abil.go
+++ b/internal/characters/ayato/abil.go
@@ -150,8 +150,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 			//check if this hits first
 			target := -1
 			for i, t := range c.Core.Targets {
-				//skip for target 0 aka player
-				if i == 0 {
+				// skip non-enemy targets
+				if t.Type() != core.TargettableEnemy {
 					continue
 				}
 				if lastHit[t] < c.Core.F {
diff --git a/internal/characters/ayato/frames.go b/internal/characters/ayato/frames.go
index fa87a4b0d..197768de5 100644
--- a/internal/characters/ayato/frames.go
+++ b/internal/characters/ayato/frames.go
@@ -93,6 +93,6 @@ func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int)
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
 		return f
 	}
-	//otherise use default implementation
+	//otherwise use default implementation
 	return c.Tmpl.ActionInterruptableDelay(next, p)
 }
diff --git a/internal/characters/beidou/abil.go b/internal/characters/beidou/abil.go
index cff1dd5dc..9fe8f372e 100644
--- a/internal/characters/beidou/abil.go
+++ b/internal/characters/beidou/abil.go
@@ -21,7 +21,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       attack[c.NormalCounter][c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
 
 	c.AdvanceNormalIndex()
 
@@ -64,7 +64,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		})
 	}
 
-	c.SetCD(core.ActionSkill, 450)
+	c.SetCDWithDelay(core.ActionSkill, 450, 4)
 	return f, a
 }
 
@@ -74,7 +74,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		return 0, 0
 	}
 
-	f, a := c.ActionFrames(core.ActionSkill, p)
+	f, a := c.ActionFrames(core.ActionBurst, p)
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
 		Abil:       "Stormbreaker (Q)",
@@ -86,7 +86,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Durability: 100,
 		Mult:       burstonhit[c.TalentLvlBurst()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
 
 	c.Core.Status.AddStatus("beidouburst", 900)
 
@@ -130,8 +130,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		}, "beidou-c6", f)
 	}
 
-	c.ConsumeEnergy(11)
-	c.SetCDWithDelay(core.ActionBurst, 1200, 11)
+	c.ConsumeEnergy(6)
+	c.SetCD(core.ActionBurst, 1200)
 	return f, a
 }
 
@@ -174,10 +174,10 @@ func (c *char) burstProc() {
 
 func (c *char) chain(src int, count int) core.AttackCBFunc {
 
-	if c.Base.Cons > 1 && count == 5 {
+	if c.Base.Cons >= 2 && count == 5 {
 		return nil
 	}
-	if c.Base.Cons < 2 && count == 3 {
+	if c.Base.Cons <= 1 && count == 3 {
 		return nil
 	}
 	return func(a core.AttackCB) {
@@ -201,3 +201,13 @@ func (c *char) chain(src int, count int) core.AttackCBFunc {
 
 	}
 }
+
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
+	return f, a
+}
+
+func (c *char) Jump(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionJump, p)
+	return f, a
+}
diff --git a/internal/characters/beidou/beidou.go b/internal/characters/beidou/beidou.go
index 546905889..85db8e017 100644
--- a/internal/characters/beidou/beidou.go
+++ b/internal/characters/beidou/beidou.go
@@ -40,6 +40,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.burstProc()
 	c.a4()
diff --git a/internal/characters/beidou/frames.go b/internal/characters/beidou/frames.go
index 19cd7ddbc..473f1fe8c 100644
--- a/internal/characters/beidou/frames.go
+++ b/internal/characters/beidou/frames.go
@@ -6,25 +6,31 @@ 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:
-			f = 23 //frames from keqing lib
+			f = 23
+			a = 31
 		case 1:
-			f = 43
+			f = 22
+			a = 36
 		case 2:
-			f = 68
+			f = 45
+			a = 54
 		case 3:
-			f = 44
+			f = 25
+			a = 36
 		case 4:
-			f = 68
+			f = 43
+			a = 96
 		}
 		atkspd := c.Stat(core.AtkSpd)
 		if c.Core.Status.Duration("beidoua4") > 0 {
 			atkspd += 0.15
 		}
 		f = int(float64(f) / (1 + atkspd))
-		return f, f
+		return f, a
 	case core.ActionCharge:
 		f := 35 //frames from keqing lib
 		atkspd := c.Stat(core.AtkSpd)
@@ -34,11 +40,41 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 		f = int(float64(f) / (1 + atkspd))
 		return f, f
 	case core.ActionSkill:
-		return 41, 41 //ok
+		return 24, 44
 	case core.ActionBurst:
-		return 45, 45 //ok
+		return 28, 55
+	case core.ActionDash:
+		return 22, 22
+	case core.ActionJump:
+		return 32, 32
 	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, 31-23) //n1 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionAttack, 36-22) //n2 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionAttack, 54-45) //n3 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionAttack, 36-25) //n4 -> next attack
+	c.SetNormalCancelFrames(4, core.ActionAttack, 96-43) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 55-28) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 58-28)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 48-28)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 48-28)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 46-28)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 44-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 45-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 24-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 24-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 44-24)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/bennett/bennett.go b/internal/characters/bennett/bennett.go
index 3cbbcb356..f6b83ce49 100644
--- a/internal/characters/bennett/bennett.go
+++ b/internal/characters/bennett/bennett.go
@@ -39,6 +39,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	if c.Base.Cons >= 2 {
 		c.c2()
@@ -59,42 +60,15 @@ func (c *char) c2() {
 	})
 }
 
-func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
+func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	switch a {
-	case core.ActionAttack:
-		f := 0
-		switch c.NormalCounter {
-		//TODO: need to add atkspd mod
-		case 0:
-			f = 12 //frames from keqing lib
-		case 1:
-			f = 20 + 5 //+5 i guess recovery from n1?
-		case 2:
-			f = 31
-		case 3:
-			f = 55
-		case 4:
-			f = 49
-		}
-		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+	case core.ActionDash:
+		return 18
 	case core.ActionCharge:
-		return 100, 100 //frames from keqing lib
-	case core.ActionSkill:
-		hold := p["hold"]
-		switch hold {
-		case 1:
-			return 112, 112
-		case 2:
-			return 197, 197
-		default:
-			return 52, 52
-		}
-	case core.ActionBurst:
-		return 51, 51 //ok
+		return 20
 	default:
-		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
-		return 0, 0
+		c.Core.Log.NewEvent("ActionStam not implemented", core.LogActionEvent, c.Index, "action", a.String())
+		return 0
 	}
 }
 
@@ -112,28 +86,62 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       attack[c.NormalCounter][c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	c.AdvanceNormalIndex()
 
 	return f, a
 }
 
+func (c *char) ChargeAttack(p map[string]int) (int, int) {
+
+	delay := []int{10, 21}
+
+	f, a := c.ActionFrames(core.ActionCharge, p)
+
+	ai := core.AttackInfo{
+		ActorIndex: c.Index,
+		AttackTag:  core.AttackTagExtra,
+		ICDTag:     core.ICDTagNormalAttack,
+		ICDGroup:   core.ICDGroupDefault,
+		Element:    core.Physical,
+		Durability: 25,
+	}
+
+	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), delay[i], delay[i])
+	}
+
+	return f, a
+}
+
 func (c *char) Skill(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
 	var cd int
+	var cdDelay int
 
-	switch p["hold"] {
-	case 1:
-		c.skillHoldShort()
+	if p["hold_c4"] == 1 { //TODO: check if they actually have c4
+		c.skillHoldShort(true)
 		cd = 450 - 90
-	case 2:
-		c.skillHoldLong()
-		cd = 600 - 120
-	default:
-		c.skillPress()
-		cd = 300 - 60
+		cdDelay = 43
+	} else {
+		switch p["hold"] {
+		case 1:
+			c.skillHoldShort(false)
+			cd = 450 - 90
+			cdDelay = 43
+		case 2:
+			c.skillHoldLong()
+			cd = 600 - 120
+			cdDelay = 110
+		default:
+			c.skillPress()
+			cd = 300 - 60
+			cdDelay = 14
+		}
 	}
 
 	//A4
@@ -141,7 +149,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		cd = cd / 2
 	}
 
-	c.SetCD(core.ActionSkill, cd)
+	c.SetCDWithDelay(core.ActionSkill, cd, cdDelay)
 
 	return f, a
 
@@ -159,7 +167,7 @@ func (c *char) skillPress() {
 		Durability: 50,
 		Mult:       skill[c.TalentLvlSkill()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 15, 15)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 16, 16)
 
 	//25 % chance of 3 orbs
 	count := 2
@@ -169,9 +177,9 @@ func (c *char) skillPress() {
 	c.QueueParticle("bennett", count, core.Pyro, 120)
 }
 
-func (c *char) skillHoldShort() {
+func (c *char) skillHoldShort(c4Active bool) {
 
-	delay := []int{89, 115}
+	delay := []int{45, 57}
 
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -188,18 +196,19 @@ func (c *char) skillHoldShort() {
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), delay[i], delay[i])
 	}
 
-	//25 % chance of 3 orbs
-	count := 2
-	if c.Core.Rand.Float64() < .25 {
-		count++
+	if c4Active { //user-specified c4 variant adds an additional attack that deals 135% of the second hit
+		ai.Mult = skill1[1][c.TalentLvlSkill()] * 1.35
+		ai.Abil = "Passion Overload (C4)"
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 94, 94)
 	}
-	c.QueueParticle("bennett", count, core.Pyro, 215)
+
+	//Bennett Hold E is guaranteed 3 orbs
+	c.QueueParticle("bennett", 3, core.Pyro, 215)
 }
 
 func (c *char) skillHoldLong() {
-	//i think explode is guaranteed 3 orbs
 
-	delay := []int{136, 154}
+	delay := []int{112, 121}
 
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -217,18 +226,14 @@ func (c *char) skillHoldLong() {
 	}
 
 	ai.Mult = explosion[c.TalentLvlSkill()]
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 198, 198)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 166, 166)
 
-	//25 % chance of 3 orbs
-	count := 2
-	if c.Core.Rand.Float64() < .25 {
-		count++
-	}
-	c.QueueParticle("bennett", count, core.Pyro, 298)
+	//Bennett Hold E is guaranteed 3 orbs
+	c.QueueParticle("bennett", 3, core.Pyro, 298)
 
 }
 
-const burstStartFrame = 31
+const burstStartFrame = 34
 
 func (c *char) Burst(p map[string]int) (int, int) {
 
@@ -249,7 +254,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Mult:       burst[c.TalentLvlBurst()],
 	}
 	//TODO: review bennett AOE size
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 33, 33)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 37, 37)
 	stats, _ := c.SnapshotStats()
 
 	//apply right away
@@ -262,9 +267,9 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		c.AddTask(c.applyBennettField(stats), "bennett-field", i)
 	}
 
-	c.ConsumeEnergy(42)
-	c.SetCDWithDelay(core.ActionBurst, 900, 42)
-	return f, a //todo fix field cast time
+	c.ConsumeEnergy(36)
+	c.SetCDWithDelay(core.ActionBurst, 900, 34)
+	return f, a
 }
 
 const bennettSelfInfusionDurationInFrames = 126
diff --git a/internal/characters/bennett/frames.go b/internal/characters/bennett/frames.go
new file mode 100644
index 000000000..e38c2f186
--- /dev/null
+++ b/internal/characters/bennett/frames.go
@@ -0,0 +1,178 @@
+package bennett
+
+import "github.com/genshinsim/gcsim/pkg/core"
+
+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:
+			f = 13
+			a = 20
+		case 1:
+			f = 9
+			a = 17
+		case 2:
+			f = 13
+			a = 37
+		case 3:
+			f = 25
+			a = 44
+		case 4:
+			f = 24
+			a = 60
+		}
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+		return f, a
+	case core.ActionCharge:
+		return 22, 55
+	case core.ActionSkill:
+		holdc4 := p["hold_c4"]
+		if holdc4 == 1 {
+			return 95, 107
+		}
+
+		hold := p["hold"]
+		switch hold {
+		case 1:
+			return 57, 98
+		case 2:
+			return 166, 175
+		default:
+			return 16, 42
+		}
+	case core.ActionBurst:
+		return 37, 53
+	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, 20-13) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 33-13) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 17-9) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 27-9) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 37-13) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 46-13) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 44-25) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 48-25) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 60-24) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 55-22) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 41-22)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 41-22)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 44-22)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 53-37) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 53-37)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 49-37)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 50-37)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 51-37)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 42-16)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 42-16)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 22-16)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 23-16)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 41-16)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Bennett's Hold E varieties
+	if c.Core.LastAction.Typ == core.ActionSkill {
+		//depends on which type of hold E was used
+		hc4 := c.Core.LastAction.Param["hold_c4"]
+		h := c.Core.LastAction.Param["hold"]
+
+		//override for c4 hold tech
+		if hc4 == 1 {
+			return SkillHoldC4Frames(next)
+		}
+
+		switch h { //determine cancel frames based on which Hold E was used
+		case 1:
+			return SkillHoldLevel1Frames(next)
+		case 2:
+			if c.ModIsActive("bennett-field") { //Charge Level 2 has different cancel frames while in burst
+				return SkillHoldLevel2FramesA4(next)
+			}
+			return SkillHoldLevel2Frames(next)
+		default:
+			return c.Tmpl.ActionInterruptableDelay(next, p)
+		}
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func SkillHoldC4Frames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 107 - 95
+	case core.ActionBurst:
+		return 107 - 95
+	case core.ActionSwap:
+		return 106 - 95
+	default:
+		return 95 - 95
+	}
+}
+
+func SkillHoldLevel1Frames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 98 - 57
+	case core.ActionBurst:
+		return 97 - 57
+	case core.ActionDash:
+		return 65 - 57
+	case core.ActionJump:
+		return 66 - 57
+	case core.ActionSwap:
+		return 96 - 57
+	default:
+		return 57 - 57
+	}
+}
+
+func SkillHoldLevel2Frames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 343 - 166
+	case core.ActionBurst:
+		return 339 - 166
+	case core.ActionDash:
+		return 231 - 166
+	case core.ActionJump:
+		return 340 - 166
+	case core.ActionSwap:
+		return 337 - 166
+	default:
+		return 340 - 166
+	}
+}
+
+func SkillHoldLevel2FramesA4(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 175 - 166
+	case core.ActionDash:
+		return 171 - 166
+	case core.ActionJump:
+		return 174 - 166
+	case core.ActionSwap:
+		return 175 - 166
+	default:
+		return 175 - 166
+	}
+}
diff --git a/internal/characters/bennett/stats.go b/internal/characters/bennett/stats.go
index 4c5fd8007..70131ba97 100644
--- a/internal/characters/bennett/stats.go
+++ b/internal/characters/bennett/stats.go
@@ -176,6 +176,34 @@ var (
 		1.26,
 		1.33,
 	}
+	charge = [][]float64{
+		{
+			.559,
+			.6045,
+			.65,
+			.715,
+			.7605,
+			.8125,
+			.884,
+			.9555,
+			1.027,
+			1.105,
+			1.183,
+		},
+		{
+			.6072,
+			.6566,
+			.706,
+			.7766,
+			.826,
+			.8825,
+			.9602,
+			1.0378,
+			1.1155,
+			1.2002,
+			1.2849,
+		},
+	}
 
 	attack = [][]float64{
 		{
diff --git a/internal/characters/diluc/abil.go b/internal/characters/diluc/abil.go
index e6b5db2e1..bccdbc9d4 100644
--- a/internal/characters/diluc/abil.go
+++ b/internal/characters/diluc/abil.go
@@ -22,7 +22,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		Mult:       attack[c.NormalCounter][c.TalentLvlAttack()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
 	c.AdvanceNormalIndex()
 
 	return f, a
@@ -30,13 +30,12 @@ func (c *char) Attack(p map[string]int) (int, int) {
 
 func (c *char) Skill(p map[string]int) (int, int) {
 
-	f, a := c.ActionFrames(core.ActionSkill, p)
-
-	if c.eCounter == 0 {
-		c.eStarted = true
-		c.eStartFrame = c.Core.F
+	// reset counter
+	if c.Core.F >= c.eWindow {
+		c.eCounter = 0
 	}
-	c.eLastUse = c.Core.F
+
+	f, a := c.ActionFrames(core.ActionSkill, p)
 
 	orb := 1
 	if c.Core.Rand.Float64() < 0.33 {
@@ -51,7 +50,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
-		Abil:       "Searing Onslaught",
+		Abil:       fmt.Sprintf("Searing Onslaught %v", c.eCounter),
 		AttackTag:  core.AttackTagElementalArt,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
@@ -61,7 +60,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Mult:       skill[c.eCounter][c.TalentLvlSkill()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f-5, f-5)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f, f)
 
 	//add a timer to activate c4
 	if c.Base.Cons >= 4 {
@@ -70,20 +69,21 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		}, "dilucc4", f+120) // 2seconds after cast
 	}
 
-	//skill only goes on cd once all 3 charges have been used
-	//or if 4 second passed since last use, skill will also go on cd
+	// allow skill to be used again if 4s hasn't passed since last use
+	c.eWindow = c.Core.F + 60*4
 
 	c.eCounter++
-	if c.eCounter == 3 {
-		//ability can go on cd now
-		cd := 600 - (c.Core.F - c.eStartFrame)
-		c.Core.Log.NewEvent("diluc skill going on cd", core.LogCharacterEvent, c.Index, "duration", cd)
-		c.SetCD(core.ActionSkill, cd)
-		c.eStarted = false
-		c.eStartFrame = -1
-		c.eLastUse = -1
+	switch c.eCounter {
+	case 1:
+		// TODO: cd delay?
+		// set cd on first use
+		c.SetCD(core.ActionSkill, 10*60)
+	case 3:
+		// reset window since we're at 3rd use
+		c.eWindow = -1
 		c.eCounter = 0
 	}
+
 	//return animation cd
 	//this also depends on which hit in the chain this is
 	return f, a
@@ -155,9 +155,15 @@ func (c *char) Burst(p map[string]int) (int, int) {
 			ai.Mult = burstExplode[c.TalentLvlBurst()]
 			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 0, 110)
 		}
-	}, "diluc-burst", 100)
+	}, "diluc-burst", f-1)
+
+	c.ConsumeEnergy(21)
+	c.SetCDWithDelay(core.ActionBurst, 720, 14)
+	return f, a
+}
 
-	c.ConsumeEnergy(24)
-	c.SetCDWithDelay(core.ActionBurst, 720, 24)
+//Diluc dash is 19 frames
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
 	return f, a
 }
diff --git a/internal/characters/diluc/diluc.go b/internal/characters/diluc/diluc.go
index 5cb95bd08..3540162de 100644
--- a/internal/characters/diluc/diluc.go
+++ b/internal/characters/diluc/diluc.go
@@ -11,10 +11,8 @@ func init() {
 
 type char struct {
 	*character.Tmpl
-	eStarted    bool
-	eStartFrame int
-	eLastUse    int
-	eCounter    int
+	eCounter int
+	eWindow  int
 }
 
 func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
@@ -35,11 +33,15 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.Weapon.Class = core.WeaponClassClaymore
 	c.NormalHitNum = 4
 
+	c.eCounter = 0
+	c.eWindow = -1
+
 	return &c, nil
 }
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	if c.Base.Cons >= 1 && c.Core.Flags.DamageMode {
 		c.c1()
@@ -52,15 +54,23 @@ func (c *char) Init() {
 	}
 }
 
+func (c *char) ActionReady(a core.ActionType, p map[string]int) bool {
+	// check if it is possible to use next skill
+	if a == core.ActionSkill && c.Core.F < c.eWindow {
+		return true
+	}
+	return c.Tmpl.ActionReady(a, p)
+}
+
 func (c *char) c1() {
+	m := make([]float64, core.EndStatType)
+	m[core.DmgP] = 0.15
 	c.AddPreDamageMod(core.PreDamageMod{
 		Key:    "diluc-c1",
 		Expiry: -1,
 		Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
-			val := make([]float64, core.EndStatType)
 			if t.HP()/t.MaxHP() > 0.5 {
-				val[core.DmgP] = 0.15
-				return val, true
+				return m, true
 			}
 			return nil, false
 		},
@@ -68,6 +78,7 @@ func (c *char) c1() {
 }
 
 func (c *char) c2() {
+	m := make([]float64, core.EndStatType)
 	stack := 0
 	last := 0
 	c.Core.Events.Subscribe(core.OnCharacterHurt, func(args ...interface{}) bool {
@@ -82,51 +93,31 @@ func (c *char) c2() {
 		if stack > 3 {
 			stack = 3
 		}
-		val := make([]float64, core.EndStatType)
-		val[core.ATKP] = 0.1 * float64(stack)
-		val[core.AtkSpd] = 0.05 * float64(stack)
+		m[core.ATKP] = 0.1 * float64(stack)
+		m[core.AtkSpd] = 0.05 * float64(stack)
 		c.AddMod(core.CharStatMod{
 			Key:    "diluc-c2",
-			Amount: func() ([]float64, bool) { return val, true },
+			Amount: func() ([]float64, bool) { return m, true },
 			Expiry: c.Core.F + 600,
 		})
 		return false
 	}, "diluc-c2")
-
 }
 
 func (c *char) c4() {
+	m := make([]float64, core.EndStatType)
+	m[core.DmgP] = 0.4
 	c.AddMod(core.CharStatMod{
 		Key:    "diluc-c4",
 		Expiry: -1,
 		Amount: func() ([]float64, bool) {
-			val := make([]float64, core.EndStatType)
 			if c.Core.Status.Duration("dilucc4") > 0 {
-				val[core.DmgP] = 0.4
-				return val, true
+				return m, true
 			}
 			return nil, false
 		},
 	})
 }
-func (c *char) Tick() {
-	c.Tmpl.Tick()
-
-	if c.eStarted {
-		//check if 4 second has passed since last use
-		if c.Core.F-c.eLastUse >= 240 {
-			//if so, set ability to be on cd equal to 10s less started
-			cd := 600 - (c.Core.F - c.eStartFrame)
-			c.Core.Log.NewEvent("diluc skill going on cd", core.LogCharacterEvent, c.Index, "duration", cd, "last", c.eLastUse)
-			c.SetCD(core.ActionSkill, cd)
-			//reset
-			c.eStarted = false
-			c.eStartFrame = -1
-			c.eLastUse = -1
-			c.eCounter = 0
-		}
-	}
-}
 
 func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	switch a {
@@ -139,5 +130,4 @@ func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 		c.Core.Log.NewEvent("ActionStam not implemented", core.LogActionEvent, c.Index, "action", a.String())
 		return 0
 	}
-
 }
diff --git a/internal/characters/diluc/frames.go b/internal/characters/diluc/frames.go
index 099ccdd63..9e3eb17ec 100644
--- a/internal/characters/diluc/frames.go
+++ b/internal/characters/diluc/frames.go
@@ -6,32 +6,117 @@ 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:
-			f = 24 //frames from keqing lib
+			f = 24
+			a = 32
 		case 1:
-			f = 77 - 24
+			f = 39
+			a = 46
 		case 2:
-			f = 115 - 77
+			f = 26
+			a = 34
 		case 3:
-			f = 181 - 115
+			f = 49
+			a = 99
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionSkill:
 		switch c.eCounter {
 		case 1:
-			return 52, 52
+			return 29, 38
 		case 2:
-			return 81, 81
+			return 47, 58
 		default:
-			return 45, 45
+			return 25, 32
 		}
 	case core.ActionBurst:
-		return 145, 145
+		return 101, 140
+	case core.ActionDash:
+		return 19, 19
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+
+	//normal cancels, missing Nx->CA
+	c.SetNormalCancelFrames(0, core.ActionAttack, 32-24) //n1 -> next attack
+	//c.SetNormalCancelFrames(0, core.ActionCharge, 33-13) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 46-39) //n2 -> next attack
+	//c.SetNormalCancelFrames(1, core.ActionCharge, 27-9) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 34-26) //n3 -> next attack
+	//c.SetNormalCancelFrames(2, core.ActionCharge, 46-13) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 99-49) //n4 -> n1
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 140-101) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 139-101)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 139-101)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 141-101)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 138-101)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 32-25)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 31-25)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 32-25)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 30-25)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Diluc's E
+	if c.Core.LastAction.Typ == core.ActionSkill {
+		switch c.eCounter { //depends on how many E's have been used
+		case 1: //1 E - use default implementation
+			return c.Tmpl.ActionInterruptableDelay(next, p)
+		case 2: //2 Es
+			return SecondSkillFrames(next)
+		default: //3 Es
+			return ThirdSkillFrames(next)
+		}
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func SecondSkillFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 38 - 29
+	case core.ActionSkill:
+		return 37 - 29
+	case core.ActionBurst:
+		return 37 - 29
+	case core.ActionDash:
+		return 29 - 29
+	case core.ActionJump:
+		return 31 - 29
+	case core.ActionSwap:
+		return 36 - 29
+	default:
+		return 38 - 29
+	}
+}
+
+func ThirdSkillFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 58 - 47
+	case core.ActionBurst:
+		return 57 - 47
+	case core.ActionDash:
+		return 47 - 47
+	case core.ActionJump:
+		return 48 - 47
+	case core.ActionSwap:
+		return 66 - 47
+	default:
+		return 58 - 47
+	}
+}
diff --git a/internal/characters/eula/abil.go b/internal/characters/eula/abil.go
index a1264c151..7ed53a7cf 100644
--- a/internal/characters/eula/abil.go
+++ b/internal/characters/eula/abil.go
@@ -6,7 +6,13 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
-var delay = [][]int{{11}, {25}, {36, 49}, {33}, {45, 63}}
+var hitmarks = [][]int{
+	{30},     // n1
+	{19},     // n2
+	{25, 42}, // n3
+	{17},     // n4
+	{29, 56}, // n5
+}
 
 func (c *char) Attack(p map[string]int) (int, int) {
 	//register action depending on number in chain
@@ -29,7 +35,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 
 	for i, mult := range auto[c.NormalCounter] {
 		ai.Mult = mult[c.TalentLvlAttack()]
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), delay[c.NormalCounter][i], delay[c.NormalCounter][i])
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -40,16 +46,15 @@ func (c *char) Attack(p map[string]int) (int, int) {
 }
 
 func (c *char) Skill(p map[string]int) (int, int) {
-	f, a := c.ActionFrames(core.ActionSkill, p)
-	if p["hold"] == 0 {
-		c.pressE()
-		return f, a
+	if p["hold"] != 0 {
+		return c.holdSkill(p)
 	}
-	c.holdE()
-	return f, a
+	return c.pressSkill(p)
 }
 
-func (c *char) pressE() {
+func (c *char) pressSkill(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionSkill, p)
+
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
 		Abil:       "Icetide Vortex",
@@ -74,18 +79,23 @@ func (c *char) pressE() {
 		}
 		c.grimheartReset = 18 * 60
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 35, cb)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 20, 20, cb)
 
 	n := 1
 	if c.Core.Rand.Float64() < .5 {
 		n = 2
 	}
-	c.QueueParticle("eula", n, core.Cryo, 100)
+	c.QueueParticle("eula", n, core.Cryo, 20+100)
 
-	c.SetCD(core.ActionSkill, 240)
+	c.SetCDWithDelay(core.ActionSkill, 60*4, 16)
+	return f, a
 }
 
-func (c *char) holdE() {
+var icewhirlDelay = []int{79, 92}
+
+func (c *char) holdSkill(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionSkill, p)
+
 	//hold e
 	//296 to 341, but cd starts at 322
 	//60 fps = 108 frames cast, cd starts 62 frames in so need to + 62 frames to cd
@@ -101,10 +111,12 @@ func (c *char) holdE() {
 		Durability: 25,
 		Mult:       skillHold[lvl],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 80)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 49, 49)
 
 	//multiple brand hits
 	ai.Abil = "Icetide Vortex (Icewhirl)"
+	ai.ICDTag = core.ICDTagElementalArt
+	ai.StrikeType = core.StrikeTypeDefault
 	ai.Mult = icewhirl[lvl]
 
 	v := c.Tags["grimheart"]
@@ -130,9 +142,14 @@ func (c *char) holdE() {
 			done = true
 		}
 	}
+
+	// this shouldn't happen, but to be safe
+	if v > 2 {
+		v = 2
+	}
 	for i := 0; i < v; i++ {
 		//spacing it out for stacks
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 92+i*7, shredCB)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), icewhirlDelay[i], icewhirlDelay[i], shredCB)
 	}
 
 	//A1
@@ -148,14 +165,14 @@ func (c *char) holdE() {
 			Durability: 25,
 			Mult:       burstExplodeBase[c.TalentLvlBurst()] * 0.5,
 		}
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, 108)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 108, 108)
 	}
 
 	n := 2
 	if c.Core.Rand.Float64() < .5 {
 		n = 3
 	}
-	c.QueueParticle("eula", n, core.Cryo, 100)
+	c.QueueParticle("eula", n, core.Cryo, 49+100)
 
 	//c1 add debuff
 	if c.Base.Cons >= 1 && v > 0 {
@@ -175,14 +192,15 @@ func (c *char) holdE() {
 	if c.Base.Cons >= 2 {
 		cd = 4 //press and hold have same cd TODO: check if this is right
 	}
-	c.SetCD(core.ActionSkill, cd*60+62)
+	c.SetCDWithDelay(core.ActionSkill, cd*60, 46)
+	return f, a
 }
 
 //ult 365 to 415, 60fps = 120
 //looks like ult charges for 8 seconds
 func (c *char) Burst(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionBurst, p)
-	c.Core.Status.AddStatus("eulaq", 7*60+f+1)
+	c.Core.Status.AddStatus("eulaq", 9*60+30) // lights up 9.5s from cast
 
 	c.burstCounter = 0
 	if c.Base.Cons == 6 {
@@ -204,7 +222,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Durability: 50,
 		Mult:       burstInitial[lvl],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 0, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), 100, 100)
 
 	//add 1 stack to Grimheart
 	v := c.Tags["grimheart"]
@@ -219,9 +237,9 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		if c.Core.Status.Duration("eulaq") > 0 {
 			c.triggerBurst()
 		}
-	}, "Eula-Burst-Lightfall", 7*60+f) //after 8 seconds
+	}, "Eula-Burst-Lightfall", 600-35) // hitmark is 600f from cast
 
-	c.SetCDWithDelay(core.ActionBurst, 20*60, 107)
+	c.SetCDWithDelay(core.ActionBurst, 20*60, 97)
 	//energy does not deplete until after animation
 	c.ConsumeEnergy(107)
 
@@ -248,7 +266,7 @@ func (c *char) triggerBurst() {
 
 	c.Core.Log.NewEvent("eula burst triggering", core.LogCharacterEvent, c.Index, "stacks", stacks, "mult", ai.Mult)
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 23, 23)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 35, 35)
 	c.Core.Status.DeleteStatus("eulaq")
 	c.burstCounter = 0
 }
diff --git a/internal/characters/eula/eula.go b/internal/characters/eula/eula.go
index 90025254b..8bad463c2 100644
--- a/internal/characters/eula/eula.go
+++ b/internal/characters/eula/eula.go
@@ -42,6 +42,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.a4()
 	c.burstStacks()
diff --git a/internal/characters/eula/frames.go b/internal/characters/eula/frames.go
index 288259064..b1f6b17fa 100644
--- a/internal/characters/eula/frames.go
+++ b/internal/characters/eula/frames.go
@@ -5,33 +5,61 @@ import "github.com/genshinsim/gcsim/pkg/core"
 func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
-		f := 0
+		var f, a int
 		switch c.NormalCounter {
 		//TODO: need to add atkspd mod
 		case 0:
-			f = 29
+			f = 30
+			a = 34
 		case 1:
-			f = 25
+			f = 19
+			a = 36
 		case 2:
-			f = 65
+			f = 42
+			a = 56
 		case 3:
-			f = 33
+			f = 17
+			a = 44
 		case 4:
-			f = 88
+			f = 56
+			a = 106
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionCharge:
 		return 35, 35 //TODO: no idea
 	case core.ActionSkill:
-		if p["hold"] == 0 {
-			return 34, 34
+		if p["hold"] != 0 {
+			return 76, 100
 		}
-		return 80, 80
+		return 30, 48
 	case core.ActionBurst:
-		return 116, 116 //ok
+		return 117, 122
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+	// NA/CA cancels. TODO: CA cancels?
+	c.SetNormalCancelFrames(0, core.ActionAttack, 34-30-1)  // n1 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionAttack, 36-19-1)  // n2 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionAttack, 56-42-1)  // n3 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionAttack, 44-17-1)  // n4 -> next attack
+	c.SetNormalCancelFrames(4, core.ActionAttack, 106-56-1) // n5 -> next attack
+
+	// skill -> x
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionWalk, 48-30)
+
+	// skill (hold) -> x
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionSwap, 100-76)
+
+	// burst -> x
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 122-117)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionCharge, 122-117) // uses n1 frames
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 122-117)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 122-117)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 122-117)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 122-117)
+}
diff --git a/internal/characters/fischl/fischl.go b/internal/characters/fischl/fischl.go
index 0c00008d2..8216c918b 100644
--- a/internal/characters/fischl/fischl.go
+++ b/internal/characters/fischl/fischl.go
@@ -46,6 +46,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.a4()
 
@@ -116,6 +117,33 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	return f, a
 }
 
+func (c *char) Aimed(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionAim, p)
+
+	travel, ok := p["travel"]
+	if !ok {
+		travel = 10
+	}
+	weakspot, ok := p["weakspot"]
+
+	ai := core.AttackInfo{
+		ActorIndex:   c.Index,
+		Abil:         "Charge Attack",
+		AttackTag:    core.AttackTagExtra,
+		ICDTag:       core.ICDTagNone,
+		ICDGroup:     core.ICDGroupDefault,
+		StrikeType:   core.StrikeTypePierce,
+		Element:      core.Electro,
+		Durability:   25,
+		Mult:         aim[c.TalentLvlAttack()],
+		HitWeakPoint: weakspot == 1,
+	}
+
+	c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), f, f+travel)
+
+	return f, a
+}
+
 func (c *char) queueOz(src string) {
 
 	dur := 600
@@ -174,6 +202,7 @@ func (c *char) ozTick(src int) func() {
 }
 
 func (c *char) Skill(p map[string]int) (int, int) {
+	const ozSpawn = 32
 	f, a := c.ActionFrames(core.ActionSkill, p)
 	//always trigger electro no ICD on initial summon
 	ai := core.AttackInfo{
@@ -191,15 +220,15 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	if c.Base.Cons >= 2 {
 		ai.Mult += 2
 	}
-	// Should be aligned with Oz's skill snapshot timing
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f-21, f-20)
+	//hitmark is 5 frames after oz spawns
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), ozSpawn, ozSpawn+5)
 
 	//set on field oz to be this one
 	c.AddTask(func() {
 		c.queueOz("Skill")
-	}, "oz-skill", f-21)
+	}, "oz-skill", ozSpawn)
 
-	c.SetCD(core.ActionSkill, 25*60)
+	c.SetCD(core.ActionSkill, 25*60+18) //18 frames until CD starts
 	//return animation cd
 	return f, a
 }
@@ -208,10 +237,11 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionBurst, p)
 
 	//set on field oz to be this one
-	// According to snapshot timings sheet, Oz snapshots after swapping out during ult animation
+	//TODO: Oz should spawn and snapshot when the burst animation is cancelled
+	//for now, the common burst->swap combo (24 frames) is used.
 	c.AddTask(func() {
 		c.queueOz("Burst")
-	}, "oz-skill", f)
+	}, "oz-skill", 24)
 
 	//initial damage; part of the burst tag
 	ai := core.AttackInfo{
@@ -225,7 +255,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       burst[c.TalentLvlBurst()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 2, 2)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
 
 	//check for C4 damage
 	if c.Base.Cons >= 4 {
@@ -240,9 +270,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 			Durability: 50,
 			Mult:       2.22,
 		}
-		// C4 damage always occurs before burst damage according to TCL.
-		// TODO: No frames given, assumed to just be a 1 frame difference
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 1, 1)
+		// C4 damage always occurs before burst damage.
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 8, 8)
 		//heal at end of animation
 		heal := c.MaxHP() * 0.2
 		c.AddTask(func() {
@@ -253,11 +282,11 @@ func (c *char) Burst(p map[string]int) (int, int) {
 				Src:     heal,
 				Bonus:   c.Stat(core.Heal),
 			})
-		}, "c4heal", f-1)
+		}, "c4heal", f)
 
 	}
 
-	c.ConsumeEnergy(11)
-	c.SetCDWithDelay(core.ActionBurst, 15*60, 11)
+	c.ConsumeEnergy(6)
+	c.SetCD(core.ActionBurst, 15*60)
 	return f, a
 }
diff --git a/internal/characters/fischl/frames.go b/internal/characters/fischl/frames.go
index 46e86b34c..6a15fa69b 100644
--- a/internal/characters/fischl/frames.go
+++ b/internal/characters/fischl/frames.go
@@ -6,29 +6,77 @@ 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:
-			f = 10 //frames from keqing lib
+			f = 15
+			a = 25
 		case 1:
-			f = 28 - 10
+			f = 11
+			a = 22
 		case 2:
-			f = 61 - 28
+			f = 24
+			a = 38
 		case 3:
-			f = 102 - 61
+			f = 26
+			a = 32
 		case 4:
-			f = 131 - 102
+			f = 21
+			a = 67
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionAim:
-		return 95, 95
+		return 86, 96
 	case core.ActionSkill:
-		return 40, 40
+		return 14, 43
 	case core.ActionBurst:
-		return 21, 21 //TODO: this is swap cancelling
+		return 18, 115
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+
+	//normal cancels (missing Nx -> Aim)
+	c.SetNormalCancelFrames(0, core.ActionAttack, 25-15) //n1 -> next attack
+	//c.SetNormalCancelFrames(0, core.ActionAim, 33-13) //n1 -> aim
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 22-11) //n2 -> next attack
+	//c.SetNormalCancelFrames(1, core.ActionAim, 27-9) //n2 -> aim
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 38-24) //n3 -> next attack
+	//c.SetNormalCancelFrames(2, core.ActionAim, 46-13) //n3 -> aim
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 32-26) //n4 -> next attack
+	//c.SetNormalCancelFrames(3, core.ActionAim, 48-25) //n4 -> aim
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 67-21) //n5 -> n1
+
+	//aim cancel frames are currently generic, should record specific cancels for each one at some point
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAttack, 96-86) //aim -> n1
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSkill, 96-86)  //aim -> skill
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionBurst, 96-86)  //aim -> burst
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSwap, 96-86)   //aim -> swap
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAim, 96-86)    //aim -> aim
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 148-18) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAim, 148-18)    //burst -> aim (assumed)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 111-18)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 115-18)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 24-18)    //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 43-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAim, 43-14) //assumed
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 43-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 14-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 16-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 42-14)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/fischl/stats.go b/internal/characters/fischl/stats.go
index 455dca06b..520573a39 100644
--- a/internal/characters/fischl/stats.go
+++ b/internal/characters/fischl/stats.go
@@ -52,6 +52,19 @@ var (
 		4.68,
 		4.94,
 	}
+	aim = []float64{
+		1.24,
+		1.333,
+		1.426,
+		1.55,
+		1.643,
+		1.736,
+		1.86,
+		1.984,
+		2.108,
+		2.232,
+		2.356,
+	}
 	auto = [][]float64{
 		{
 			0.4412,
diff --git a/internal/characters/ganyu/abil.go b/internal/characters/ganyu/abil.go
index 1d475ebb2..66ff2448d 100644
--- a/internal/characters/ganyu/abil.go
+++ b/internal/characters/ganyu/abil.go
@@ -41,7 +41,7 @@ func (c *char) Aimed(p map[string]int) (int, int) {
 	}
 	bloom, ok := p["bloom"]
 	if !ok {
-		bloom = 20
+		bloom = 24
 	}
 	weakspot, ok := p["weakspot"]
 
@@ -98,12 +98,12 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	snap := c.Snapshot(&ai)
 	//flower damage immediately
-	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(2, false, core.TargettableEnemy), 30)
+	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(2, false, core.TargettableEnemy), 13)
 	//we get the orbs right away
 	c.QueueParticle("ganyu", 2, core.Cryo, 90)
 
 	//flower damage is after 6 seconds
-	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(2, false, core.TargettableEnemy), 360)
+	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(2, false, core.TargettableEnemy), 373)
 	// TODO: Particle flight time is 60s?
 	c.QueueParticle("ganyu", 2, core.Cryo, 420)
 
@@ -114,7 +114,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		c.Core.Status.AddStatus("ganyuc6", 1800)
 	}
 
-	c.SetCD(core.ActionSkill, 600)
+	c.SetCDWithDelay(core.ActionSkill, 600, 10)
 
 	return f, a
 }
@@ -135,7 +135,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	}
 	snap := c.Snapshot(&ai)
 
-	c.Core.Status.AddStatus("ganyuburst", 15*60)
+	c.Core.Status.AddStatus("ganyuburst", 15*60+130)
 
 	rad, ok := p["radius"]
 	if !ok {
@@ -148,13 +148,14 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	lastHit := make(map[core.Target]int)
 	// ccc := 0
 	//tick every .3 sec, every fifth hit is targetted i.e. 1, 0, 0, 0, 0, 1
-	for delay := 0; delay < 900; delay += 18 {
+	//first hit at 148
+	for delay := a; delay < 900+a; delay += 18 {
 		c.AddTask(func() {
 			//check if this hits first
 			target := -1
 			for i, t := range c.Core.Targets {
-				//skip for target 0 aka player
-				if i == 0 {
+				// skip non-enemy targets
+				if t.Type() != core.TargettableEnemy {
 					continue
 				}
 				if lastHit[t] < c.Core.F {
@@ -172,7 +173,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 			}
 			//deal dmg
 			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(9, false, core.TargettableEnemy), 0)
-		}, "ganyu-q", delay+f)
+		}, "ganyu-q", delay)
 
 	}
 	// c.AddTask(func() {
@@ -182,49 +183,61 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	//a4 every .3 seconds for the duration of the burst, add ice dmg up to active char for 1sec
 	//duration is 15 seconds
 	//starts from end of cast
-	for i := f; i < 900+f; i += 18 {
+	mA4 := make([]float64, core.EndStatType)
+	mC4 := make([]float64, core.EndStatType)
+	mA4[core.CryoP] = 0.2
+	for i := a; i < 900+a; i += 18 {
 		t := i
 		c.AddTask(func() {
 			active := c.Core.Chars[c.Core.ActiveChar]
-			val := make([]float64, core.EndStatType)
-			val[core.CryoP] = 0.2
 			active.AddMod(core.CharStatMod{
-				Key: "ganyu-field",
+				Key:    "ganyu-field",
+				Expiry: c.Core.F + 60,
 				Amount: func() ([]float64, bool) {
-					return val, true
+					return mA4, true
 				},
-				Expiry: c.Core.F + 60,
 			})
-			if t >= 900-18 {
+			if t >= 900+a-18 {
 				c.Core.Log.NewEvent("a4 last tick", core.LogCharacterEvent, c.Index, "ends_on", c.Core.F+60)
 			}
-		}, "ganyu-a4", i)
-	}
 
-	if c.Base.Cons >= 4 {
-		//we just assume this lasts for the full duration since no one moves...
-		start := c.Core.F
-
-		val := make([]float64, core.EndStatType)
-		c.AddMod(core.CharStatMod{
-			Key:    "ganyu-c4",
-			Expiry: c.Core.F + 1080,
-			Amount: func() ([]float64, bool) {
-				elapsed := c.Core.F - start
-				stacks := int(elapsed / 180)
-				if stacks > 5 {
-					stacks = 5
+			// C4: similar to A4 expect it lingers for 3s
+			// assume this lasts for the full duration since no one moves...
+			if c.Base.Cons >= 4 {
+				// check for 1st tick and reset stacks if expired
+				if t == a && !c.PreDamageModIsActive("ganyu-c4") {
+					c.c4Stacks = 0
+				}
+
+				// increase stacks at 3s interval
+				if (t-a)%180 == 0 {
+					c.c4Stacks++
+					if c.c4Stacks > 5 {
+						c.c4Stacks = 5
+					}
+					mC4[core.DmgP] = float64(c.c4Stacks) * 0.05
 				}
-				val[core.DmgP] = float64(stacks) * 0.05
-				return val, true
-			},
-		})
+
+				// TODO: should be changed to target mod
+				for _, char := range c.Core.Chars {
+					char.AddPreDamageMod(core.PreDamageMod{
+						Key:    "ganyu-c4",
+						Expiry: c.Core.F + 60*3,
+						Amount: func(atk *core.AttackEvent, t core.Target) ([]float64, bool) {
+							return mC4, true
+						},
+					})
+				}
+
+				c.Core.Log.NewEvent("ganyu c4 stacks", core.LogCharacterEvent, c.Index, "stacks", c.c4Stacks)
+			}
+		}, "ganyu-burst-checks", i)
 	}
 
 	//add cooldown to sim
-	c.SetCDWithDelay(core.ActionBurst, 15*60, 8)
+	c.SetCD(core.ActionBurst, 15*60)
 	//use up energy
-	c.ConsumeEnergy(8)
+	c.ConsumeEnergy(3)
 
 	return f, a
 }
diff --git a/internal/characters/ganyu/frames.go b/internal/characters/ganyu/frames.go
index 57f58db92..8a0d3f6d3 100644
--- a/internal/characters/ganyu/frames.go
+++ b/internal/characters/ganyu/frames.go
@@ -6,23 +6,30 @@ 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:
-			f = 18 //frames from keqing lib
+			f = 13
+			a = 19
 		case 1:
-			f = 43 - 18
+			f = 14
+			a = 27
 		case 2:
-			f = 73 - 43
+			f = 20
+			a = 38
 		case 3:
-			f = 117 - 73
+			f = 26
+			a = 37
 		case 4:
-			f = 153 - 117
+			f = 21
+			a = 28
 		case 5:
-			f = 190 - 153
+			f = 22
+			a = 59
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionAim:
 		//check for c6, if active then return 10, otherwise 115
 		if c.Core.Status.Duration("ganyuc6") > 0 {
@@ -30,13 +37,60 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 			c.Core.Status.DeleteStatus("ganyuc6")
 			return 10, 10
 		}
-		return 115, 115 //frames from keqing lib
+		return 103, 113
 	case core.ActionSkill:
-		return 30, 30 //ok
+		return 13, 28 //ok
 	case core.ActionBurst:
-		return 122, 122 //ok
+		return 122, 130 //ok
 	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, 19-13) //n1 -> next attack
+	//c.SetNormalCancelFrames(0, core.Actionaim, 35-10) //n1 -> aim
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 27-14) //n2 -> next attack
+	//c.SetNormalCancelFrames(1, core.Actionaim, 29-13) //n2 -> aim
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 38-20) //n3 -> next attack
+	//c.SetNormalCancelFrames(2, core.Actionaim, 35-19) //n3 -> aim
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 37-26) //n4 -> next attack
+	//c.SetNormalCancelFrames(3, core.Actionaim, 33-17) //n4 -> aim
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 28-21) //n5 -> next attack
+	//c.SetNormalCancelFrames(4, core.Actionaim, 33-17) //n5 -> aim
+
+	c.SetNormalCancelFrames(5, core.ActionAttack, 59-22) //n6 -> next attack (n1)
+	//c.SetNormalCancelFrames(5, core.Actionaim, 33-17) //n6 -> aim
+
+	//todo: get separate counts for each cancel, currently using generic frames for all of them
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAttack, 113-103) //aim -> n1
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAim, 113-103)    //aim -> aim
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSkill, 113-103)  //aim -> skill
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionBurst, 113-103)  //aim -> burst
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSwap, 113-103)   //aim -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 124-122) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAim, 124-122)    //burst -> aim (assumed)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 124-122)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 124-122)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 124-122)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 122-122)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 28-13)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAim, 28-13) //assumed
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 28-13)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 28-13)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 28-13)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 27-13)
+
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/ganyu/ganyu.go b/internal/characters/ganyu/ganyu.go
index 77ea73241..cab185aed 100644
--- a/internal/characters/ganyu/ganyu.go
+++ b/internal/characters/ganyu/ganyu.go
@@ -12,6 +12,7 @@ func init() {
 type char struct {
 	*character.Tmpl
 	a1Expiry int
+	c4Stacks int
 }
 
 func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
@@ -34,8 +35,10 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.BurstCon = 3
 	c.SkillCon = 5
 	c.CharZone = core.ZoneLiyue
+	c.InitCancelFrames()
 
 	c.a1Expiry = -1
+	c.c4Stacks = 0
 
 	if c.Base.Cons >= 2 {
 		c.SetNumCharges(core.ActionSkill, 2)
diff --git a/internal/characters/gorou/cons.go b/internal/characters/gorou/cons.go
index 53d682d2d..c449c6e28 100644
--- a/internal/characters/gorou/cons.go
+++ b/internal/characters/gorou/cons.go
@@ -31,30 +31,17 @@ func (c *char) c1() {
 //active character obtains an Elemental Shard from a Crystallize reaction.
 //This effect can occur once every 0.1s. Max extension is 3s.
 func (c *char) c2() {
-	//TODO: this is currently on reaction but really should be on pick up
-	cb := func(args ...interface{}) bool {
-		dur := c.Core.Status.Duration(generalGloryKey)
-		if dur == 0 {
+	c.Core.Events.Subscribe(core.OnShielded, func(args ...interface{}) bool {
+		if c.Core.Status.Duration(generalGloryKey) <= 0 {
 			return false
 		}
-		if c.c2Extension == 180 {
+		if c.c2Extension >= 3 {
 			return false
 		}
-		//to simulate pickup we add 30 frames delay
-		c.Core.Tasks.Add(func() {
-			ext := 60
-			if c.c2Extension+ext > 180 {
-				ext = 180 - c.c2Extension
-			}
-			c.c2Extension += ext
-			c.Core.Status.AddStatus(generalGloryKey, c.Core.F+dur+ext)
-		}, 30)
+		c.c2Extension++
+		c.Core.Status.AddStatus(generalGloryKey, 60)
 		return false
-	}
-	c.Core.Events.Subscribe(core.OnCrystallizeCryo, cb, "gorou-c2")
-	c.Core.Events.Subscribe(core.OnCrystallizeElectro, cb, "gorou-c2")
-	c.Core.Events.Subscribe(core.OnCrystallizeHydro, cb, "gorou-c2")
-	c.Core.Events.Subscribe(core.OnCrystallizePyro, cb, "gorou-c2")
+	}, "gorou-c2")
 }
 
 func (c *char) c6() {
diff --git a/internal/characters/hutao/abil.go b/internal/characters/hutao/abil.go
index ef86f549a..95d412b5b 100644
--- a/internal/characters/hutao/abil.go
+++ b/internal/characters/hutao/abil.go
@@ -8,7 +8,6 @@ import (
 
 func (c *char) Attack(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionAttack, p)
-	hits := len(attack[c.NormalCounter])
 	//check for particles
 	c.ppParticles()
 	ai := core.AttackInfo{
@@ -21,10 +20,9 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		Element:    core.Physical,
 		Durability: 25,
 	}
-	for i := 0; i < hits; i++ {
+	for i := range attack[c.NormalCounter] {
 		ai.Mult = attack[c.NormalCounter][i][c.TalentLvlAttack()]
-		finalFrame := dmgFrame[c.NormalCounter][i]
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), finalFrame, finalFrame)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -42,8 +40,8 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		//trying to change animations mid-attack, but not sure how to fully test that
 		//[4:41 PM] jstern25| ₼WHO_SUPREMACY: this mostly checks out
 		//her e can't expire during q as well
-		if f > c.Core.Status.Duration("paramita") {
-			c.Core.Status.AddStatus("paramita", f)
+		if paramitaChargeHitmark > c.Core.Status.Duration("paramita") {
+			c.Core.Status.AddStatus("paramita", paramitaChargeHitmark)
 			// c.S.Status["paramita"] = c.Core.F + f //extend this to barely cover the burst
 		}
 
@@ -55,7 +53,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 	}
 
 	//check for particles
-	//TODO: assuming charge can generate particles as well
+	//TODO: assuming charge can generate particles as well (is this accurate? needs testing)
 	c.ppParticles()
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -68,7 +66,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       charge[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), f-5, f-5)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), 6, 6)
 
 	return f, a
 }
@@ -81,7 +79,7 @@ func (c *char) ppParticles() {
 			if c.Core.Rand.Float64() < 0.5 {
 				count = 3
 			}
-			c.QueueParticle("Hutao", count, core.Pyro, dmgFrame[c.NormalCounter][0])
+			c.QueueParticle("Hutao", count, core.Pyro, hitmarks[c.NormalCounter][0])
 		}
 	}
 }
@@ -139,8 +137,10 @@ func (c *char) bbtickfunc(src int) func() {
 func (c *char) Skill(p map[string]int) (int, int) {
 	//increase based on hp at cast time
 	//drains hp
-	c.Core.Status.AddStatus("paramita", 540+20) //to account for animation
-	c.Core.Log.NewEvent("Paramita acivated", core.LogCharacterEvent, c.Index, "expiry", c.Core.F+540+20)
+	f, a := c.ActionFrames(core.ActionSkill, p)
+
+	c.Core.Status.AddStatus("paramita", 540+f) //to account for animation
+	c.Core.Log.NewEvent("Paramita acivated", core.LogCharacterEvent, c.Index, "expiry", c.Core.F+540+f)
 	//figure out atk buff
 	c.ppBonus = ppatk[c.TalentLvlSkill()] * c.MaxHP()
 	max := (c.Base.Atk + c.Weapon.Atk) * 4
@@ -156,8 +156,8 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	})
 	c.checkc6()
 
-	c.SetCD(core.ActionSkill, 960)
-	return c.ActionFrames(core.ActionSkill, p)
+	c.SetCDWithDelay(core.ActionSkill, 960, 14)
+	return f, a
 }
 
 func (c *char) ppHook() {
@@ -232,9 +232,9 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Durability: 50,
 		Mult:       mult,
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), f-5, f-5)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), f, f)
 
-	c.ConsumeEnergy(73)
-	c.SetCDWithDelay(core.ActionBurst, 900, 73)
+	c.ConsumeEnergy(68)
+	c.SetCDWithDelay(core.ActionBurst, 900, 62)
 	return f, a
 }
diff --git a/internal/characters/hutao/frames.go b/internal/characters/hutao/frames.go
index 641c6c563..fe99c98da 100644
--- a/internal/characters/hutao/frames.go
+++ b/internal/characters/hutao/frames.go
@@ -2,36 +2,111 @@ package hutao
 
 import "github.com/genshinsim/gcsim/pkg/core"
 
-/**
-[11:32 PM] sakuno | yanfei is my new maid: @gimmeabreak
-https://www.youtube.com/watch?v=3aCiH2U4BjY
-
-framecounts for 7 attempts of N2CJ (no hitlag):
-83, 85, 88, 89, 77, 82, 84
-
-first 3 not from the uploaded recording (as a n1cd player i cud barely pull it off :monkaS: )
-YouTube
-**/
-
-//var normalFrames = []int{13, 16, 25, 36, 44, 39}               // from kqm lib
-var normalFrames = []int{10, 13, 22, 33, 41, 36} // from kqm lib, -3 for hit lag
-//var dmgFrame = [][]int{{13}, {16}, {25}, {36}, {26, 44}, {39}} // from kqm lib
-var dmgFrame = [][]int{{10}, {13}, {22}, {33}, {23, 41}, {36}} // from kqm lib - 3 for hit lag
+var hitmarks = [][]int{{12}, {10}, {17}, {23}, {16, 27}, {27}}
+var paramitaChargeHitmark = 6
 
 func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
-		f := normalFrames[c.NormalCounter]
+		f := 0
+		a := 0
+
+		switch c.NormalCounter {
+		//TODO: need to add atkspd mod
+		case 0:
+			f = 12
+			a = 16
+		case 1:
+			f = 10
+			a = 12
+		case 2:
+			f = 17
+			a = 28
+		case 3:
+			f = 23
+			a = 30
+		case 4:
+			f = 27
+			a = 36
+		case 5:
+			f = 27
+			a = 72
+		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+
+		return f, a
 	case core.ActionCharge:
-		return 9, 9 //rough.. 11, -2 for hit lag
+		if c.Core.Status.Duration("paramita") > 0 {
+			return 2, 44
+		}
+		return 19, 57
 	case core.ActionSkill:
-		return 42, 42 // from kqm lib
+		return 14, 29
 	case core.ActionBurst:
-		return 130, 130 // from kqm lib
+		return 66, 98
 	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, 19-12) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 12-10) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 18-10) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 28-17) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 22-17) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 30-23) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 32-23) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 36-27) //n5 -> n6
+	c.SetNormalCancelFrames(3, core.ActionCharge, 48-27) //n5 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 72-27) //n6 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 57-19) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 57-19)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 60-19)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 62-19)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 98-66) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 97-66)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 98-66)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 100-66)  //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 95-66)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 29-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 28-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 37-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 37-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 52-14)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for her CA cancels during paramita
+	if c.Core.LastAction.Typ == core.ActionCharge &&
+		c.Core.Status.Duration("paramita") > 0 {
+		f := 0
+		switch next {
+		case core.ActionAttack:
+			f = 44 - 2
+		case core.ActionBurst:
+			f = 35 - 2
+		case core.ActionDash:
+			f = 2 - 2
+		case core.ActionJump:
+			f = 2 - 2
+		case core.ActionSwap:
+			f = 42 - 2
+		}
+		return f
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/hutao/hutao.go b/internal/characters/hutao/hutao.go
index b17daefe3..a253d32db 100644
--- a/internal/characters/hutao/hutao.go
+++ b/internal/characters/hutao/hutao.go
@@ -43,6 +43,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.ppHook()
 	c.onExitField()
diff --git a/internal/characters/jean/frames.go b/internal/characters/jean/frames.go
new file mode 100644
index 000000000..8520349f8
--- /dev/null
+++ b/internal/characters/jean/frames.go
@@ -0,0 +1,87 @@
+package jean
+
+import "github.com/genshinsim/gcsim/pkg/core"
+
+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:
+			f = 13
+			a = 22
+		case 1:
+			f = 6
+			a = 14
+		case 2:
+			f = 17
+			a = 28
+		case 3:
+			f = 37
+			a = 44
+		case 4:
+			f = 25
+			a = 68
+		}
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+
+		return f, a
+	case core.ActionCharge:
+		return 36, 57
+	case core.ActionSkill:
+		hold := p["hold"]
+		//hold for p up to 5 seconds
+		if hold > 300 {
+			hold = 300
+		}
+
+		return 21 + hold, 46 + hold
+	case core.ActionBurst:
+		return 40, 83
+	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, 22-13) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 25-13) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 14-6) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 20-6) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 28-17) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 31-17) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 44-37) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 49-37) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 68-36) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 57-36) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 57-36)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 56-36)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 39-36)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 83-40) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 83-40)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 70-40)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 70-40)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 84-40)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 46-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 46-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 28-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 28-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 45-21)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/jean/jean.go b/internal/characters/jean/jean.go
index 6bb164d03..290607890 100644
--- a/internal/characters/jean/jean.go
+++ b/internal/characters/jean/jean.go
@@ -35,6 +35,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.NormalHitNum = 5
 	c.BurstCon = 3
 	c.SkillCon = 5
+	c.InitCancelFrames()
 
 	return &c, nil
 }
@@ -47,44 +48,6 @@ func (c *char) Init() {
 	}
 }
 
-func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
-	switch a {
-	case core.ActionAttack:
-		f := 0
-		switch c.NormalCounter {
-		//TODO: need to add atkspd mod
-		case 0:
-			f = 14 //frames from keqing lib
-		case 1:
-			f = 37 - 14
-		case 2:
-			f = 66 - 37
-		case 3:
-			f = 124 - 66
-		case 4:
-			f = 159 - 124
-		}
-		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
-	case core.ActionCharge:
-		return 73, 73
-	case core.ActionSkill:
-
-		hold := p["hold"]
-		//hold for p up to 5 seconds
-		if hold > 300 {
-			hold = 300
-		}
-
-		return 23 + hold, 23 + hold
-	case core.ActionBurst:
-		return 88, 88
-	default:
-		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
-		return 0, 0
-	}
-}
-
 func (c *char) Attack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionAttack, p)
@@ -115,7 +78,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 				Bonus:   c.Stat(core.Heal),
 			})
 		}
-	}, "jean-na", f-1)
+	}, "jean-na", f)
 
 	c.AdvanceNormalIndex()
 
@@ -137,11 +100,23 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       charge[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.4, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.4, false, core.TargettableEnemy), f, f)
 
 	return f, a
 }
 
+func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
+	switch a {
+	case core.ActionDash:
+		return 18
+	case core.ActionCharge:
+		return 20
+	default:
+		c.Core.Log.NewEvent("ActionStam not implemented", core.LogActionEvent, c.Index, "action", a.String())
+		return 0
+	}
+}
+
 func (c *char) Skill(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionSkill, p)
@@ -163,15 +138,15 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		c.Core.Log.NewEvent("jean c1 adding 40% dmg", core.LogCharacterEvent, c.Index, "final dmg%", snap.Stats[core.DmgP])
 	}
 
-	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), f-1)
+	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), f)
 
 	count := 2
-	if c.Core.Rand.Float64() < .67 {
+	if c.Core.Rand.Float64() < 2.0/3.0 {
 		count++
 	}
 	c.QueueParticle("Jean", count, core.Anemo, f+100)
 
-	c.SetCD(core.ActionSkill, 360)
+	c.SetCDWithDelay(core.ActionSkill, 360, f-2)
 	return f, a
 }
 
@@ -200,23 +175,23 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	}
 	snap := c.Snapshot(&ai)
 
-	//looks to be around 60 frames in
-	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(5, false, core.TargettableEnemy), 60)
+	//initial hit at 40f
+	c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(5, false, core.TargettableEnemy), 40)
 
 	ai.Abil = "Dandelion Breeze (In/Out)"
 	ai.Mult = burstEnter[c.TalentLvlBurst()]
-	//first enter is at frame 66
+	//first enter is at frame 55
 	for i := 0; i < enter; i++ {
-		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(5, false, core.TargettableEnemy), 66+i*delay)
+		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(5, false, core.TargettableEnemy), 55+i*delay)
 	}
 
-	c.Core.Status.AddStatus("jeanq", 630)
+	c.Core.Status.AddStatus("jeanq", 600+f)
 
 	if c.Base.Cons >= 4 {
 		//add debuff to all target for ??? duration
 		for _, t := range c.Core.Targets {
 			t.AddResMod("jeanc4", core.ResistMod{
-				Duration: 600, //10 seconds
+				Duration: 600 + f, //10 seconds + animation
 				Ele:      core.Anemo,
 				Value:    -0.4,
 			})
@@ -274,10 +249,10 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		}, "Jean Tick", i)
 	}
 
-	c.SetCD(core.ActionBurst, 1200)
+	c.SetCDWithDelay(core.ActionBurst, 1200, 38)
 	c.AddTask(func() {
 		c.Energy = 16 //jean a4
-	}, "jean-burst-energy-consume", 46)
+	}, "jean-burst-energy-consume", 41)
 
 	return f, a
 }
diff --git a/internal/characters/kazuha/frames.go b/internal/characters/kazuha/frames.go
index 0d4a3a2b9..d405a6dd6 100644
--- a/internal/characters/kazuha/frames.go
+++ b/internal/characters/kazuha/frames.go
@@ -80,8 +80,8 @@ func (c *char) InitCancelFrames() {
 
 	// 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.ActionDash, 100-95) //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 100-95) //burst -> jump
 	// c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 32-21) //burst -> swap
 
 	//skill press frames, dmg at 14
diff --git a/internal/characters/keqing/abil.go b/internal/characters/keqing/abil.go
index 3cbc8cd45..38ce23cd3 100644
--- a/internal/characters/keqing/abil.go
+++ b/internal/characters/keqing/abil.go
@@ -6,7 +6,7 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
-var hitmarks = [][]int{{8}, {20}, {25}, {25, 35}, {34}}
+var hitmarks = [][]int{{11}, {11}, {15}, {12, 22}, {26}}
 
 func (c *char) Attack(p map[string]int) (int, int) {
 	//apply attack speed
@@ -35,6 +35,8 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	return f, a
 }
 
+var chargeHitmarks = []int{22, 24}
+
 func (c *char) ChargeAttack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionCharge, p)
@@ -51,13 +53,17 @@ 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-i*10-5, f-i*10-5)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), chargeHitmarks[i], chargeHitmarks[i])
 	}
 
-	if c.Tags["e"] == 1 {
+	if c.Core.Status.Duration(stilettoKey) > 0 {
+		// despawn stiletto
+		c.Core.Status.DeleteStatus(stilettoKey)
+
 		//2 hits
 		ai := core.AttackInfo{
 			ActorIndex: c.Index,
+			Abil:       "Thunderclap Slash",
 			AttackTag:  core.AttackTagElementalArt,
 			ICDTag:     core.ICDTagElementalArt,
 			ICDGroup:   core.ICDGroupDefault,
@@ -66,14 +72,9 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 			Mult:       skillCA[c.TalentLvlSkill()],
 		}
 		for i := 0; i < 2; i++ {
-			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
+			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), chargeHitmarks[i], chargeHitmarks[i])
 		}
 
-		//place on cooldown
-		c.Tags["e"] = 0
-		// c.CD[def.SkillCD] = c.eStartFrame + 100
-		c.SetCD(core.ActionSkill, c.eStartFrame+450-c.Core.F)
-
 		// TODO: Particle timing?
 		if c.Core.Rand.Float64() < .5 {
 			c.QueueParticle("keqing", 2, core.Electro, 100)
@@ -90,7 +91,8 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 }
 
 func (c *char) Skill(p map[string]int) (int, int) {
-	if c.Tags["e"] == 1 {
+	// check if stiletto is on-field
+	if c.Core.Status.Duration(stilettoKey) > 0 {
 		return c.skillNext(p)
 	}
 	return c.skillFirst(p)
@@ -111,25 +113,17 @@ func (c *char) skillFirst(p map[string]int) (int, int) {
 		Mult:       skill[c.TalentLvlSkill()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
-
-	c.Tags["e"] = 1
-	c.eStartFrame = c.Core.F
-
-	//place on cd after certain frames if started is still true
-	//looks like the E thing lasts 5 seconds
-	c.AddTask(func() {
-		if c.Tags["e"] == 1 {
-			c.Tags["e"] = 0
-			// c.CD[def.SkillCD] = c.eStartFrame + 100
-			c.SetCD(core.ActionSkill, c.eStartFrame+450-c.Core.F) //TODO: cooldown if not triggered, 7.5s
-		}
-	}, "keqing-skill-cd", c.Core.F+300) //TODO: check this
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 25, 25)
 
 	if c.Base.Cons == 6 {
 		c.activateC6("skill")
 	}
 
+	// spawn after cd and stays for 5s
+	c.Core.Status.AddStatus(stilettoKey, 5*60+20)
+
+	c.SetCDWithDelay(core.ActionSkill, 7*60+30, 20)
+
 	return f, a
 }
 
@@ -147,7 +141,7 @@ func (c *char) skillNext(p map[string]int) (int, int) {
 		Mult:       skillPress[c.TalentLvlSkill()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 27, 27)
 	//add electro infusion
 
 	c.Core.Status.AddStatus("keqinginfuse", 300)
@@ -166,7 +160,7 @@ func (c *char) skillNext(p map[string]int) (int, int) {
 			hits = 1 //default 1 hit
 		}
 		ai := core.AttackInfo{
-			Abil:       "Stellar Restoration (Slashing)",
+			Abil:       "Stellar Restoration (C1)",
 			ActorIndex: c.Index,
 			AttackTag:  core.AttackTagElementalArtHold,
 			ICDTag:     core.ICDTagElementalArt,
@@ -176,7 +170,7 @@ func (c *char) skillNext(p map[string]int) (int, int) {
 			Mult:       .5,
 		}
 		for i := 0; i < hits; i++ {
-			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 0, f)
+			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 0, 27)
 		}
 	}
 
@@ -187,9 +181,9 @@ func (c *char) skillNext(p map[string]int) (int, int) {
 		c.QueueParticle("keqing", 3, core.Electro, 100)
 	}
 
-	//place on cooldown
-	c.Tags["e"] = 0
-	c.SetCD(core.ActionSkill, c.eStartFrame+450-c.Core.F)
+	// despawn stiletto
+	c.Core.Status.DeleteStatus(stilettoKey)
+
 	return f, a
 }
 
@@ -198,10 +192,10 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	c.a4()
 
-	//first hit 70 frame
-	//first tick 74 frame
-	//last tick 168
-	//last hit 211
+	//first hit 56 frame
+	//first tick 82 frame
+	//last tick 162
+	//last hit 197
 
 	//initial
 	ai := core.AttackInfo{
@@ -215,12 +209,12 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Mult:       burstInitial[c.TalentLvlBurst()],
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 70, 70)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 56, 56)
 	//8 hits
 
 	ai.Abil = "Starward Sword (Consecutive Slash)"
 	ai.Mult = burstDot[c.TalentLvlBurst()]
-	for i := 70; i < 170; i += 13 {
+	for i := 82; i < 162; i += 11 {
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), i, i)
 	}
 
@@ -228,14 +222,20 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	ai.Abil = "Starward Sword (Last Attack)"
 	ai.Mult = burstFinal[c.TalentLvlBurst()]
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 211, 211)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 197, 197)
 
 	if c.Base.Cons == 6 {
 		c.activateC6("burst")
 	}
 
-	c.ConsumeEnergy(60)
-	c.SetCDWithDelay(core.ActionBurst, 720, 60)
+	c.ConsumeEnergy(55)
+	c.SetCDWithDelay(core.ActionBurst, 720, 52)
+
+	return f, a
+}
 
+//Keqing dash is 20 frames
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
 	return f, a
 }
diff --git a/internal/characters/keqing/frames.go b/internal/characters/keqing/frames.go
index 5e2cbd73e..7bee902d2 100644
--- a/internal/characters/keqing/frames.go
+++ b/internal/characters/keqing/frames.go
@@ -6,30 +6,102 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
 		f := 11
+		a := 0
 		switch c.NormalCounter {
 		//TODO: need to add atkspd mod
+		case 0:
+			f = 11
+			a = 15
 		case 1:
-			f = 33 - 11
+			f = 11
+			a = 16
 		case 2:
-			f = 60 - 33
+			f = 15
+			a = 27
 		case 3:
-			f = 97 - 60
+			f = 22
+			a = 31
 		case 4:
-			f = 133 - 97
+			f = 26
+			a = 66
 		}
-		return f, f
+		return f, a
 	case core.ActionCharge:
-		return 52, 52
+		return 24, 36
 	case core.ActionSkill:
-		if c.Tags["e"] == 1 {
+		if c.Core.Status.Duration(stilettoKey) > 0 {
 			//2nd part
-			return 84, 84
+			return 15, 42
 		}
 		//first part
-		return 34, 34
+		return 21, 36
 	case core.ActionBurst:
-		return 125, 125
+		return 56, 124
+	case core.ActionDash:
+		return 20, 20
 	}
 	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, 15-11) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 21-11) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 16-11) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 24-11) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 27-15) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 36-15) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 31-22) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 58-22) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 66-26) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 36-24) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 35-24)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 35-24)  //charge -> burst
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 124-56) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 124-56)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 122-56)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 124-56)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 123-56)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 36-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSkill, 35-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 37-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 21-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 21-21)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 28-21)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Keqing's E recast
+	if c.Core.LastAction.Typ == core.ActionSkill &&
+		c.Core.Status.Duration(stilettoKey) == 0 {
+		return SkillRecastFrames(next)
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func SkillRecastFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 42 - 15
+	case core.ActionBurst:
+		return 43 - 15
+	case core.ActionDash:
+		return 15 - 15
+	case core.ActionJump:
+		return 16 - 15
+	case core.ActionSwap:
+		return 42 - 15
+	default:
+		return 42 - 15
+	}
+}
diff --git a/internal/characters/keqing/keqing.go b/internal/characters/keqing/keqing.go
index 293d96660..57e7a38dd 100644
--- a/internal/characters/keqing/keqing.go
+++ b/internal/characters/keqing/keqing.go
@@ -5,14 +5,15 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+const stilettoKey = "keqingstiletto"
+
 func init() {
 	core.RegisterCharFunc(core.Keqing, NewChar)
 }
 
 type char struct {
 	*character.Tmpl
-	eStartFrame int
-	c2ICD       int
+	c2ICD int
 }
 
 func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
@@ -41,6 +42,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	if c.Base.Cons >= 2 {
 		c.c2()
@@ -50,6 +52,14 @@ func (c *char) Init() {
 	}
 }
 
+func (c *char) ActionReady(a core.ActionType, p map[string]int) bool {
+	// check if stiletto is on-field
+	if a == core.ActionSkill && c.Core.Status.Duration(stilettoKey) > 0 {
+		return true
+	}
+	return c.Tmpl.ActionReady(a, p)
+}
+
 func (c *char) a4() {
 	m := make([]float64, core.EndStatType)
 	m[core.CR] = 0.15
diff --git a/internal/characters/klee/abil.go b/internal/characters/klee/abil.go
index 4a474d94b..581bf970a 100644
--- a/internal/characters/klee/abil.go
+++ b/internal/characters/klee/abil.go
@@ -163,40 +163,43 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		NoImpulse:  true,
 	}
 	//lasts 10 seconds, starts after 2.2 seconds maybe?
+	c.Core.Status.AddStatus("kleeq", 600+132)
 
 	//every 1.8 second +on added shoots between 3 to 5, ignore the queue thing.. space it out .2 between each wave i guess
 
+	// snapshot at end of animation?
+	var snap core.Snapshot
+	c.AddTask(func() {
+		snap = c.Snapshot(&ai)
+	}, "klee-burst-snapshot", 100)
+
 	for i := 132; i < 732; i += 108 {
 		c.AddTask(func() {
-			//no more if klee is not on field
-			if c.Core.ActiveChar != c.Index {
+			//no more if burst has ended early
+			if c.Core.Status.Duration("kleeq") <= 0 {
 				return
 			}
 			//wave 1 = 1
-			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 0, 0)
+			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), 0)
 			//wave 2 = 1 + 30% chance of 1
-			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 0, 12)
+			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), 12)
 			if c.Core.Rand.Float64() < 0.3 {
-				c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 0, 12)
+				c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), 12)
 			}
 			//wave 3 = 1 + 50% chance of 1
-			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 0, 24)
+			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), 24)
 			if c.Core.Rand.Float64() < 0.5 {
-				c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), 0, 24)
+				c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(1, false, core.TargettableEnemy), 24)
 			}
 		}, "klee-burst", i)
 	}
 
-	c.AddTask(func() {
-		c.Core.Status.AddStatus("kleeq", 600)
-	}, "klee-burst-status", 132)
-
 	//every 3 seconds add energy if c6
 	if c.Base.Cons == 6 {
 		for i := f + 180; i < f+600; i += 180 {
 			c.AddTask(func() {
-				//no more if klee is not on field
-				if c.Core.ActiveChar != c.Index {
+				//no more if burst has ended early
+				if c.Core.Status.Duration("kleeq") <= 0 {
 					return
 				}
 
@@ -206,17 +209,16 @@ func (c *char) Burst(p map[string]int) (int, int) {
 					}
 					x.AddEnergy("klee-c6", 3)
 				}
-
 			}, "klee-c6", i)
 		}
 
-		//add 25% buff
+		// add 10% pyro for 25s
+		m := make([]float64, core.EndStatType)
+		m[core.PyroP] = .1
 		for _, x := range c.Core.Chars {
-			val := make([]float64, core.EndStatType)
-			val[core.PyroP] = .1
 			x.AddMod(core.CharStatMod{
 				Key:    "klee-c6",
-				Amount: func() ([]float64, bool) { return val, true },
+				Amount: func() ([]float64, bool) { return m, true },
 				Expiry: c.Core.F + 1500,
 			})
 		}
diff --git a/internal/characters/klee/klee.go b/internal/characters/klee/klee.go
index 7e3e0b03b..d9fd08817 100644
--- a/internal/characters/klee/klee.go
+++ b/internal/characters/klee/klee.go
@@ -44,10 +44,7 @@ func (c *char) Init() {
 	c.Tmpl.Init()
 
 	c.a4()
-
-	if c.Base.Cons >= 4 {
-		c.c4()
-	}
+	c.onExitField()
 }
 
 func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
@@ -125,11 +122,16 @@ func (c *char) c1(delay int) {
 
 }
 
-func (c *char) c4() {
+// clear klee burst when she leaves the field and handle c4
+func (c *char) onExitField() {
 	c.Core.Events.Subscribe(core.OnCharacterSwap, func(args ...interface{}) bool {
-		//if burst is active and klee no longer active char
-		if c.Core.ActiveChar != c.Index && c.Core.Status.Duration("kleeq") > 0 {
-			c.Core.Status.DeleteStatus("kleeq")
+		// check if burst is active
+		if c.Core.Status.Duration("kleeq") <= 0 {
+			return false
+		}
+		c.Core.Status.DeleteStatus("kleeq")
+
+		if c.Base.Cons >= 4 {
 			//blow up
 			ai := core.AttackInfo{
 				ActorIndex: c.Index,
@@ -145,6 +147,5 @@ func (c *char) c4() {
 			c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), 0, 0)
 		}
 		return false
-
-	}, "klee-c4")
+	}, "klee-exit")
 }
diff --git a/internal/characters/kokomi/abil.go b/internal/characters/kokomi/abil.go
index 94fe4488e..86f8cf080 100644
--- a/internal/characters/kokomi/abil.go
+++ b/internal/characters/kokomi/abil.go
@@ -28,11 +28,11 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	}
 	ai.FlatDmg = c.burstDmgBonus(ai.AttackTag)
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), 0, f+travel)
+	c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), f, f+travel)
 	// TODO: Assume that this is not dynamic (snapshot on projectile release)
 
 	if c.NormalCounter == c.NormalHitNum-1 {
-		c.c1(f)
+		c.c1(f, travel)
 	}
 
 	c.AdvanceNormalIndex()
@@ -40,7 +40,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	return f, a
 }
 
-func (c *char) c1(f int) {
+func (c *char) c1(f, travel int) {
 	if c.Base.Cons == 0 {
 		return
 	}
@@ -51,7 +51,7 @@ func (c *char) c1(f int) {
 	// TODO: Assume that these are 1A (not specified in library)
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
-		Abil:       fmt.Sprintf("Normal %v", c.NormalCounter),
+		Abil:       "At Water's Edge (C1)",
 		AttackTag:  core.AttackTagNone,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
@@ -61,9 +61,8 @@ func (c *char) c1(f int) {
 	}
 	ai.FlatDmg = 0.3 * c.MaxHP()
 
-	// TODO: Frames not in library - Think it's 7 frames based on a rough count
 	// TODO: Is this snapshotted/dynamic?
-	c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), 0, f+7)
+	c.Core.Combat.QueueAttack(ai, core.NewDefSingleTarget(1, core.TargettableEnemy), f, f+travel)
 }
 
 // Standard charge attack
@@ -93,15 +92,15 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 func (c *char) Skill(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
+	// skill duration is ~12.5s
 	// Plus 1 to avoid same frame issues with skill ticks
-	// add 20 because the skill duration is not exactly 12s
-	c.Core.Status.AddStatus("kokomiskill", 20+12*60+1)
+	c.Core.Status.AddStatus("kokomiskill", 12*60+30+1)
 
 	d := c.createSkillSnapshot()
 
 	// You get 1 tick immediately, then 1 tick every 2 seconds for a total of 7 ticks
 	c.AddTask(func() { c.skillTick(d) }, "kokomi-e-tick", 24)
-	c.AddTask(c.skillTickTask(d, c.Core.F), "kokomi-e-ticks", 127)
+	c.AddTask(c.skillTickTask(d, c.Core.F), "kokomi-e-ticks", 24+126)
 
 	c.skillLastUsed = c.Core.F
 	c.SetCDWithDelay(core.ActionSkill, 20*60, 20)
@@ -233,23 +232,20 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	// C4 attack speed buff
 	if c.Base.Cons >= 4 {
+		m := make([]float64, core.EndStatType)
+		m[core.AtkSpd] = 0.1
 		c.AddMod(core.CharStatMod{
-			Key: "kokomi-c4",
+			Key:    "kokomi-c4",
+			Expiry: c.Core.F + 10*60,
 			Amount: func() ([]float64, bool) {
-				val := make([]float64, core.EndStatType)
-				val[core.AtkSpd] = 0.1
-				if c.Core.Status.Duration("kokomiburst") > 0 {
-					return val, true
-				}
-				return nil, false
+				return m, true
 			},
-			Expiry: c.Core.F + 10*60,
 		})
 	}
 
 	// Cannot be prefed particles
 	c.ConsumeEnergy(57)
-	c.SetCDWithDelay(core.ActionBurst, 18*60, 57)
+	c.SetCDWithDelay(core.ActionBurst, 18*60, 46)
 	return f, a
 }
 
diff --git a/internal/characters/kokomi/frames.go b/internal/characters/kokomi/frames.go
index ec0fa9a8e..5a1cfa2cd 100644
--- a/internal/characters/kokomi/frames.go
+++ b/internal/characters/kokomi/frames.go
@@ -5,32 +5,61 @@ import "github.com/genshinsim/gcsim/pkg/core"
 func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
-		f := 0
-		recovery := 0
+		var f, a int
 		switch c.NormalCounter {
 		//TODO: need to add atkspd mod
 		case 0:
-			f = 10 //frames from keqing lib
-			recovery = f
+			f = 4
+			a = 30
 		case 1:
-			f = 36 - 10
-			recovery = f
+			f = 12
+			a = 34
 		case 2:
-			f = 82 - 10
-			recovery = 122 - 82
+			f = 28
+			a = 65
 		}
-		atkspd := c.Stat(core.AtkSpd)
-		f = int(float64(f) / (1 + atkspd))
-		return f, recovery
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+		return f, a
 	case core.ActionCharge:
-		return 45, 45
+		return 48, 76
 	case core.ActionSkill:
-		// Took dash cancel frames for now
-		return 51, 51
+		return 30, 61
 	case core.ActionBurst:
-		return 75, 75
+		return 76, 76
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+	// NA/CA cancels
+	// we can't make the next CA faster by 14f so we reduce it here instead
+	c.SetNormalCancelFrames(0, core.ActionAttack, 14-4-1)    // n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 19-4-1-14) // n1 -> charge
+	c.SetNormalCancelFrames(0, core.ActionWalk, 30-4-1)      // n1 -> walk
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 30-12-1)    // n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 34-12-1-14) // n2 -> charge
+	c.SetNormalCancelFrames(1, core.ActionWalk, 34-12-1)      // n2 -> walk
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 65-28-1)    // n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 60-28-1-14) // n3 -> charge
+	c.SetNormalCancelFrames(2, core.ActionWalk, 60-28-1)      // n3 -> walk
+
+	// charge -> x
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 62-48)
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionCharge, 62-48)
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 62-48)
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 62-48)
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 62-48)
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionWalk, 76-48)
+
+	// skill -> x
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 61-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionCharge, 61-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSkill, 61-30) // uses burst frames
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 61-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 61-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionWalk, 61-30)
+}
diff --git a/internal/characters/kokomi/kokomi.go b/internal/characters/kokomi/kokomi.go
index 05829deb6..6b9f47626 100644
--- a/internal/characters/kokomi/kokomi.go
+++ b/internal/characters/kokomi/kokomi.go
@@ -48,6 +48,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.passive()
 	c.onExitField()
diff --git a/internal/characters/lisa/abil.go b/internal/characters/lisa/abil.go
index b78dd901b..d14690110 100644
--- a/internal/characters/lisa/abil.go
+++ b/internal/characters/lisa/abil.go
@@ -117,11 +117,9 @@ func (c *char) skillHold(p map[string]int) (int, int) {
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
 		Element:    core.Electro,
-		Durability: 25,
+		Durability: 50,
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(3, false, core.TargettableEnemy), 0, f)
-
 	//c2 add defense? no interruptions either way
 	if c.Base.Cons >= 2 {
 		//increase def for the duration of this abil in however many frames
diff --git a/internal/characters/ningguang/abil.go b/internal/characters/ningguang/abil.go
index 66d0f728a..1a84b6164 100644
--- a/internal/characters/ningguang/abil.go
+++ b/internal/characters/ningguang/abil.go
@@ -28,7 +28,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 
 	done := false
 	cb := func(a core.AttackCB) {
-		if !done {
+		if done {
 			return
 		}
 		count := c.Tags["jade"]
@@ -37,6 +37,8 @@ func (c *char) Attack(p map[string]int) (int, int) {
 			count++
 			if count > 3 {
 				count = 3
+			} else {
+				c.Core.Log.NewEvent("adding star jade", core.LogCharacterEvent, c.Index, "count", count)
 			}
 			c.Tags["jade"] = count
 		}
@@ -177,6 +179,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	if c.Base.Cons == 6 {
 		c.Tags["jade"] = 7
+		c.Core.Log.NewEvent("c6 - adding star jade", core.LogCharacterEvent, c.Index, "count", c.Tags["jade"])
 	}
 
 	c.ConsumeEnergy(8)
diff --git a/internal/characters/ningguang/screen.go b/internal/characters/ningguang/screen.go
index e9642adb6..19e40be8a 100644
--- a/internal/characters/ningguang/screen.go
+++ b/internal/characters/ningguang/screen.go
@@ -27,13 +27,14 @@ func (c *construct) Type() core.GeoConstructType {
 func (c *construct) OnDestruct() {
 	if c.char.Base.Cons >= 2 {
 		//make sure last reset is more than 6 seconds ago
-		if c.char.c2reset <= c.char.Core.F-360 {
+		if c.char.c2reset <= c.char.Core.F-360 && c.char.Cooldown(core.ActionSkill) > 0 {
 			//reset cd
 			c.char.ResetActionCooldown(core.ActionSkill)
 			c.char.c2reset = c.char.Core.F
 		}
 	}
 }
+
 func (c *construct) Expiry() int {
 	return c.expiry
 }
diff --git a/internal/characters/noelle/frames.go b/internal/characters/noelle/frames.go
index 29aa08041..91f63aa74 100644
--- a/internal/characters/noelle/frames.go
+++ b/internal/characters/noelle/frames.go
@@ -19,7 +19,7 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 			a = 31
 		case 3:
 			f = 42
-			a = 105
+			a = 107
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
 		return f, a
@@ -38,7 +38,7 @@ func (c *char) InitCancelFrames() {
 	c.SetNormalCancelFrames(0, core.ActionAttack, 38-28-1)  // n1 -> next attack
 	c.SetNormalCancelFrames(1, core.ActionAttack, 46-25-1)  // n2 -> next attack
 	c.SetNormalCancelFrames(2, core.ActionAttack, 31-20-1)  // n3 -> next attack
-	c.SetNormalCancelFrames(3, core.ActionAttack, 105-42-1) // n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionAttack, 107-42-1) // n4 -> next attack
 
 	// skill -> x
 	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 78-12)
diff --git a/internal/characters/raiden/abil.go b/internal/characters/raiden/abil.go
index f2a97fb76..40f949837 100644
--- a/internal/characters/raiden/abil.go
+++ b/internal/characters/raiden/abil.go
@@ -6,13 +6,7 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
-var polearmDelayOffset = [][]int{
-	{1},
-	{1},
-	{1},
-	{14, 1},
-	{1},
-}
+var hitmarks = [][]int{{14}, {9}, {14}, {14, 27}, {34}}
 
 func (c *char) Attack(p map[string]int) (int, int) {
 
@@ -37,8 +31,8 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		c.Core.Combat.QueueAttack(
 			ai,
 			core.NewDefCircHit(0.5, false, core.TargettableEnemy),
-			f-polearmDelayOffset[c.NormalCounter][i],
-			f-polearmDelayOffset[c.NormalCounter][i],
+			hitmarks[c.NormalCounter][i],
+			hitmarks[c.NormalCounter][i],
 		)
 	}
 
@@ -66,25 +60,12 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Mult:       charge[c.TalentLvlAttack()],
 	}
 
-	c.Core.Combat.QueueAttack(
-		ai,
-		core.NewDefCircHit(0.5, false, core.TargettableEnemy),
-		f-31,
-		f-31,
-	)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), f, f)
 
 	return f, a
 
 }
 
-var swordDelayOffset = [][]int{
-	{1},
-	{1},
-	{1},
-	{14, 1},
-	{1},
-}
-
 func (c *char) burstRestorefunc(a core.AttackCB) {
 	if c.Core.F > c.restoreICD && c.restoreCount < 5 {
 		c.restoreCount++
@@ -100,6 +81,8 @@ func (c *char) burstRestorefunc(a core.AttackCB) {
 	}
 }
 
+var burstHitmarks = [][]int{{12}, {13}, {11}, {22, 33}, {33}}
+
 func (c *char) swordAttack(f int, a int) (int, int) {
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -121,8 +104,8 @@ func (c *char) swordAttack(f int, a int) (int, int) {
 		c.Core.Combat.QueueAttack(
 			ai,
 			core.NewDefCircHit(2, false, core.TargettableEnemy),
-			f-swordDelayOffset[c.NormalCounter][i],
-			f-swordDelayOffset[c.NormalCounter][i],
+			burstHitmarks[c.NormalCounter][i],
+			burstHitmarks[c.NormalCounter][i],
 			c.burstRestorefunc,
 			c.c6(),
 		)
@@ -156,9 +139,9 @@ func (c *char) swordCharge(p map[string]int) (int, int) {
 		}
 		c.Core.Combat.QueueAttack(
 			ai,
-			core.NewDefCircHit(2, false, core.TargettableEnemy),
-			f-42,
-			f-42,
+			core.NewDefCircHit(5, false, core.TargettableEnemy),
+			f,
+			f,
 			c.burstRestorefunc,
 			c.c6(),
 		)
@@ -185,12 +168,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       skill[c.TalentLvlSkill()],
 	}
-	c.Core.Combat.QueueAttack(
-		ai,
-		core.NewDefCircHit(2, false, core.TargettableEnemy),
-		f+19,
-		f+19,
-	)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 51, 51)
 
 	//activate eye
 	c.Core.Status.AddStatus("raidenskill", 1500+f)
@@ -214,7 +192,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		})
 	}
 
-	c.SetCD(core.ActionSkill, 600)
+	c.SetCDWithDelay(core.ActionSkill, 600, 6)
 	return f, a
 }
 
@@ -278,6 +256,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionBurst, p)
 
 	//activate burst, reset stacks
+	c.burstCastF = c.Core.F
 	c.stacksConsumed = c.stacks
 	c.stacks = 0
 	c.Core.Status.AddStatus("raidenburst", 420+f) //7 seconds
@@ -286,21 +265,16 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	c.c6Count = 0
 	c.c6ICD = 0
 
+	// apply when burst ends
 	if c.Base.Cons >= 4 {
-		val := make([]float64, core.EndStatType)
-		val[core.ATKP] = 0.3
-		for i, char := range c.Core.Chars {
-			if i == c.Index {
-				continue
+		c.applyC4 = true
+		src := c.burstCastF
+		c.AddTask(func() {
+			if src == c.burstCastF && c.applyC4 {
+				c.applyC4 = false
+				c.c4()
 			}
-			char.AddMod(core.CharStatMod{
-				Key:    "raiden-c4",
-				Expiry: c.Core.F + 600, //10s
-				Amount: func() ([]float64, bool) {
-					return val, true
-				},
-			})
-		}
+		}, "raiden-c4", 420+f)
 	}
 
 	if c.Base.Cons == 6 {
@@ -325,8 +299,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	}
 	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f, f)
 
-	c.SetCDWithDelay(core.ActionBurst, 18*60, 12)
-	c.ConsumeEnergy(12)
+	c.SetCD(core.ActionBurst, 18*60)
+	c.ConsumeEnergy(8)
 	return f, a
 }
 
@@ -339,6 +313,10 @@ func (c *char) onSwapClearBurst() {
 		prev := args[0].(int)
 		if prev == c.Index {
 			c.Core.Status.DeleteStatus("raidenburst")
+			if c.applyC4 {
+				c.applyC4 = false
+				c.c4()
+			}
 		}
 		return false
 	}, "raiden-burst-clear")
diff --git a/internal/characters/raiden/cons.go b/internal/characters/raiden/cons.go
index 6e2401995..ff762d4e6 100644
--- a/internal/characters/raiden/cons.go
+++ b/internal/characters/raiden/cons.go
@@ -2,6 +2,26 @@ package raiden
 
 import "github.com/genshinsim/gcsim/pkg/core"
 
+// When the Musou Isshin state applied by Secret Art: Musou Shinsetsu expires
+// all nearby party members (excluding the Raiden Shogun) gain 30% bonus ATK for 10s.
+func (c *char) c4() {
+	m := make([]float64, core.EndStatType)
+	m[core.ATKP] = 0.3
+
+	for i, char := range c.Core.Chars {
+		if i == c.Index {
+			continue
+		}
+		char.AddMod(core.CharStatMod{
+			Key:    "raiden-c4",
+			Expiry: c.Core.F + 600, //10s
+			Amount: func() ([]float64, bool) {
+				return m, true
+			},
+		})
+	}
+}
+
 func (c *char) c6() func(ac core.AttackCB) {
 	if c.Base.Cons < 6 {
 		return nil
diff --git a/internal/characters/raiden/frames.go b/internal/characters/raiden/frames.go
index 14cd9c42b..69f6f53f2 100644
--- a/internal/characters/raiden/frames.go
+++ b/internal/characters/raiden/frames.go
@@ -8,55 +8,164 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 	switch a {
 	case core.ActionAttack:
 		f := 0
+		a := 0
 		if c.Core.Status.Duration("raidenburst") == 0 {
 			switch c.NormalCounter {
 			//TODO: need to add atkspd mod
 			case 0:
-				if c.Core.LastAction.Target == core.Raiden && c.Core.LastAction.Typ == core.ActionAttack {
-					f += 21
-				}
 				f = 14
+				a = 18
 			case 1:
-				f = 31 - 14
+				f = 9
+				a = 13
 			case 2:
-				f = 56 - 31
+				f = 14
+				a = 26
 			case 3:
-				f = 102 - 56
+				f = 27
+				a = 41
 			case 4:
-				f = 151 - 102
+				f = 34
+				a = 50
 			}
 		} else {
 			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.Raiden && c.Core.LastAction.Typ == core.ActionAttack {
-					f += 32
-				}
 				f = 12
+				a = 19
 			case 1:
-				f = 32 - 12
+				f = 13
+				a = 16
 			case 2:
-				f = 54 - 32
+				f = 11
+				a = 16
 			case 3:
-				f = 95 - 54
+				f = 33
+				a = 44
 			case 4:
-				f = 139 - 95
+				f = 33
+				a = 59
 			}
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionCharge:
 		if c.Core.Status.Duration("raidenburst") == 0 {
-			return 61, 61 //30 if cancelled
+			return 22, 37
 		}
-		return 79, 79 //37 <- if cancelled
+		return 24, 56
 	case core.ActionSkill:
-		return 35, 35 // going by first swapable
+		return 17, 37
 	case core.ActionBurst:
-		return 108, 108
+		return 98, 111
 	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, 18-14) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 24-14) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 13-9) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 26-9) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 26-14) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 36-14) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 41-27) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 57-27) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 50-34) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 37-22) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 37-22)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 37-22)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 36-22)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 111-98) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 111-98)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 110-98)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 112-98)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 110-98)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 37-17)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 37-17)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 17-17)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 17-17)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 36-17)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Raiden's Q Normals and Charges
+	if (c.Core.LastAction.Typ == core.ActionAttack ||
+		c.Core.LastAction.Typ == core.ActionCharge) &&
+		c.Core.Status.Duration("raidenburst") > 0 {
+		f := 0
+		switch c.Core.LastAction.Typ {
+		case core.ActionAttack:
+			f = burstNormalCancels(next, c.NormalCounter)
+		case core.ActionCharge:
+			f = burstChargeCancels(next)
+		}
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+		return f
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func burstNormalCancels(next core.ActionType, NormalCounter int) int {
+	switch next {
+	case core.ActionAttack:
+		switch NormalCounter { //counter for the next attack
+		case 1:
+			return 19 - 12
+		case 2:
+			return 16 - 13
+		case 3:
+			return 16 - 11
+		case 4:
+			return 44 - 33
+		case 0:
+			return 59 - 33
+
+		}
+	case core.ActionCharge:
+		switch NormalCounter {
+		case 1:
+			return 24 - 12
+		case 2:
+			return 26 - 13
+		case 3:
+			return 34 - 11
+		case 4:
+			return 67 - 33
+		case 0:
+			return 83 - 33
+		}
+	}
+	//all other actions can cancel immediately
+	return 0
+}
+
+func burstChargeCancels(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 56 - 24
+	case core.ActionSkill:
+		return 56 - 24
+	case core.ActionDash:
+		return 35 - 24
+	case core.ActionJump:
+		return 35 - 24
+	case core.ActionSwap:
+		return 55 - 24
+	default:
+		return 0
+	}
+}
diff --git a/internal/characters/raiden/raiden.go b/internal/characters/raiden/raiden.go
index 75c489501..67f3f1781 100644
--- a/internal/characters/raiden/raiden.go
+++ b/internal/characters/raiden/raiden.go
@@ -7,11 +7,13 @@ import (
 
 type char struct {
 	*character.Tmpl
+	burstCastF     int
 	eyeICD         int
 	stacksConsumed float64
 	stacks         float64
 	restoreICD     int
 	restoreCount   int
+	applyC4        bool
 	c6Count        int
 	c6ICD          int
 }
@@ -46,6 +48,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.eyeOnDamage()
 	c.onBurstStackCount()
diff --git a/internal/characters/rosaria/abil.go b/internal/characters/rosaria/abil.go
index 13da815f0..f79d7d1d9 100644
--- a/internal/characters/rosaria/abil.go
+++ b/internal/characters/rosaria/abil.go
@@ -2,9 +2,12 @@ package rosaria
 
 import (
 	"fmt"
+
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{{9}, {13}, {19, 28}, {32}, {26, 40}}
+
 // Normal attack damage queue generator
 // relatively standard with no major differences versus other characters
 func (c *char) Attack(p map[string]int) (int, int) {
@@ -22,7 +25,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.1, false, core.TargettableEnemy), f-5+i, f-5+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -47,7 +50,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       nc[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	//return animation cd
 	return f, a
@@ -71,8 +74,8 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       skill[0][c.TalentLvlSkill()],
 	}
-	// First hit comes out 20 frames before second
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-20, f-20)
+
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	// A1 activation
 	// When Rosaria strikes an opponent from behind using Ravaging Confession, Rosaria's CRIT RATE increases by 12% for 5s.
@@ -82,12 +85,12 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		val[core.CR] = 0.12
 		c.AddMod(core.CharStatMod{
 			Key:    "rosaria-a1",
-			Expiry: c.Core.F + 300,
+			Expiry: c.Core.F + 300 + f,
 			Amount: func() ([]float64, bool) {
 				return val, true
 			},
 		})
-		c.Core.Log.NewEvent("Rosaria A1 activation", core.LogCharacterEvent, c.Index, "ends_on", c.Core.F+300)
+		c.Core.Log.NewEvent("Rosaria A1 activation", core.LogCharacterEvent, c.Index, "ends_on", c.Core.F+300+f)
 	}
 
 	// Rosaria E is dynamic, so requires a second snapshot
@@ -102,12 +105,13 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       skill[1][c.TalentLvlSkill()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	//second hit is 14 frames after the first
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f+14, f+14)
 
 	// Particles are emitted after the second hit lands
-	c.QueueParticle("rosaria", 3, core.Cryo, f+100)
+	c.QueueParticle("rosaria", 3, core.Cryo, f+14+100)
 
-	c.SetCD(core.ActionSkill, 360)
+	c.SetCDWithDelay(core.ActionSkill, 360, 23)
 
 	return f, a
 }
@@ -138,14 +142,15 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	if c.Base.Cons == 6 {
 		cb = c6cb
 	}
-	// Hit 1 comes out on frame 10
+	// Hit 1 comes out on frame 15
 	// 2nd hit comes after lance drop animation finishes
-	c.Core.Combat.QueueAttack(ai, core.NewCircleHit(x, y, 1, false, core.TargettableEnemy), 10, 10, cb)
+	c.Core.Combat.QueueAttack(ai, core.NewCircleHit(x, y, 1, false, core.TargettableEnemy), 15, 15, cb)
 
 	ai.Abil = "Rites of Termination (Hit 2)"
 	ai.Mult = burst[1][c.TalentLvlBurst()]
-	// Note old code set the hit 10 frames before the recorded one - not sure why
-	c.Core.Combat.QueueAttack(ai, core.NewCircleHit(0, 0, 2, false, core.TargettableEnemy), f-10, f-10, cb)
+
+	//lance lands at 60f/1s
+	c.Core.Combat.QueueAttack(ai, core.NewCircleHit(0, 0, 2, false, core.TargettableEnemy), 60, 60, cb)
 
 	//duration is 8 second (extended by c2 by 4s), + 0.5
 	dur := 510
@@ -168,9 +173,9 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	c.AddTask(func() {
 		// dot every 2 second after lance lands
 		for i := 120; i < dur; i += 120 {
-			c.Core.Combat.QueueAttack(ai, core.NewCircleHit(0, 0, 2, false, core.TargettableEnemy), 0, i+10, cb)
+			c.Core.Combat.QueueAttack(ai, core.NewCircleHit(0, 0, 2, false, core.TargettableEnemy), 0, i, cb)
 		}
-	}, "rosaria-snapshot", f-10)
+	}, "rosaria-snapshot", 60)
 
 	c.Core.Status.AddStatus("rosariaburst", dur)
 
@@ -200,8 +205,8 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		})
 	}
 
-	c.SetCDWithDelay(core.ActionBurst, 15*60, 12)
-	c.ConsumeEnergy(12)
+	c.SetCD(core.ActionBurst, 15*60)
+	c.ConsumeEnergy(6)
 
 	return f, a
 }
diff --git a/internal/characters/rosaria/frames.go b/internal/characters/rosaria/frames.go
index 6573c5bf2..f58f0e3e4 100644
--- a/internal/characters/rosaria/frames.go
+++ b/internal/characters/rosaria/frames.go
@@ -6,30 +6,78 @@ 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:
-			f = 10 //frames from keqing lib
+			f = 9
+			a = 19
 		case 1:
-			f = 36 - 10
+			f = 13
+			a = 23
 		case 2:
-			f = 81 - 36
+			f = 28
+			a = 31
 		case 3:
-			f = 115 - 81
+			f = 32
+			a = 44
 		case 4:
-			f = 175 - 115
+			f = 40
+			a = 66
 		}
-		atkspd := c.Stat(core.AtkSpd)
-		f = int(float64(f) / (1 + atkspd))
-		return f, f
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+
+		return f, a
 	case core.ActionCharge:
-		return 89, 89 //frames from keqing libf
+		return 22, 69
 	case core.ActionSkill:
-		return 65, 65 //ok
+		return 24, 51
 	case core.ActionBurst:
-		return 74, 74 //ok
+		return 15, 70
 	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, 19-9) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 24-9) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 23-13) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 27-13) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 31-28) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 34-28) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 44-32) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 52-32) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 66-40) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 69-22) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 69-22)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 69-22)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionDash, 39-22)   //charge -> dash
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionJump, 25-22)   //charge -> jump
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 66-22)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 70-15) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 70-15)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 57-15)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 59-15)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 69-15)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 51-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 51-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 38-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 40-24)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 50-24)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/rosaria/rosaria.go b/internal/characters/rosaria/rosaria.go
index 538da13cf..5b6d7ffa4 100644
--- a/internal/characters/rosaria/rosaria.go
+++ b/internal/characters/rosaria/rosaria.go
@@ -38,6 +38,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	if c.Base.Cons >= 1 {
 		c.c1()
diff --git a/internal/characters/sucrose/abil.go b/internal/characters/sucrose/abil.go
index 7f1655a5b..0cea485fb 100644
--- a/internal/characters/sucrose/abil.go
+++ b/internal/characters/sucrose/abil.go
@@ -56,6 +56,12 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 	return f, a
 }
 
+//sucrose's dash can be cancelled by her E and Q, so we override it here
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
+	return f, a
+}
+
 func (c *char) Skill(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
diff --git a/internal/characters/sucrose/frames.go b/internal/characters/sucrose/frames.go
index 364470afd..d569e0197 100644
--- a/internal/characters/sucrose/frames.go
+++ b/internal/characters/sucrose/frames.go
@@ -31,6 +31,8 @@ func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
 		return 11, 57
 	case core.ActionBurst:
 		return 46, 49
+	case core.ActionDash:
+		return 1, 24
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
@@ -88,6 +90,15 @@ func (c *char) InitCancelFrames() {
 	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 11-11)
 	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 11-11)
 	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 56-11)
+
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionAttack, 24-1)
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionCharge, 24-1)
+	//sucrose cancel her dash with her E and Q
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionSkill, 1-1)
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionBurst, 1-1)
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionDash, 24-1)
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionJump, 24-1)
+	c.SetAbilCancelFrames(core.ActionDash, core.ActionSwap, 24-1)
 }
 
 func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
diff --git a/internal/characters/tartaglia/abil.go b/internal/characters/tartaglia/abil.go
index 47b599c8e..ad7f9a8e5 100644
--- a/internal/characters/tartaglia/abil.go
+++ b/internal/characters/tartaglia/abil.go
@@ -173,7 +173,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	c.eCast = c.Core.F
 	c.Core.Status.AddStatus("tartagliamelee", 30*60)
-	c.Core.Log.NewEvent("Foul Legacy acivated", core.LogCharacterEvent, c.Index, "rtexpiry", c.Core.F+30*60)
+	c.Core.Log.NewEvent("Foul Legacy activated", core.LogCharacterEvent, c.Index, "rtexpiry", c.Core.F+30*60)
 
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
@@ -189,7 +189,15 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), f, f)
 
+	src := c.eCast
+	c.AddTask(func() {
+		if src == c.eCast && c.Core.Status.Duration("tartagliamelee") > 0 {
+			c.onExitMeleeStance()
+			c.ResetNormalCounter()
+		}
+	}, "tartagliamelee-cd", 30*60)
 	c.SetCD(core.ActionSkill, 60)
+
 	return f, a
 }
 
@@ -221,7 +229,7 @@ func (c *char) onExitMeleeStance() {
 	}
 
 	if c.mlBurstUsed {
-		c.SetCD(core.ActionSkill, 0)
+		c.ResetActionCooldown(core.ActionSkill)
 		c.mlBurstUsed = false
 	} else {
 		c.SetCD(core.ActionSkill, skillCD)
diff --git a/internal/characters/travelerelectro/abil.go b/internal/characters/travelerelectro/abil.go
index a55a413f8..7303f28eb 100644
--- a/internal/characters/travelerelectro/abil.go
+++ b/internal/characters/travelerelectro/abil.go
@@ -46,10 +46,12 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	hits, ok := p["hits"]
 	if !ok {
 		hits = 1
+	} else if hits > 3 {
+		hits = 3
 	}
 
 	maxAmulets := 2
-	if c.Base.Cons > 0 {
+	if c.Base.Cons >= 1 {
 		maxAmulets = 3
 	}
 
@@ -58,7 +60,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	// accept param to limit the amount of amulets generated
 	pMaxAmulets, ok := p["max_amulets"]
-	if ok {
+	if ok && pMaxAmulets < maxAmulets {
 		maxAmulets = pMaxAmulets
 	}
 
@@ -80,27 +82,30 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		c.QueueParticle(c.Name(), 1, core.Electro, 100) //this way we're future proof if for whatever reason this misses
 	}
 
+	amuletCB := func(atk core.AttackCB) {
+		// generate amulet if generated amulets < limit
+		if c.abundanceAmulets >= maxAmulets {
+			return
+		}
+
+		// 1 amulet per attack
+		c.abundanceAmulets++
+		c.AddTag("generated", c.abundanceAmulets)
+
+		c.Core.Log.NewEvent("travelerelectro abundance amulet generated", core.LogCharacterEvent, c.Index, "amulets", c.abundanceAmulets)
+	}
+
 	for i := 0; i < hits; i++ {
-		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(0.3, false, core.TargettableEnemy), f, particlesCB, func(atk core.AttackCB) {
-			// generate amulet if generated amulets < limit
-			if c.abundanceAmulets >= maxAmulets {
-				return
-			}
-
-			// 1 amulet per attack
-			c.abundanceAmulets++
-			c.AddTag("generated", c.abundanceAmulets)
-
-			c.Core.Log.NewEvent("travelerelectro abundance amulet generated", core.LogCharacterEvent, c.Index, "amulets", c.abundanceAmulets)
-
-			c.AddTask(func() {
-				activeChar := c.Core.Chars[c.Core.ActiveChar]
-				c.collectAmulets(c.Core.F, activeChar)
-			}, fmt.Sprintf("pickup-abundance-amulets-%s", c.Name()), amuletDelay)
-		})
+		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(0.3, false, core.TargettableEnemy), f, particlesCB, amuletCB)
 	}
 
-	c.SetCD(core.ActionSkill, 810+21) //13.5s, starts 21 frames in
+	// try to pick up amulets
+	c.AddTask(func() {
+		activeChar := c.Core.Chars[c.Core.ActiveChar]
+		c.collectAmulets(c.Core.F, activeChar)
+	}, "pickup-abundance-amulets", amuletDelay)
+
+	c.SetCDWithDelay(core.ActionSkill, 810, 21) //13.5s, starts 21 frames in
 
 	return f, a
 }
diff --git a/internal/characters/travelerelectro/amulet.go b/internal/characters/travelerelectro/amulet.go
index 119eb300c..b6fa4dd2f 100644
--- a/internal/characters/travelerelectro/amulet.go
+++ b/internal/characters/travelerelectro/amulet.go
@@ -35,8 +35,8 @@ func (c *char) collectAmulets(f int, collector core.Character) bool {
 
 	// handle a1 - When another nearby character in the party obtains an Abundance Amulet created by Lightning Blade,
 	//   Lightning Blade's CD is decreased by 1.5s.
-	if collector.Key() != core.TravelerElectro {
-		c.ReduceActionCooldown(core.ActionSkill, 90)
+	if collector.CharIndex() != c.Index {
+		c.ReduceActionCooldown(core.ActionSkill, 90*c.abundanceAmulets)
 	}
 
 	// apply ER mod
diff --git a/internal/characters/venti/abil.go b/internal/characters/venti/abil.go
index b9d4f1784..0653f6543 100644
--- a/internal/characters/venti/abil.go
+++ b/internal/characters/venti/abil.go
@@ -6,6 +6,8 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{{17, 27}, {19}, {28}, {15, 28}, {17}, {49}}
+
 func (c *char) Attack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionAttack, p)
@@ -28,7 +30,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	for i, mult := range attack[c.NormalCounter] {
 		ai.Mult = mult[c.TalentLvlAttack()]
 		// TODO - double check snapshotDelay
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f+i, f+travel+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i]+travel)
 	}
 
 	c.AdvanceNormalIndex()
@@ -74,6 +76,8 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
 	cd := 360
+	cdstart := 21
+	hitmark := 51
 	ai := core.AttackInfo{
 		ActorIndex:   c.Index,
 		Abil:         "Skyward Sonnett",
@@ -88,6 +92,8 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	if p["hold"] == 1 {
 		cd = 900
+		cdstart = 34
+		hitmark = 74
 		ai.Mult = skillHold[c.TalentLvlSkill()]
 	}
 
@@ -97,11 +103,11 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		cb = c2cb
 	}
 
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(4, false, core.TargettableEnemy), 0, f-1, cb)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(4, false, core.TargettableEnemy), 0, hitmark, cb)
 
-	c.QueueParticle("venti", 3, core.Anemo, f+100)
+	c.QueueParticle("venti", 3, core.Anemo, hitmark+100)
 
-	c.SetCD(core.ActionSkill, cd)
+	c.SetCDWithDelay(core.ActionSkill, cd, cdstart)
 	return f, a
 }
 
@@ -121,26 +127,59 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       burstDot[c.TalentLvlBurst()],
 	}
-	snap := c.Snapshot(&ai)
+	c.aiAbsorb = ai
+	c.aiAbsorb.Abil = "Wind's Grand Ode (Infused)"
+	c.aiAbsorb.Mult = burstAbsorbDot[c.TalentLvlBurst()]
+	c.aiAbsorb.Element = core.NoElement
+
+	// snapshot is around cd frame and 1st tick?
+	var snap core.Snapshot
+	c.AddTask(func() {
+		snap = c.Snapshot(&ai)
+		c.snapAbsorb = c.Snapshot(&c.aiAbsorb)
+	}, "venti-q-snapshot", 104)
 
 	var cb core.AttackCBFunc
-	if c.Base.Cons == 6 {
+	if c.Base.Cons >= 6 {
 		cb = c6cb(core.Anemo)
 	}
 
-	for i := 24; i <= 480; i += 24 {
-		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(4, false, core.TargettableEnemy), i, cb)
+	// starts at 106 with 24f interval between ticks. 20 total
+	for i := 0; i < 20; i++ {
+		c.AddTask(func() {
+			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(4, false, core.TargettableEnemy), 0, cb)
+		}, "venti-burst-tick", 106+24*i)
 	}
-
 	// Infusion usually occurs after 4 ticks of anemo according to KQM library
-	c.AddTask(c.absorbCheckQ(c.Core.F, 0, int(480/18)-24*4), "venti-absorb-check", 24*4)
+	c.AddTask(c.absorbCheckQ(c.Core.F, 0, int((480-24*4)/18)), "venti-absorb-check", 106+24*3)
 
 	c.AddTask(func() {
 		c.a4Restore()
 	}, "venti-a4-restore", 480+f)
 
-	c.SetCDWithDelay(core.ActionBurst, 15*60, 90)
-	c.ConsumeEnergy(90)
+	c.SetCDWithDelay(core.ActionBurst, 15*60, 81)
+	c.ConsumeEnergy(84)
+	return f, a
+}
+
+func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionHighPlunge, p)
+
+	ai := core.AttackInfo{
+		ActorIndex:     c.Index,
+		Abil:           "Plunge",
+		AttackTag:      core.AttackTagPlunge,
+		ICDTag:         core.ICDTagNormalAttack,
+		ICDGroup:       core.ICDGroupDefault,
+		StrikeType:     core.StrikeTypeBlunt,
+		Element:        core.Physical,
+		Durability:     25,
+		Mult:           highPlunge[c.TalentLvlAttack()],
+		IgnoreInfusion: true,
+	}
+
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1.5, false, core.TargettableEnemy), f, f)
+
 	return f, a
 }
 
@@ -157,24 +196,14 @@ func (c *char) a4Restore() {
 }
 
 func (c *char) burstInfusedTicks() {
-	ai := core.AttackInfo{
-		ActorIndex: c.Index,
-		Abil:       "Wind's Grand Ode (Infused)",
-		AttackTag:  core.AttackTagElementalBurst,
-		ICDTag:     core.ICDTagVentiBurstAnemo,
-		ICDGroup:   core.ICDGroupVenti,
-		Element:    c.qInfuse,
-		Durability: 25,
-		Mult:       burstAbsorbDot[c.TalentLvlBurst()],
-	}
-	snap := c.Snapshot(&ai)
 	var cb core.AttackCBFunc
-	if c.Base.Cons == 6 {
+	if c.Base.Cons >= 6 {
 		cb = c6cb(c.qInfuse)
 	}
 
-	for i := 24; i <= 360; i += 24 {
-		c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(4, false, core.TargettableEnemy), i, cb)
+	// ticks at 24f. 15 total
+	for i := 0; i < 15; i++ {
+		c.Core.Combat.QueueAttackWithSnap(c.aiAbsorb, c.snapAbsorb, core.NewDefCircHit(4, false, core.TargettableEnemy), i*24, cb)
 	}
 }
 
@@ -185,6 +214,17 @@ func (c *char) absorbCheckQ(src, count, max int) func() {
 		}
 		c.qInfuse = c.Core.AbsorbCheck(c.infuseCheckLocation, core.Pyro, core.Hydro, core.Electro, core.Cryo)
 		if c.qInfuse != core.NoElement {
+			c.aiAbsorb.Element = c.qInfuse
+			switch c.qInfuse {
+			case core.Pyro:
+				c.aiAbsorb.ICDTag = core.ICDTagVentiBurstPyro
+			case core.Hydro:
+				c.aiAbsorb.ICDTag = core.ICDTagVentiBurstHydro
+			case core.Electro:
+				c.aiAbsorb.ICDTag = core.ICDTagVentiBurstElectro
+			case core.Cryo:
+				c.aiAbsorb.ICDTag = core.ICDTagVentiBurstCryo
+			}
 			//trigger dmg ticks here
 			c.burstInfusedTicks()
 			return
diff --git a/internal/characters/venti/frames.go b/internal/characters/venti/frames.go
index 02530f5dd..096547c98 100644
--- a/internal/characters/venti/frames.go
+++ b/internal/characters/venti/frames.go
@@ -1,39 +1,115 @@
 package venti
 
-import "github.com/genshinsim/gcsim/pkg/core"
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
 
 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:
-			f = 21 //frames from keqing lib
+			f = 27
+			a = 30
 		case 1:
-			f = 44 - 21
+			f = 19
+			a = 38
 		case 2:
-			f = 90 - 44
+			f = 28
+			a = 33
 		case 3:
-			f = 123 - 90
+			f = 28
+			a = 31
 		case 4:
-			f = 140 - 123
+			f = 17
+			a = 22
 		case 5:
-			f = 191 - 140
+			f = 49
+			a = 98
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionAim:
-		return 86, 86 //frames from keqing libf
+		return 86, 94
+	case core.ActionHighPlunge:
+		c.Core.Log.NewEvent("plunge skill check", core.LogCharacterEvent, c.Index, "previous", c.Core.LastAction)
+		if c.Core.LastAction.Target == core.Venti && c.Core.LastAction.Typ == core.ActionSkill {
+			h := c.Core.LastAction.Param["hold"]
+			if h > 0 {
+				return 58, 58
+			}
+		}
+		c.Core.Log.NewEvent("invalid plunge (missing hold skill use)", core.LogActionEvent, c.Index, "action", a)
+		return 0, 0
 	case core.ActionSkill:
 		if p["hold"] == 0 {
-			return 38, 38
+			return 22, 98
 		}
-		return 65, 65
+		return 116, 174
 	case core.ActionBurst:
-		return 94, 94
+		return 94, 108
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+	//normal cancels
+	//normal cancels
+	c.SetNormalCancelFrames(0, core.ActionAttack, 30-27) //n1 -> next attack
+	//c.SetNormalCancelFrames(0, core.Actionaim, 35-10) //n1 -> aim
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 38-19) //n2 -> next attack
+	//c.SetNormalCancelFrames(1, core.Actionaim, 29-13) //n2 -> aim
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 33-28) //n3 -> next attack
+	//c.SetNormalCancelFrames(2, core.Actionaim, 35-19) //n3 -> aim
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 31-28) //n4 -> next attack
+	//c.SetNormalCancelFrames(3, core.Actionaim, 33-17) //n4 -> aim
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 22-17) //n5 -> next attack
+	//c.SetNormalCancelFrames(4, core.Actionaim, 33-17) //n5 -> aim
+
+	c.SetNormalCancelFrames(5, core.ActionAttack, 98-49) //n6 -> next attack (n1)
+	//c.SetNormalCancelFrames(5, core.Actionaim, 33-17) //n6 -> aim
+
+	//todo: get separate counts for each cancel, currently using generic frames for all of them
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAttack, 94-86) //aim -> n1
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAim, 94-86)    //aim -> aim
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSkill, 94-86)  //aim -> skill
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionBurst, 94-86)  //aim -> burst
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSwap, 94-86)   //aim -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 95-94) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAim, 95-94)    //burst -> aim (assumed)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 96-94)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 95-94)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 95-94)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 94-94)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 22-22)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAim, 22-22) //assumed
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 22-22)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 22-22)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 22-22)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 98-22)
+
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionHighPlunge, 116-116) //plunge should take 58 frames to hit
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionAttack, 289-116)     //float down if you didn't plunge
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionAim, 289-116)
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionSkill, 289-116)
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionBurst, 289-116)
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionDash, 289-116)
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionJump, 289-116)
+	c.SetAbilCancelFrames(core.ActionSkillHoldFramesOnly, core.ActionSwap, 289-116)
+
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/venti/stats.go b/internal/characters/venti/stats.go
index b9ed23426..f0d436d43 100644
--- a/internal/characters/venti/stats.go
+++ b/internal/characters/venti/stats.go
@@ -243,4 +243,16 @@ var (
 		0.423,
 		0.4465,
 	}
+	highPlunge = []float64{
+		1.4193,
+		1.5349,
+		1.6504,
+		1.931,
+		2.063,
+		2.2445,
+		2.4261,
+		2.6076,
+		2.8057,
+		3.0037,
+	}
 )
diff --git a/internal/characters/venti/venti.go b/internal/characters/venti/venti.go
index 06f9dbc41..7ae08289b 100644
--- a/internal/characters/venti/venti.go
+++ b/internal/characters/venti/venti.go
@@ -7,8 +7,10 @@ import (
 
 type char struct {
 	*character.Tmpl
-	qInfuse             core.EleType
+	qInfuse    core.EleType
 	infuseCheckLocation core.AttackPattern
+	aiAbsorb   core.AttackInfo
+	snapAbsorb core.Snapshot
 }
 
 func init() {
@@ -34,6 +36,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.NormalHitNum = 6
 	c.BurstCon = 3
 	c.SkillCon = 5
+	c.InitCancelFrames()
 
 	c.infuseCheckLocation = core.NewDefCircHit(0.1, false, core.TargettableEnemy, core.TargettablePlayer, core.TargettableObject)
 
diff --git a/internal/characters/xiangling/frames.go b/internal/characters/xiangling/frames.go
new file mode 100644
index 000000000..ea34d6aa4
--- /dev/null
+++ b/internal/characters/xiangling/frames.go
@@ -0,0 +1,82 @@
+package xiangling
+
+import "github.com/genshinsim/gcsim/pkg/core"
+
+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 {
+		case 0:
+			f = 12
+			a = 20
+		case 1:
+			f = 8
+			a = 17
+		case 2:
+			f = 18
+			a = 28
+		case 3:
+			f = 29
+			a = 37
+		case 4:
+			f = 21
+			a = 71
+		}
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+		return f, a
+	case core.ActionSkill:
+		return 14, 29
+	case core.ActionBurst:
+		return 18, 56
+	case core.ActionCharge:
+		return 24, 67
+	case core.ActionDash:
+		return 20, 20
+	case core.ActionJump:
+		return 30, 30
+	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, 20-12) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 20-12) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 17-8) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 17-8) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 28-18) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 24-18) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 37-29) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 34-29) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 71-21) //n5 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 67-24) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 69-24)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 67-24)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 66-24)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 80-18) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 80-18)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 80-18)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 80-18)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 79-18)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 39-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 39-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 14-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 14-14)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 38-14)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/xiangling/xiangling.go b/internal/characters/xiangling/xiangling.go
index fc9a8a3f7..a10b7f4f0 100644
--- a/internal/characters/xiangling/xiangling.go
+++ b/internal/characters/xiangling/xiangling.go
@@ -42,42 +42,13 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	//add in a guoba
 	c.guoba = newGuoba(c.Core)
 	c.Core.AddTarget(c.guoba)
 }
 
-func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
-	switch a {
-	case core.ActionAttack:
-		f := 0
-		switch c.NormalCounter {
-		case 0:
-			f = 12
-		case 1:
-			f = 38 - 12
-		case 2:
-			f = 72 - 38
-		case 3:
-			f = 141 - 72
-		case 4:
-			f = 167 - 141
-		}
-		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
-	case core.ActionSkill:
-		return 26, 26
-	case core.ActionBurst:
-		return 99, 99
-	case core.ActionCharge:
-		return 78, 78
-	default:
-		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
-		return 0, 0
-	}
-}
-
 func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
 	switch a {
 	case core.ActionDash:
@@ -108,6 +79,8 @@ func (c *char) c6(dur int) {
 	}
 }
 
+var hitmarks = [][]int{{12}, {8}, {11, 18}, {5, 15, 24, 29}, {21}}
+
 func (c *char) Attack(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionAttack, p)
 	ai := core.AttackInfo{
@@ -125,8 +98,8 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		c.Core.Combat.QueueAttack(
 			ai,
 			core.NewDefCircHit(0.1, false, core.TargettableEnemy),
-			f-i,
-			f-i,
+			hitmarks[c.NormalCounter][i],
+			hitmarks[c.NormalCounter][i],
 		)
 	}
 
@@ -144,7 +117,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 			Durability: 25,
 			Mult:       .75,
 		}
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 120, 120)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 120, 120) //todo: explosion frames
 	}
 	//add a 75 frame attackcounter reset
 	c.AdvanceNormalIndex()
@@ -165,7 +138,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       nc[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	//return animation cd
 	return f, a
@@ -186,7 +159,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 	}
 
 	var cb core.AttackCBFunc
-	if c.Base.Cons > 1 {
+	if c.Base.Cons >= 1 {
 		cb = func(a core.AttackCB) {
 			a.Target.AddResMod("xiangling-c1", core.ResistMod{
 				Ele:      core.Pyro,
@@ -197,22 +170,22 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	}
 
-	delay := 120
-	c.Core.Status.AddStatus("xianglingguoba", 500)
+	delay := 126 //first tick at 126
+	c.Core.Status.AddStatus("xianglingguoba", 500+f)
 
-	//lasts 73 seconds, shoots every 1.6 seconds
+	//lasts 7.3 seconds, shoots every 100 frames
 	snap := c.Snapshot(&ai)
 	for i := 0; i < 4; i++ {
 		c.AddTask(func() {
 			c.Core.Combat.QueueAttackWithSnap(ai, snap, core.NewDefCircHit(0.5, false, core.TargettableEnemy), 10, cb)
 			c.guoba.pyroWindowStart = c.Core.F
 			c.guoba.pyroWindowEnd = c.Core.F + 20
-		}, "guoba-shoot", delay+i*90-10) //10 frame window to swirl
-		//TODO: check guoba fire delay
-		c.QueueParticle("xiangling", 1, core.Pyro, delay+i*95+90+60)
+		}, "guoba-shoot", delay+i*100-10) //10 frame window to swirl
+		//TODO: check guoba particle generation
+		c.QueueParticle("xiangling", 1, core.Pyro, delay+i*100+150)
 	}
 
-	c.SetCD(core.ActionSkill, 12*60)
+	c.SetCDWithDelay(core.ActionSkill, 12*60, 13)
 	//return animation cd
 	return f, a
 }
@@ -221,7 +194,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionBurst, p)
 	lvl := c.TalentLvlBurst()
 
-	delay := []int{34, 50, 75}
+	delay := []int{18, 33, 56}
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
 		AttackTag:  core.AttackTagElementalBurst,
@@ -237,11 +210,11 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.5, false, core.TargettableEnemy), delay[i], delay[i])
 	}
 
-	//ok for now we assume it's 80 (or 70??) frames per cycle, that gives us roughly 10s uptime
-	//max is either 10s or 14s
-	max := 10 * 60
+	//approx 73 frames per cycle
+	//max is either 10s or 14s, plus animation
+	max := 10*60 + a
 	if c.Base.Cons >= 4 {
-		max = 14 * 60
+		max = 14*60 + a
 	}
 
 	ai = core.AttackInfo{
@@ -257,21 +230,31 @@ func (c *char) Burst(p map[string]int) (int, int) {
 
 	c.Core.Status.AddStatus("xianglingburst", max)
 
-	for delay := 70; delay <= max; delay += 70 {
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2.5, false, core.TargettableEnemy), 69, delay)
+	for delay := 56; delay <= max; delay += 73 { //first hit on same frame as 3rd initial hit
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2.5, false, core.TargettableEnemy), 54, delay)
 	}
 
-	//add an effect starting at frame 70 to end of duration to increase pyro dmg by 15% if c6
+	//add an effect starting at frame 55 to end of duration to increase pyro dmg by 15% if c6
 	if c.Base.Cons >= 6 {
-		//wait 70 frames, add effect
-		c.AddTask(func() { c.c6(max) }, "xiangling-c6", 70)
+		//wait 55 frames, add effect.
+		c.AddTask(func() { c.c6(max) }, "xiangling-c6", 55)
 	}
 
 	//add cooldown to sim
-	c.SetCDWithDelay(core.ActionBurst, 20*60, 29)
+	c.SetCDWithDelay(core.ActionBurst, 20*60, 18)
 	//use up energy
-	c.ConsumeEnergy(29)
+	c.ConsumeEnergy(24)
 
 	//return animation cd
 	return f, a
 }
+
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
+	return f, a
+}
+
+func (c *char) Jump(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionJump, p)
+	return f, a
+}
diff --git a/internal/characters/xiao/abil.go b/internal/characters/xiao/abil.go
index b821905c5..e2b00cfaa 100644
--- a/internal/characters/xiao/abil.go
+++ b/internal/characters/xiao/abil.go
@@ -6,6 +6,8 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{{4, 17}, {15}, {15}, {14, 31}, {16}, {39}}
+
 // Normal attack damage queue generator
 // relatively standard with no major differences versus other characters
 func (c *char) Attack(p map[string]int) (int, int) {
@@ -24,7 +26,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.1, false, core.TargettableEnemy), f-5+i, f-5+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -51,18 +53,20 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       charge[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	//return animation cd
 	return f, a
 }
 
+var collisionFrame = 38
+
 // Plunge normal falling attack damage queue generator
 // Standard - Always part of high/low plunge attacks
 func (c *char) PlungeAttack(delay int) (int, int) {
 	ai := core.AttackInfo{
 		ActorIndex: c.Index,
-		Abil:       "Plunge (Normal)",
+		Abil:       "Plunge Collision",
 		AttackTag:  core.AttackTagPlunge,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
@@ -77,21 +81,19 @@ func (c *char) PlungeAttack(delay int) (int, int) {
 }
 
 // High Plunge attack damage queue generator
-// Use the "plunge_hits" optional argument to determine how many plunge falling hits you do on the way down
+// Use the "collision" optional argument if you want to do a falling hit on the way down
 // Default = 0
 func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionHighPlunge, p)
 
-	plungeHits, ok := p["plunge_hits"]
+	collision, ok := p["collision"]
 	if !ok {
-		plungeHits = 0 // Number of normal plunge hits
+		collision = 0 // Whether or not Xiao does a collision hit
 	}
 
-	for i := 0; i < plungeHits; i++ {
-		// Add plunge attack in each frame leading up to final hit for now - not sure we have clear mechanics on this
-		// TODO: Perhaps amend later, but functionally in combat you usually get at most one of these anyway
-		c.PlungeAttack(f - i - 2)
+	if collision > 0 {
+		c.PlungeAttack(collisionFrame)
 	}
 
 	ai := core.AttackInfo{
@@ -105,28 +107,26 @@ func (c *char) HighPlungeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       highplunge[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f, f)
 
 	//return animation cd
 	return f, a
 }
 
 // Low Plunge attack damage queue generator
-// Use the "plunge_hits" optional argument to determine how many plunge falling hits you do on the way down
+// Use the "collision" optional argument if you want to do a falling hit on the way down
 // Default = 0
 func (c *char) LowPlungeAttack(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionLowPlunge, p)
 
-	plungeHits, ok := p["plunge_hits"]
+	collision, ok := p["collision"]
 	if !ok {
-		plungeHits = 0 // Number of normal plunge hits
+		collision = 0 // Whether or not Xiao does a collision hit
 	}
 
-	for i := 0; i < plungeHits; i++ {
-		// Add plunge attack in each frame leading up to final hit for now - not sure we have clear mechanics on this
-		// TODO: Perhaps amend later, but functionally in combat you usually get at most one of these anyway
-		c.PlungeAttack(f - i - 2)
+	if collision > 0 {
+		c.PlungeAttack(collisionFrame)
 	}
 
 	ai := core.AttackInfo{
@@ -140,7 +140,7 @@ func (c *char) LowPlungeAttack(p map[string]int) (int, int) {
 		Durability: 25,
 		Mult:       lowplunge[c.TalentLvlAttack()],
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), f, f)
 
 	//return animation cd
 	return f, a
@@ -152,13 +152,6 @@ func (c *char) LowPlungeAttack(p map[string]int) (int, int) {
 func (c *char) Skill(p map[string]int) (int, int) {
 
 	f, a := c.ActionFrames(core.ActionSkill, p)
-	// According to KQM library, double E is only 60 frames long whereas single cast is 36
-	// No idea how this works, but add a special case to reduce the frames of the 2nd cast
-	// TODO: No data listed on how 3 casts work - this might be too few frames compared to actual 3x usage
-	if c.Core.LastAction.Target == core.Xiao && c.Core.LastAction.Typ == core.ActionSkill {
-		f = 60 - f
-		a = 60 - a
-	}
 
 	// Add damage based on A4
 	if c.a4Expiry <= c.Core.F {
@@ -170,7 +163,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		Abil:       "Lemniscatic Wind Cycling",
 		AttackTag:  core.AttackTagElementalArt,
 		ICDTag:     core.ICDTagElementalArt,
-		ICDGroup:   core.ICDGroupDefault,
+		ICDGroup:   core.ICDGroupXiaoDash,
 		Element:    core.Anemo,
 		Durability: 25,
 		Mult:       skill[c.TalentLvlSkill()],
@@ -229,9 +222,18 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		}, "xiaoburst-hp-drain", i)
 	}
 
-	// Checked gameplay - burst starts ticking down from activation. CD is 16.6 seconds after animation is done
-	c.SetCDWithDelay(core.ActionBurst, 18*60, 39)
-	c.ConsumeEnergy(39)
+	c.SetCDWithDelay(core.ActionBurst, 18*60, 29)
+	c.ConsumeEnergy(36)
+
+	return f, a
+}
+
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
+	return f, a
+}
 
+func (c *char) Jump(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionJump, p)
 	return f, a
 }
diff --git a/internal/characters/xiao/frames.go b/internal/characters/xiao/frames.go
index 1f0f2b3be..9262cb41a 100644
--- a/internal/characters/xiao/frames.go
+++ b/internal/characters/xiao/frames.go
@@ -6,36 +6,132 @@ 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:
-			f = 16 //frames from keqing lib
+			f = 17
+			a = 25
 		case 1:
-			f = 42 - 16
+			f = 15
+			a = 22
 		case 2:
-			f = 68 - 42
+			f = 15
+			a = 26
 		case 3:
-			f = 115 - 68
+			f = 31
+			a = 39
 		case 4:
-			f = 145 - 115
+			f = 16
+			a = 24
 		case 5:
-			f = 197 - 145
+			f = 39
+			a = 79
 		}
 		atkspd := c.Stat(core.AtkSpd)
 		f = int(float64(f) / (1 + atkspd))
-		return f, f
+		return f, a
 	case core.ActionCharge:
-		return 80 - 16, 80 - 16 // Taken as N1C - N1. Not entirely right due to different recovery frames
+		return 17, 46
 	case core.ActionHighPlunge:
-		return 75, 75
+		c.Core.Log.NewEvent("plunge jump check", core.LogCharacterEvent, c.Index, "previous", c.Core.LastAction)
+		if c.Core.LastAction.Target == core.Xiao && c.Core.LastAction.Typ == core.ActionJump {
+			return 46, 61
+		}
+		c.Core.Log.NewEvent("invalid plunge (missing jump)", core.LogActionEvent, c.Index, "action", a)
+		return 0, 0
 	case core.ActionLowPlunge:
-		return 65, 65
+		c.Core.Log.NewEvent("plunge jump check", core.LogCharacterEvent, c.Index, "previous", c.Core.LastAction)
+		if c.Core.LastAction.Target == core.Xiao && c.Core.LastAction.Typ == core.ActionJump {
+			return 44, 60
+		}
+		c.Core.Log.NewEvent("invalid plunge (missing jump)", core.LogActionEvent, c.Index, "action", a)
+		return 0, 0
 	case core.ActionSkill:
-		return 36, 36
+		return 4, 24
 	case core.ActionBurst:
-		return 58, 58
+		return 57, 82
+	case core.ActionDash:
+		return 21, 21
+	case core.ActionJump:
+		if c.Core.Status.Duration("xiaoburst") > 0 {
+			return 5, 58
+		}
+		return 32, 32
 	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, 25-17) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 26-17) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 22-15) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 27-15) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 26-15) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 38-15) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 39-31) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 42-31) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 24-16) //n5 -> next attack
+	c.SetNormalCancelFrames(4, core.ActionCharge, 30-16) //n5 -> charge
+
+	c.SetNormalCancelFrames(5, core.ActionAttack, 79-39) //n5 -> n6
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 45-17) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 38-17)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 37-17)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 43-17)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 82-57) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 82-57)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 57-57)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 58-57)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 67-57)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 24-4)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSkill, 24-4)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 24-4)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 35-4)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 37-4)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 35-4)
+
+	c.SetAbilCancelFrames(core.ActionHighPlunge, core.ActionAttack, 61-46)
+	c.SetAbilCancelFrames(core.ActionHighPlunge, core.ActionSkill, 66-46)
+	c.SetAbilCancelFrames(core.ActionHighPlunge, core.ActionDash, 66-46)
+	c.SetAbilCancelFrames(core.ActionHighPlunge, core.ActionJump, 65-46)
+	c.SetAbilCancelFrames(core.ActionHighPlunge, core.ActionSwap, 64-46)
+
+	c.SetAbilCancelFrames(core.ActionLowPlunge, core.ActionAttack, 60-44)
+	c.SetAbilCancelFrames(core.ActionLowPlunge, core.ActionSkill, 59-44)
+	c.SetAbilCancelFrames(core.ActionLowPlunge, core.ActionDash, 60-44)
+	c.SetAbilCancelFrames(core.ActionLowPlunge, core.ActionJump, 61-44)
+	c.SetAbilCancelFrames(core.ActionLowPlunge, core.ActionSwap, 62-44)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Xiao's Jump
+	if c.Core.LastAction.Typ == core.ActionJump &&
+		c.Core.Status.Duration("xiaoburst") > 0 {
+		return BurstJumpFrames(next)
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func BurstJumpFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionHighPlunge:
+		return 6 - 5
+	case core.ActionLowPlunge:
+		return 5 - 5
+	default:
+		return 58 - 5 //if jump was not followed by plunge, he must wait to float down
+	}
+}
diff --git a/internal/characters/xiao/xiao.go b/internal/characters/xiao/xiao.go
index 42ecdcec3..aa876b5d4 100644
--- a/internal/characters/xiao/xiao.go
+++ b/internal/characters/xiao/xiao.go
@@ -52,6 +52,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.onExitField()
 
diff --git a/internal/characters/xingqiu/frames.go b/internal/characters/xingqiu/frames.go
new file mode 100644
index 000000000..c260ad991
--- /dev/null
+++ b/internal/characters/xingqiu/frames.go
@@ -0,0 +1,82 @@
+package xingqiu
+
+import (
+	"github.com/genshinsim/gcsim/pkg/core"
+)
+
+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:
+			f = 10
+			a = 18
+		case 1:
+			f = 13
+			a = 24
+		case 2:
+			f = 19
+			a = 26
+		case 3:
+			f = 17
+			a = 28
+		case 4:
+			f = 39
+			a = 66
+		}
+		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
+		return f, a
+	case core.ActionCharge:
+		return 20, 58
+	case core.ActionSkill:
+		return 30, 67
+	case core.ActionBurst:
+		return 18, 33
+	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, 18-10) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 35-10) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 24-13) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 29-13) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 26-19) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 35-19) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 28-17) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 33-17) //n4 -> charge
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 66-39) //n5 -> next attack (n1)
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 58-20) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 32-20)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 32-20)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 31-20)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 33-18) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 33-18)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 33-18)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 33-18)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 40-18)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 67-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSkill, 65-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 67-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 30-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 34-30)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 67-30)
+
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/xingqiu/stats.go b/internal/characters/xingqiu/stats.go
index 000ab7c57..1a726f961 100644
--- a/internal/characters/xingqiu/stats.go
+++ b/internal/characters/xingqiu/stats.go
@@ -192,4 +192,32 @@ var (
 			},
 		},
 	}
+	ca = [][]float64{
+		{
+			.473,
+			.5115,
+			.55,
+			.605,
+			.6435,
+			.6875,
+			.748,
+			.8085,
+			.869,
+			.935,
+			1.0106,
+		},
+		{
+			.5616,
+			.6073,
+			.653,
+			.7183,
+			.764,
+			.8163,
+			.8881,
+			.9599,
+			1.0317,
+			1.1101,
+			1.1999,
+		},
+	}
 )
diff --git a/internal/characters/xingqiu/xingqiu.go b/internal/characters/xingqiu/xingqiu.go
index 102125a7e..a9b26ef55 100644
--- a/internal/characters/xingqiu/xingqiu.go
+++ b/internal/characters/xingqiu/xingqiu.go
@@ -42,6 +42,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	c.SkillCon = 5
 	c.NormalHitNum = 5
 	c.CharZone = core.ZoneLiyue
+	c.InitCancelFrames()
 
 	return &c, nil
 }
@@ -66,38 +67,7 @@ func (c *char) a4() {
 	})
 }
 
-func (c *char) ActionFrames(a core.ActionType, p map[string]int) (int, int) {
-	switch a {
-	case core.ActionAttack:
-		f := 0
-		switch c.NormalCounter {
-		//TODO: need to add atkspd mod
-		case 0:
-			f = 9
-		case 1:
-			f = 25
-		case 2:
-			f = 44
-		case 3:
-			f = 37
-		case 4:
-			f = 79
-		}
-		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
-	case core.ActionCharge:
-		return 63, 63
-	case core.ActionSkill:
-		return 77, 77 //should be 82
-	case core.ActionBurst:
-		return 39, 39 //ok
-	default:
-		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
-		return 0, 0
-	}
-}
-
-var hitmarks = [][]int{{8}, {24}, {24, 43}, {36}, {43, 78}}
+var hitmarks = [][]int{{10}, {13}, {9, 19}, {17}, {18, 39}}
 
 func (c *char) Attack(p map[string]int) (int, int) {
 	//apply attack speed
@@ -125,6 +95,43 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	return f, a
 }
 
+var caHitmarks = [2]int{8, 20}
+
+func (c *char) ChargeAttack(p map[string]int) (int, int) {
+
+	f, a := c.ActionFrames(core.ActionCharge, p)
+
+	ai := core.AttackInfo{
+		Abil:       "Charge",
+		ActorIndex: c.Index,
+		AttackTag:  core.AttackTagExtra,
+		ICDTag:     core.ICDTagExtraAttack,
+		ICDGroup:   core.ICDGroupDefault,
+		Element:    core.Physical,
+		Durability: 25,
+	}
+
+	for i, mult := range ca {
+		ai.Mult = mult[c.TalentLvlAttack()]
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), caHitmarks[i], caHitmarks[i])
+	}
+
+	return f, a
+}
+
+func (c *char) ActionStam(a core.ActionType, p map[string]int) float64 {
+	switch a {
+	case core.ActionDash:
+		return 18
+	case core.ActionCharge:
+		return 20
+	default:
+		c.Core.Log.NewEvent("ActionStam not implemented", core.LogActionEvent, c.Index, "action", a.String())
+		return 0
+	}
+
+}
+
 func (c *char) orbitalfunc(src int) func() {
 	return func() {
 		c.Core.Log.NewEvent("orbital checking tick", core.LogCharacterEvent, c.Index, "expiry", c.Core.Status.Duration("xqorb"), "src", src)
@@ -135,7 +142,7 @@ func (c *char) orbitalfunc(src int) func() {
 
 		ai := core.AttackInfo{
 			ActorIndex: c.Index,
-			Abil:       "Xingqiu Skill (Orbital)",
+			Abil:       "Xingqiu Orbital",
 			AttackTag:  core.AttackTagNone,
 			ICDTag:     core.ICDTagNone,
 			ICDGroup:   core.ICDGroupDefault,
@@ -145,19 +152,19 @@ func (c *char) orbitalfunc(src int) func() {
 		c.Core.Log.NewEvent("orbital ticked", core.LogCharacterEvent, c.Index, "next expected tick", c.Core.F+150, "expiry", c.Core.Status.Duration("xqorb"), "src", src)
 
 		//queue up next instance
-		c.AddTask(c.orbitalfunc(src), "xq-skill-orbital", 135)
+		c.AddTask(c.orbitalfunc(src), "xq-orbital", 135)
 
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), -1, 1)
 	}
 }
 
-func (c *char) applyOrbital(duration int) {
+func (c *char) applyOrbital(duration int, delay int) {
 	f := c.Core.F
 	c.Core.Log.NewEvent("Applying orbital", core.LogCharacterEvent, c.Index, "current status", c.Core.Status.Duration("xqorb"))
 	//check if orbitals already active, if active extend duration
 	//other wise start first tick func
 	if !c.orbitalActive {
-		c.AddTask(c.orbitalfunc(f), "xq-skill-orbital", 14)
+		c.AddTask(c.orbitalfunc(f), "xq-orbital", delay)
 		c.orbitalActive = true
 		c.Core.Log.NewEvent("orbital applied", core.LogCharacterEvent, c.Index, "expected end", f+900, "next expected tick", f+40)
 	}
@@ -166,7 +173,7 @@ func (c *char) applyOrbital(duration int) {
 	c.Core.Log.NewEvent("orbital duration extended", core.LogCharacterEvent, c.Index, "new expiry", c.Core.Status.Duration("xqorb"))
 }
 
-var rainscreenDelay = [2]int{19, 35}
+var rainscreenDelay = [2]int{12, 31}
 
 func (c *char) Skill(p map[string]int) (int, int) {
 	//applies wet to self 30 frame after cast, sword applies wet every 2.5seconds, so should be 7 seconds
@@ -194,18 +201,20 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(1, false, core.TargettableEnemy), rainscreenDelay[i], rainscreenDelay[i])
 	}
 
-	// Orbitals spawn in 1 frame before the second hit connects going by the "Wet" text
-	c.AddTask(func() {
-		orbital := p["orbital"]
-		if orbital == 1 {
-			c.applyOrbital(15 * 60)
-		}
-	}, "xingqiu-spawn-orbitals", 34)
+	orbital, ok := p["orbital"]
+	if !ok {
+		orbital = 1
+	}
+
+	// orbitals apply wet at 44f
+	if orbital == 1 {
+		c.applyOrbital(15*60, 43) //takes 1 frame to apply it
+	}
 
 	c.QueueParticle(c.Base.Key.String(), 5, core.Hydro, 100)
 
 	//should last 15s, cd 21s
-	c.SetCD(core.ActionSkill, 21*60)
+	c.SetCDWithDelay(core.ActionSkill, 21*60, 10)
 	return f, a
 }
 
@@ -235,20 +244,24 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		dur += 3
 	}
 	dur = dur * 60
-	c.Core.Status.AddStatus("xqburst", dur)
-	c.Core.Log.NewEvent("Xingqiu burst activated", core.LogCharacterEvent, c.Index, "expiry", c.Core.F+dur)
+	c.Core.Status.AddStatus("xqburst", dur+a)
+	c.Core.Log.NewEvent("Xingqiu burst activated", core.LogCharacterEvent, c.Index, "expiry", c.Core.F+dur+a)
 
-	orbital := p["orbital"]
+	orbital, ok := p["orbital"]
+	if !ok {
+		orbital = 1
+	}
 
 	if orbital == 1 {
-		c.applyOrbital(dur)
+		c.applyOrbital(dur, f)
 	}
 
 	c.burstCounter = 0
 	c.numSwords = 2
+	c.nextRegen = false
 
 	// c.CD[combat.BurstCD] = c.S.F + 20*60
-	c.SetCDWithDelay(core.ActionBurst, 20*60, 7)
-	c.ConsumeEnergy(7)
+	c.SetCD(core.ActionBurst, 20*60)
+	c.ConsumeEnergy(3)
 	return f, a
 }
diff --git a/internal/characters/yanfei/abil.go b/internal/characters/yanfei/abil.go
index 92845f4e1..1db8d1474 100644
--- a/internal/characters/yanfei/abil.go
+++ b/internal/characters/yanfei/abil.go
@@ -77,7 +77,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		AttackTag:  core.AttackTagExtra,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
-		// StrikeType: core.StrikeTypeBlunt,
+		StrikeType: core.StrikeTypeBlunt,
 		Element:    core.Pyro,
 		Durability: 25,
 		Mult:       charge[stacks][c.TalentLvlAttack()],
@@ -122,7 +122,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		AttackTag:  core.AttackTagElementalArt,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
-		// StrikeType: core.StrikeTypeBlunt,
+		StrikeType: core.StrikeTypeBlunt,
 		Element:    core.Pyro,
 		Durability: 25,
 		Mult:       skill[c.TalentLvlSkill()],
@@ -178,7 +178,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		AttackTag:  core.AttackTagElementalBurst,
 		ICDTag:     core.ICDTagNone,
 		ICDGroup:   core.ICDGroupDefault,
-		// StrikeType: core.StrikeTypeBlunt,
+		StrikeType: core.StrikeTypeBlunt,
 		Element:    core.Pyro,
 		Durability: 50,
 		Mult:       burst[c.TalentLvlBurst()],
diff --git a/internal/characters/yanfei/yanfei.go b/internal/characters/yanfei/yanfei.go
index 1a47f0910..34829b615 100644
--- a/internal/characters/yanfei/yanfei.go
+++ b/internal/characters/yanfei/yanfei.go
@@ -113,7 +113,6 @@ func (c *char) a4() {
 			AttackTag:  core.AttackTagExtra,
 			ICDTag:     core.ICDTagNone,
 			ICDGroup:   core.ICDGroupDefault,
-			StrikeType: core.StrikeTypeBlunt,
 			Element:    core.Pyro,
 			Durability: 25,
 			Mult:       0.8,
diff --git a/internal/characters/yoimiya/abil.go b/internal/characters/yoimiya/abil.go
index 7f72cca27..8823b88ed 100644
--- a/internal/characters/yoimiya/abil.go
+++ b/internal/characters/yoimiya/abil.go
@@ -6,6 +6,8 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{{15, 24}, {17}, {25}, {11, 26}, {17}}
+
 func (c *char) Attack(p map[string]int) (int, int) {
 	travel, ok := p["travel"]
 	if !ok {
@@ -31,11 +33,9 @@ func (c *char) Attack(p map[string]int) (int, int) {
 		totalMV += mult[c.TalentLvlAttack()]
 
 		// TODO - double check snapshotDelay
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-5+i, travel+f-5+i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], travel+hitmarks[c.NormalCounter][i])
 	}
 
-	c.AdvanceNormalIndex()
-
 	if c.Base.Cons == 6 && c.Core.Status.Duration("yoimiyaskill") > 0 && c.Core.Rand.Float64() < 0.5 {
 		//trigger attack
 		ai := core.AttackInfo{
@@ -48,10 +48,13 @@ func (c *char) Attack(p map[string]int) (int, int) {
 			Durability: 25,
 			Mult:       totalMV * 0.6,
 		}
+		//TODO: frames?
 		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), 0, travel+f+5)
 
 	}
 
+	c.AdvanceNormalIndex()
+
 	if c.Core.Status.Duration("yoimiyaskill") > 0 {
 		if c.lastPart < c.Core.F || c.lastPart == 0 {
 			c.lastPart = c.Core.F + 300 //every 5 second
@@ -82,14 +85,14 @@ func (c *char) onExit() {
 func (c *char) Skill(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
-	c.Core.Status.AddStatus("yoimiyaskill", 600) //activate for 10
+	c.Core.Status.AddStatus("yoimiyaskill", 600+f) //activate for 10
 	// log.Println(c.Core.Status.Duration("yoimiyaskill"))
 
 	if c.Core.Status.Duration("yoimiyaa1") == 0 {
 		c.a1stack = 0
 	}
 
-	c.SetCD(core.ActionSkill, 1080)
+	c.SetCDWithDelay(core.ActionSkill, 1080, 11)
 	return f, a
 }
 
@@ -137,7 +140,7 @@ func (c *char) Burst(p map[string]int) (int, int) {
 	}, "start-blaze", f)
 
 	//add cooldown to sim
-	c.SetCDWithDelay(core.ActionBurst, 15*60, 5)
+	c.SetCD(core.ActionBurst, 15*60)
 	//use up energy
 	c.ConsumeEnergy(5)
 
@@ -205,3 +208,13 @@ func (c *char) burstHook() {
 		}, "yoimiya-died")
 	}
 }
+
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
+	return f, a
+}
+
+func (c *char) Jump(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionJump, p)
+	return f, a
+}
diff --git a/internal/characters/yoimiya/frames.go b/internal/characters/yoimiya/frames.go
index 54fc3197a..096b8904a 100644
--- a/internal/characters/yoimiya/frames.go
+++ b/internal/characters/yoimiya/frames.go
@@ -6,29 +6,83 @@ 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:
-			f = 29
+			f = 24
+			a = 35
 		case 1:
-			f = 25
+			f = 17
+			a = 26
 		case 2:
-			f = 31
+			f = 25
+			a = 39
 		case 3:
-			f = 44
+			f = 26
+			a = 44
 		case 4:
-			f = 29
+			f = 17
+			a = 52
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionAim:
-		return 94, 94
+		return 86, 97
 	case core.ActionSkill:
-		return 19, 19 //should be 82
+		return 11, 31
 	case core.ActionBurst:
-		return 129, 129 //ok
+		return 75, 114
+	case core.ActionDash:
+		return 20, 20
+	case core.ActionJump:
+		return 30, 30
 	default:
 		c.Core.Log.NewEventBuildMsg(core.LogActionEvent, c.Index, "unknown action (invalid frames): ", a.String())
 		return 0, 0
 	}
 }
+
+func (c *char) InitCancelFrames() {
+	//normal cancels (missing Nx -> Aim)
+	c.SetNormalCancelFrames(0, core.ActionAttack, 35-24) //n1 -> next attack
+	//c.SetNormalCancelFrames(0, core.Actionaim, 35-10) //n1 -> aim
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 26-17) //n2 -> next attack
+	//c.SetNormalCancelFrames(1, core.Actionaim, 29-13) //n2 -> aim
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 39-25) //n3 -> next attack
+	//c.SetNormalCancelFrames(2, core.Actionaim, 35-19) //n3 -> aim
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 44-26) //n4 -> next attack
+	//c.SetNormalCancelFrames(3, core.Actionaim, 33-17) //n4 -> aim
+
+	c.SetNormalCancelFrames(4, core.ActionAttack, 52-17) //n5 -> n1
+	//c.SetNormalCancelFrames(4, core.Actionaim, 33-17) //n5 -> aim
+
+	//todo: get separate counts for each cancel, currently using generic frames for all of them
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAttack, 97-86) //aim -> n1
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionAim, 97-86)    //aim -> aim
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSkill, 97-86)  //aim -> skill
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionBurst, 97-86)  //aim -> burst
+	c.SetAbilCancelFrames(core.ActionAim, core.ActionSwap, 97-86)   //aim -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 114-75) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAim, 114-75)    //burst -> aim (assumed)
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 110-75)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 111-75)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 113-75)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 109-75)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 22-11)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAim, 22-11) //assumed
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 23-11)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 34-11)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 32-11)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 31-11)
+
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
diff --git a/internal/characters/yoimiya/yoimiya.go b/internal/characters/yoimiya/yoimiya.go
index 162f11481..9ef0cdaa4 100644
--- a/internal/characters/yoimiya/yoimiya.go
+++ b/internal/characters/yoimiya/yoimiya.go
@@ -30,7 +30,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 	}
 	c.Energy = float64(e)
 	c.EnergyMax = 60
-	c.Weapon.Class = core.WeaponClassSword
+	c.Weapon.Class = core.WeaponClassBow
 	c.NormalHitNum = 5
 	c.BurstCon = 5
 	c.SkillCon = 3
@@ -41,6 +41,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.a1()
 	c.onExit()
diff --git a/internal/characters/zhongli/abil.go b/internal/characters/zhongli/abil.go
index ee88b4c52..f04d32d04 100644
--- a/internal/characters/zhongli/abil.go
+++ b/internal/characters/zhongli/abil.go
@@ -6,6 +6,8 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core"
 )
 
+var hitmarks = [][]int{{11}, {9}, {8}, {16}, {11, 18, 23, 29}, {29}}
+
 func (c *char) Attack(p map[string]int) (int, int) {
 	f, a := c.ActionFrames(core.ActionAttack, p)
 	ai := core.AttackInfo{
@@ -21,7 +23,7 @@ func (c *char) Attack(p map[string]int) (int, int) {
 	}
 
 	for i := 0; i < hits[c.NormalCounter]; i++ {
-		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-i, f-i)
+		c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), hitmarks[c.NormalCounter][i], hitmarks[c.NormalCounter][i])
 	}
 
 	c.AdvanceNormalIndex()
@@ -41,7 +43,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 		Mult:       charge[c.TalentLvlAttack()],
 		FlatDmg:    0.0139 * c.MaxHP(),
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(0.1, false, core.TargettableEnemy), f, f)
 
 	return f, a
 }
@@ -49,6 +51,7 @@ func (c *char) ChargeAttack(p map[string]int) (int, int) {
 func (c *char) Skill(p map[string]int) (int, int) {
 
 	cd := 240
+	cdDelay := 47
 	f, a := c.ActionFrames(core.ActionSkill, p)
 
 	max, ok := p["res_count"]
@@ -65,9 +68,10 @@ func (c *char) Skill(p map[string]int) (int, int) {
 		cd = 720
 	} else {
 		c.skillPress(f, max)
+		cdDelay = 22
 	}
 
-	c.SetCD(core.ActionSkill, cd)
+	c.SetCDWithDelay(core.ActionSkill, cd, cdDelay)
 	//no geo drain
 	return f, a
 }
@@ -92,7 +96,7 @@ func (c *char) skillHold(f, max int, createStele bool) {
 		Mult:       skillHold[c.TalentLvlSkill()],
 		FlatDmg:    0.019 * c.MaxHP(),
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 0, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(2, false, core.TargettableEnemy), 0, f)
 	//create a stele if less than zhongli's max stele count and desired by player
 	if (c.steleCount <= c.maxStele) && createStele {
 		c.AddTask(func() {
@@ -103,7 +107,7 @@ func (c *char) skillHold(f, max int, createStele bool) {
 	//make a shield - enemy debuff arrows appear 3-5 frames after the damage number shows up in game
 	c.AddTask(func() {
 		c.addJadeShield()
-	}, "zhongli-create-shield", f-1)
+	}, "zhongli-create-shield", f)
 }
 
 func (c *char) Burst(p map[string]int) (int, int) {
@@ -122,13 +126,19 @@ func (c *char) Burst(p map[string]int) (int, int) {
 		Mult:       burst[c.TalentLvlBurst()],
 		FlatDmg:    0.33 * c.MaxHP(),
 	}
-	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), f-1, f-1)
+	c.Core.Combat.QueueAttack(ai, core.NewDefCircHit(5, false, core.TargettableEnemy), f, f)
 
 	if c.Base.Cons >= 2 {
 		c.addJadeShield()
 	}
 
-	c.SetCDWithDelay(core.ActionBurst, 720, 6)
-	c.ConsumeEnergy(6)
+	c.SetCD(core.ActionBurst, 720)
+	c.ConsumeEnergy(7)
+	return f, a
+}
+
+//Zhongli dash is 19 frames
+func (c *char) Dash(p map[string]int) (int, int) {
+	f, a := c.ActionFrames(core.ActionDash, p)
 	return f, a
 }
diff --git a/internal/characters/zhongli/frames.go b/internal/characters/zhongli/frames.go
index 32daa217c..3cd930650 100644
--- a/internal/characters/zhongli/frames.go
+++ b/internal/characters/zhongli/frames.go
@@ -6,37 +6,120 @@ 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:
-			f = 8
+			f = 11
+			a = 18
 		case 1:
-			f = 29 - 8
+			f = 9
+			a = 13
 		case 2:
-			f = 45 - 29
+			f = 8
+			a = 19
 		case 3:
-			f = 71 - 45
+			f = 16
+			a = 34
 		case 4:
-			f = 109 - 71
+			f = 27
+			a = 27
 		case 5:
-			f = 153 - 109
+			f = 29
+			a = 54
 		}
 		f = int(float64(f) / (1 + c.Stat(core.AtkSpd)))
-		return f, f
+		return f, a
 	case core.ActionCharge:
-		return 95, 95
+		return 4, 47
 	case core.ActionSkill:
 		hold := p["hold"]
-		if hold == 0 {
+		holdNoStele := p["hold_nostele"]
+		if hold == 0 && holdNoStele == 0 {
 			//no hold
-			return 39, 39
+			return 23, 37
 		}
 		//yes hold
-		return 97, 97
+		return 48, 96
 	case core.ActionBurst:
-		return 140, 140
+		return 101, 138
+	case core.ActionDash:
+		return 19, 19
 	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, 18-11) //n1 -> next attack
+	c.SetNormalCancelFrames(0, core.ActionCharge, 30-11) //n1 -> charge
+
+	c.SetNormalCancelFrames(1, core.ActionAttack, 13-9) //n2 -> next attack
+	c.SetNormalCancelFrames(1, core.ActionCharge, 30-9) //n2 -> charge
+
+	c.SetNormalCancelFrames(2, core.ActionAttack, 19-8) //n3 -> next attack
+	c.SetNormalCancelFrames(2, core.ActionCharge, 28-8) //n3 -> charge
+
+	c.SetNormalCancelFrames(3, core.ActionAttack, 34-16) //n4 -> next attack
+	c.SetNormalCancelFrames(3, core.ActionCharge, 33-16) //n4 -> charge
+
+	//n5 -> n6 is before hitmark, so we delay everything that must wait until hitmark
+	c.SetNormalCancelFrames(4, core.ActionCharge, 31-27) //n5 -> charge
+	c.SetNormalCancelFrames(4, core.ActionSkill, 29-27)  //n5 -> skill
+	c.SetNormalCancelFrames(4, core.ActionBurst, 29-27)  //n5 -> burst
+	c.SetNormalCancelFrames(4, core.ActionDash, 29-27)   //n5 -> dash
+	c.SetNormalCancelFrames(4, core.ActionJump, 29-27)   //n5 -> jump
+	c.SetNormalCancelFrames(4, core.ActionSwap, 29-27)   //n5 -> swap
+
+	c.SetNormalCancelFrames(5, core.ActionAttack, 54-29) //n6 -> n1
+
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionAttack, 47-4) //charge -> n1
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSkill, 33-4)  //charge -> skill
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionBurst, 33-4)  //charge -> burst
+	c.SetAbilCancelFrames(core.ActionCharge, core.ActionSwap, 31-4)   //charge -> swap
+
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionAttack, 138-101) //burst -> n1
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSkill, 139-101)  //burst -> skill
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionDash, 122-101)   //burst -> dash
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionJump, 122-101)   //burst -> jump
+	c.SetAbilCancelFrames(core.ActionBurst, core.ActionSwap, 138-101)   //burst -> swap
+
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionAttack, 37-23)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionBurst, 38-23)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionDash, 23-23)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionJump, 23-23)
+	c.SetAbilCancelFrames(core.ActionSkill, core.ActionSwap, 37-23)
+}
+
+func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int) int {
+	// Provide a custom override for Zhongli's Hold E
+	if c.Core.LastAction.Typ == core.ActionSkill {
+		h := c.Core.LastAction.Param["hold"]
+		hNS := c.Core.LastAction.Param["hold_nostele"]
+		if h > 0 || hNS > 0 {
+			return SkillHoldFrames(next)
+		}
+	}
+	//otherwise use default implementation
+	return c.Tmpl.ActionInterruptableDelay(next, p)
+}
+
+func SkillHoldFrames(next core.ActionType) int {
+	switch next {
+	case core.ActionAttack:
+		return 96 - 48
+	case core.ActionBurst:
+		return 96 - 48
+	case core.ActionDash:
+		return 55 - 48
+	case core.ActionJump:
+		return 55 - 48
+	case core.ActionSwap:
+		return 96 - 48
+	default:
+		return 96 - 48
+	}
+}
diff --git a/internal/characters/zhongli/zhongli.go b/internal/characters/zhongli/zhongli.go
index 6c6541604..5e2681422 100644
--- a/internal/characters/zhongli/zhongli.go
+++ b/internal/characters/zhongli/zhongli.go
@@ -50,6 +50,7 @@ func NewChar(s *core.Core, p core.CharacterProfile) (core.Character, error) {
 
 func (c *char) Init() {
 	c.Tmpl.Init()
+	c.InitCancelFrames()
 
 	c.a1()
 }
diff --git a/internal/reactable/freeze.go b/internal/reactable/freeze.go
index f10ad18db..1bd2f2240 100644
--- a/internal/reactable/freeze.go
+++ b/internal/reactable/freeze.go
@@ -20,6 +20,7 @@ func (r *Reactable) tryFreeze(a *core.AttackEvent) {
 			//ec should have been taken care of already
 			a.Info.Durability -= consumed
 			a.Info.Durability = max(a.Info.Durability, 0)
+			r.core.Events.Emit(core.OnFrozen, r.self, a)
 			return
 		}
 		//otherwise attach hydro only if frozen exists
@@ -37,6 +38,7 @@ func (r *Reactable) tryFreeze(a *core.AttackEvent) {
 			r.Durability[core.Hydro] = max(r.Durability[core.Hydro], 0)
 			a.Info.Durability -= consumed
 			a.Info.Durability = max(a.Info.Durability, 0)
+			r.core.Events.Emit(core.OnFrozen, r.self, a)
 			return
 		}
 		//otherwise attach cryo only if frozen exists
@@ -51,7 +53,7 @@ func (r *Reactable) tryFreeze(a *core.AttackEvent) {
 		//should be here
 		return
 	}
-	r.core.Events.Emit(core.OnFrozen, r.self, a)
+
 }
 
 func max(a, b core.Durability) core.Durability {
diff --git a/internal/reactable/reactable.go b/internal/reactable/reactable.go
index d66485635..f8d58c10e 100644
--- a/internal/reactable/reactable.go
+++ b/internal/reactable/reactable.go
@@ -59,9 +59,9 @@ func (r *Reactable) React(a *core.AttackEvent) {
 		switch a.Info.Element {
 		case core.Electro:
 			r.tryOverload(a)
+			r.tryFrozenSuperconduct(a)
 			r.tryAddEC(a)
 			r.trySuperconduct(a)
-			r.tryFrozenSuperconduct(a)
 		case core.Pyro:
 			r.tryOverload(a)
 			r.tryMelt(a)
diff --git a/internal/substatoptimizer/substatoptimizer.go b/internal/substatoptimizer/substatoptimizer.go
index ad5965396..28a437431 100644
--- a/internal/substatoptimizer/substatoptimizer.go
+++ b/internal/substatoptimizer/substatoptimizer.go
@@ -232,7 +232,7 @@ func RunSubstatOptim(simopt simulator.Options, verbose bool, additionalOptions s
 		// Need special exception to Raiden due to her burst mechanics
 		// TODO: Don't think there's a better solution without an expensive recursive solution to check across all Raiden ER states
 		// Practically high ER substat Raiden is always currently unoptimal, so we just set her initial stacks low
-		erStack := 10
+		erStack := charSubstatLimits[i][core.ER]
 		if char.Base.Key == core.Raiden {
 			erStack = 0
 		}
@@ -389,6 +389,12 @@ func RunSubstatOptim(simopt simulator.Options, verbose bool, additionalOptions s
 
 			substatGradients[idxSubstat] = substatEvalResult.DPS.Mean - initialMean
 
+			// fixes cases in which fav holders don't get enough crit rate to reliably proc fav (an important example would be fav kazuha)
+			// might give them "too much" cr (= max out liquid cr subs) but that's probably not a big deal
+			if charWithFavonius[idxChar] && substat == core.CR {
+				substatGradients[idxSubstat] += 1000
+			}
+
 			charProfilesCopy[idxChar].Stats[substat] -= 10 * substatValues[substat] * charSubstatRarityMod[idxChar]
 		}
 
diff --git a/internal/tmpl/action/action.go b/internal/tmpl/action/action.go
index 5ba020619..9ec0d3bab 100644
--- a/internal/tmpl/action/action.go
+++ b/internal/tmpl/action/action.go
@@ -203,7 +203,7 @@ func (a *Ctrl) execAction(n *core.ActionItem) (int, bool, error) {
 		f = a.execActionItem(n, core.PreDash, core.PostDash, core.DashState, true, c.Dash)
 		a.core.Events.Emit(core.OnStamUse, core.ActionDash)
 	case core.ActionJump:
-		f = core.JumpFrames
+		f = a.execActionItem(n, core.PreJump, core.PostJump, core.JumpState, true, c.Jump)
 		a.core.ResetAllNormalCounter()
 	case core.ActionWalk:
 		f = core.WalkFrames
diff --git a/internal/tmpl/character/actions.go b/internal/tmpl/character/actions.go
index 1135defb7..9613c1936 100644
--- a/internal/tmpl/character/actions.go
+++ b/internal/tmpl/character/actions.go
@@ -33,7 +33,11 @@ func (c *Tmpl) Burst(p map[string]int) (int, int) {
 }
 
 func (c *Tmpl) Dash(p map[string]int) (int, int) {
-	return 24, 24
+	return 24, 24 //likely too long
+}
+
+func (c *Tmpl) Jump(p map[string]int) (int, int) {
+	return 33, 33 //likely too long
 }
 
 func (c *Tmpl) ActionStam(a core.ActionType, p map[string]int) float64 {
diff --git a/internal/weapons/catalyst/mappa/mappa.go b/internal/weapons/catalyst/mappa/mappa.go
index cf46b8ee5..89cd8c081 100644
--- a/internal/weapons/catalyst/mappa/mappa.go
+++ b/internal/weapons/catalyst/mappa/mappa.go
@@ -15,12 +15,20 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 
 	addStack := func(args ...interface{}) bool {
 		atk := args[1].(*core.AttackEvent)
-		if atk.Info.ActorIndex == char.CharIndex() {
+		if atk.Info.ActorIndex != char.CharIndex() {
+			return false
+		}
+		if c.ActiveChar != char.CharIndex() {
+			return false
+		}
+
+		if c.F > dur {
+			stacks = 1
+			dur = c.F + 600
+			c.Log.NewEvent("mappa proc'd", core.LogWeaponEvent, char.CharIndex(), "stacks", stacks, "expiry", dur)
+		} else if stacks < 2 {
 			stacks++
-			if stacks > 2 {
-				stacks = 2
-				dur = c.F + 600
-			}
+			c.Log.NewEvent("mappa proc'd", core.LogWeaponEvent, char.CharIndex(), "stacks", stacks, "expiry", dur)
 		}
 		return false
 	}
@@ -30,21 +38,23 @@ func weapon(char core.Character, c *core.Core, r int, param map[string]int) stri
 	}
 
 	dmg := 0.06 + float64(r)*0.02
-
 	m := make([]float64, core.EndStatType)
 
-	m[core.PyroP] = dmg
-	m[core.HydroP] = dmg
-	m[core.CryoP] = dmg
-	m[core.ElectroP] = dmg
-	m[core.AnemoP] = dmg
-	m[core.GeoP] = dmg
-	m[core.DendroP] = dmg
-
 	char.AddMod(core.CharStatMod{
 		Key: "mappa",
 		Amount: func() ([]float64, bool) {
-			return m, dur > c.F
+			if c.F > dur {
+				return nil, false
+			}
+
+			m[core.PyroP] = dmg * float64(stacks)
+			m[core.HydroP] = dmg * float64(stacks)
+			m[core.CryoP] = dmg * float64(stacks)
+			m[core.ElectroP] = dmg * float64(stacks)
+			m[core.AnemoP] = dmg * float64(stacks)
+			m[core.GeoP] = dmg * float64(stacks)
+			m[core.DendroP] = dmg * float64(stacks)
+			return m, true
 		},
 		Expiry: -1,
 	})
diff --git a/pkg/core/cfg.go b/pkg/core/cfg.go
index a1b5f9fae..e946da6bb 100644
--- a/pkg/core/cfg.go
+++ b/pkg/core/cfg.go
@@ -68,13 +68,14 @@ const (
 // }
 
 type CharacterProfile struct {
-	Base      CharacterBase             `json:"base"`
-	Weapon    WeaponProfile             `json:"weapon"`
-	Talents   TalentProfile             `json:"talents"`
-	Stats     []float64                 `json:"stats"`
-	Sets      map[string]int            `json:"sets"`
-	SetParams map[string]map[string]int `json:"-"`
-	Params    map[string]int            `json:"-"`
+	Base         CharacterBase             `json:"base"`
+	Weapon       WeaponProfile             `json:"weapon"`
+	Talents      TalentProfile             `json:"talents"`
+	Stats        []float64                 `json:"stats"`
+	StatsByLabel map[string][]float64      `json:"stats_by_label"`
+	Sets         map[string]int            `json:"sets"`
+	SetParams    map[string]map[string]int `json:"-"`
+	Params       map[string]int            `json:"-"`
 }
 
 func (c *CharacterProfile) Clone() CharacterProfile {
diff --git a/pkg/core/character.go b/pkg/core/character.go
index f558353f1..745c01715 100644
--- a/pkg/core/character.go
+++ b/pkg/core/character.go
@@ -37,6 +37,7 @@ type Character interface {
 	Skill(p map[string]int) (int, int)
 	Burst(p map[string]int) (int, int)
 	Dash(p map[string]int) (int, int)
+	Jump(p map[string]int) (int, int)
 
 	//info methods
 	ActionReady(a ActionType, p map[string]int) bool
diff --git a/pkg/core/charkey.go b/pkg/core/charkey.go
index da02203aa..816697e2b 100644
--- a/pkg/core/charkey.go
+++ b/pkg/core/charkey.go
@@ -93,87 +93,80 @@ func (c CharKey) String() string {
 }
 
 var CharNameToKey = map[string]CharKey{
-	"travelerelectro":    TravelerElectro,
-	"traveleranemo":      TravelerAnemo,
-	"travelergeo":        TravelerGeo,
-	"travelerhydro":      TravelerHydro,
-	"travelercryo":       TravelerCryo,
-	"travelerpyro":       TravelerPyro,
-	"travelerdendro":     TravelerDendro,
-	"traveler (electro)": TravelerElectro,
-	"traveler (anemo)":   TravelerAnemo,
-	"traveler (geo)":     TravelerGeo,
-	"traveler (hydro)":   TravelerHydro,
-	"traveler (cryo)":    TravelerCryo,
-	"traveler (pyro)":    TravelerPyro,
-	"traveler (dendro)":  TravelerDendro,
-	"albedo":             Albedo,
-	"aloy":               Aloy,
-	"amber":              Amber,
-	"barbara":            Barbara,
-	"beidou":             Beidou,
-	"bennett":            Bennett,
-	"chongyun":           Chongyun,
-	"diluc":              Diluc,
-	"diona":              Diona,
-	"eula":               Eula,
-	"fischl":             Fischl,
-	"ganyu":              Ganyu,
-	"hutao":              Hutao,
-	"jean":               Jean,
-	"kaedeharakazuha":    Kazuha,
-	"kazuha":             Kazuha,
-	"kaeya":              Kaeya,
-	"kamisatoayaka":      Ayaka,
-	"ayaka":              Ayaka,
-	"kamisatoayato":      Ayato,
-	"ayato":              Ayato,
-	"keqing":             Keqing,
-	"klee":               Klee,
-	"kujousara":          Sara,
-	"kujosara":           Sara,
-	"sara":               Sara,
-	"lisa":               Lisa,
-	"mona":               Mona,
-	"ningguang":          Ningguang,
-	"noelle":             Noelle,
-	"qiqi":               Qiqi,
-	"raidenshogun":       Raiden,
-	"raiden":             Raiden,
-	"razor":              Razor,
-	"rosaria":            Rosaria,
-	"sangonomiyakokomi":  Kokomi,
-	"kokomi":             Kokomi,
-	"sayu":               Sayu,
-	"sucrose":            Sucrose,
-	"tartaglia":          Tartaglia,
-	"thoma":              Thoma,
-	"venti":              Venti,
-	"xiangling":          Xiangling,
-	"xiao":               Xiao,
-	"xingqiu":            Xingqiu,
-	"xinyan":             Xinyan,
-	"yanfei":             Yanfei,
-	"yoimiya":            Yoimiya,
-	"yunjin":             Yunjin,
-	"zhongli":            Zhongli,
-	"gorou":              Gorou,
-	"aratakiitto":        Itto,
-	"itto":               Itto,
-	"shenhe":             Shenhe,
-	"yae":                YaeMiko,
-	"yaemiko":            YaeMiko,
+	"travelerelectro":   TravelerElectro,
+	"traveleranemo":     TravelerAnemo,
+	"travelergeo":       TravelerGeo,
+	"travelerhydro":     TravelerHydro,
+	"travelercryo":      TravelerCryo,
+	"travelerpyro":      TravelerPyro,
+	"travelerdendro":    TravelerDendro,
+	"albedo":            Albedo,
+	"aloy":              Aloy,
+	"amber":             Amber,
+	"barbara":           Barbara,
+	"beidou":            Beidou,
+	"bennett":           Bennett,
+	"chongyun":          Chongyun,
+	"diluc":             Diluc,
+	"diona":             Diona,
+	"eula":              Eula,
+	"fischl":            Fischl,
+	"ganyu":             Ganyu,
+	"hutao":             Hutao,
+	"jean":              Jean,
+	"kaedeharakazuha":   Kazuha,
+	"kazuha":            Kazuha,
+	"kaeya":             Kaeya,
+	"kamisatoayaka":     Ayaka,
+	"ayaka":             Ayaka,
+	"kamisatoayato":     Ayato,
+	"ayato":             Ayato,
+	"keqing":            Keqing,
+	"klee":              Klee,
+	"kujousara":         Sara,
+	"kujosara":          Sara,
+	"sara":              Sara,
+	"lisa":              Lisa,
+	"mona":              Mona,
+	"ningguang":         Ningguang,
+	"noelle":            Noelle,
+	"qiqi":              Qiqi,
+	"raidenshogun":      Raiden,
+	"raiden":            Raiden,
+	"razor":             Razor,
+	"rosaria":           Rosaria,
+	"sangonomiyakokomi": Kokomi,
+	"kokomi":            Kokomi,
+	"sayu":              Sayu,
+	"sucrose":           Sucrose,
+	"tartaglia":         Tartaglia,
+	"thoma":             Thoma,
+	"venti":             Venti,
+	"xiangling":         Xiangling,
+	"xiao":              Xiao,
+	"xingqiu":           Xingqiu,
+	"xinyan":            Xinyan,
+	"yanfei":            Yanfei,
+	"yoimiya":           Yoimiya,
+	"yunjin":            Yunjin,
+	"zhongli":           Zhongli,
+	"gorou":             Gorou,
+	"aratakiitto":       Itto,
+	"itto":              Itto,
+	"shenhe":            Shenhe,
+	"yae":               YaeMiko,
+	"yaemiko":           YaeMiko,
 }
 
 var charNames = []string{
 	"",
-	"traveler (electro)",
-	"traveler (anemo)",
-	"traveler (geo)",
-	"traveler (hydro)",
-	"traveler (cryo)",
-	"traveler (pyro)",
-	"traveler (dendro)",
+	"travelerelectro",
+	"traveleranemo",
+	"travelergeo",
+	"travelerhydro",
+	"travelercryo",
+	"travelerpyro",
+	"travelerdendro",
 	"aether",
 	"lumine",
 	"", //delim for traveler
diff --git a/pkg/core/core.go b/pkg/core/core.go
index fed800ed8..2b8127116 100644
--- a/pkg/core/core.go
+++ b/pkg/core/core.go
@@ -170,11 +170,9 @@ func (c *Core) Swap(next CharKey) int {
 }
 
 func (c *Core) AnimationCancelDelay(next ActionType, p map[string]int) int {
-	//if last action is jump, dash, swap,
+	//if last action is swap,
 	switch c.LastAction.Typ {
 	case ActionSwap:
-		fallthrough
-	case ActionJump:
 		return 0
 	}
 	//other wise check with the current character
diff --git a/pkg/core/events.go b/pkg/core/events.go
index b858e386d..ec52a92c7 100644
--- a/pkg/core/events.go
+++ b/pkg/core/events.go
@@ -53,6 +53,8 @@ const (
 	PostAimShoot     //nil, frames
 	PreDash
 	PostDash
+	PreJump
+	PostJump
 	//sim stuff
 	OnInitialize  //nil
 	OnStateChange //prev, next
diff --git a/pkg/core/state.go b/pkg/core/state.go
index 01d5fff06..7632fa6c1 100644
--- a/pkg/core/state.go
+++ b/pkg/core/state.go
@@ -11,6 +11,7 @@ const (
 	BurstState
 	AimState
 	DashState
+	JumpState
 )
 
 func (c *Core) SetState(state AnimationState, duration int) {
diff --git a/pkg/parse/parseCharLine.go b/pkg/parse/parseCharLine.go
index b85aa2618..8b44fe27c 100644
--- a/pkg/parse/parseCharLine.go
+++ b/pkg/parse/parseCharLine.go
@@ -26,6 +26,7 @@ func (p *Parser) newChar(key core.CharKey) {
 	r := core.CharacterProfile{}
 	r.Base.Key = key
 	r.Stats = make([]float64, len(core.StatTypeString))
+	r.StatsByLabel = make(map[string][]float64)
 	r.Params = make(map[string]int)
 	r.Sets = make(map[string]int)
 	r.SetParams = make(map[string]map[string]int)
@@ -246,6 +247,13 @@ func parseCharAddStats(p *Parser) (parseFn, error) {
 	//xiangling add stats hp=4780 atk=311 er=.518 pyro%=0.466 cr=0.311;
 	c := p.chars[p.currentCharKey]
 
+	//each line will be parsed separately into the map
+	var line = make([]float64, len(core.StatTypeString))
+	var key string
+	useRolls := false
+	rollOpt := "avg"
+	rarity := 5
+
 	for n := p.next(); n.typ != itemEOF; n = p.next() {
 		switch n.typ {
 		case itemStatKey:
@@ -258,8 +266,63 @@ func parseCharAddStats(p *Parser) (parseFn, error) {
 				return nil, err
 			}
 			pos := statKeys[n.val]
-			c.Stats[pos] += amt
+			line[pos] += amt
+		case itemLabel:
+			x, err := p.acceptSeqReturnLast(itemEqual, itemIdentifier)
+			if err != nil {
+				return nil, err
+			}
+			key = x.val
+		case itemIdentifier:
+			switch n.val {
+			case "roll":
+				x, err := p.acceptSeqReturnLast(itemEqual, itemIdentifier)
+				if err != nil {
+					return nil, err
+				}
+				//should be min, max, avg
+				switch x.val {
+				case "avg", "min", "max":
+					useRolls = true
+					rollOpt = x.val
+				default:
+					return nil, fmt.Errorf("ln%v: invalid roll option: %v", n.line, x.val)
+				}
+			case "rarity":
+				x, err := p.acceptSeqReturnLast(itemEqual, itemNumber)
+				if err != nil {
+					return nil, err
+				}
+				amt, err := itemNumberToInt(x)
+				if err != nil {
+					return nil, err
+				}
+				if amt > 5 {
+					amt = 5
+				}
+				if amt < 1 {
+					amt = 1
+				}
+				rarity = amt
+			default:
+				return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats: %v", n.line, n)
+			}
 		case itemTerminateLine:
+			//add stats into label
+			m, ok := c.StatsByLabel[key]
+			if !ok {
+				m = make([]float64, len(core.StatTypeString))
+			}
+			for i, v := range line {
+				if useRolls {
+					c.Stats[i] += v * rolls[rarity-1][rollOpt][i]
+					m[i] += v * rolls[rarity-1][rollOpt][i]
+				} else {
+					c.Stats[i] += v
+					m[i] += v
+				}
+			}
+			c.StatsByLabel[key] = m
 			return parseRows, nil
 		default:
 			return nil, fmt.Errorf("ln%v: unrecognized token parsing add stats: %v", n.line, n)
@@ -267,3 +330,31 @@ func parseCharAddStats(p *Parser) (parseFn, error) {
 	}
 	return nil, errors.New("unexpected end of line while parsing character add stats")
 }
+
+var rolls = []map[string][]float64{
+	{
+		"min": {0, 0.0146, 1.85, 23.9, 0.0117, 1.56, 0.0117, 0.013, 4.66, 0.0078, 0.0155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"avg": {0, 0.0164, 2.08, 26.89, 0.01315, 1.755, 0.01315, 0.0146, 5.245, 0.00875, 0.017450001, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"max": {0, 0.0182, 2.31, 29.88, 0.0146, 1.95, 0.0146, 0.0162, 5.83, 0.0097, 0.0194, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+	},
+	{
+		"min": {0, 0.0204, 3.89, 50.19, 0.0163, 3.27, 0.0163, 0.0181, 6.53, 0.0109, 0.0218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"avg": {0, 0.024766669, 4.7233334, 60.946667, 0.0198, 3.97, 0.0198, 0.022, 7.9300003, 0.0132, 0.026433334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"max": {0, 0.0291, 5.56, 71.7, 0.0233, 4.67, 0.0233, 0.0259, 9.33, 0.0155, 0.0311, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+	},
+	{
+		"min": {0, 0.0306, 7.78, 100.38, 0.0245, 6.54, 0.0245, 0.0272, 9.79, 0.0163, 0.0326, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"avg": {0, 0.03715, 9.445, 121.89, 0.02975, 7.9375, 0.02975, 0.03305, 11.889999, 0.0198, 0.039625, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"max": {0, 0.0437, 11.11, 143.4, 0.035, 9.34, 0.035, 0.0389, 13.99, 0.0233, 0.0466, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+	},
+	{
+		"min": {0, 0.0408, 12.96, 167.3, 0.0326, 10.89, 0.0326, 0.0363, 13.06, 0.0218, 0.0435, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"avg": {0, 0.04955, 15.742499, 203.15, 0.039625, 13.225, 0.039625, 0.044025, 15.855, 0.026449999, 0.052849997, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"max": {0, 0.0583, 18.52, 239, 0.0466, 15.56, 0.0466, 0.0518, 18.65, 0.0311, 0.0622, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+	},
+	{
+		"min": {0, 0.051, 16.2, 209.13, 0.0408, 13.62, 0.0408, 0.0453, 16.32, 0.0272, 0.0544, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"avg": {0, 0.06195, 19.675001, 253.94, 0.04955, 16.535, 0.04955, 0.05505, 19.815, 0.03305, 0.06605, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+		"max": {0, 0.0729, 23.15, 298.75, 0.0583, 19.45, 0.0583, 0.0648, 23.31, 0.0389, 0.0777, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+	},
+}
diff --git a/pkg/simulation/imports.go b/pkg/simulation/imports.go
index d3cb0dcf5..5c539af53 100644
--- a/pkg/simulation/imports.go
+++ b/pkg/simulation/imports.go
@@ -9,6 +9,7 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/artifacts/crimson"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/echoes"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/exile"
+	_ "github.com/genshinsim/gcsim/internal/artifacts/gambler"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/gladiator"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/heartofdepth"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/huskofopulentdreams"
@@ -20,6 +21,7 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/artifacts/paleflame"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/reminiscence"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/seal"
+	_ "github.com/genshinsim/gcsim/internal/artifacts/scholar"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/tenacity"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/thunderingfury"
 	_ "github.com/genshinsim/gcsim/internal/artifacts/thundersoother"
diff --git a/pkg/simulation/queue.go b/pkg/simulation/queue.go
index 012defac2..1c1135b34 100644
--- a/pkg/simulation/queue.go
+++ b/pkg/simulation/queue.go
@@ -20,6 +20,7 @@ func (s *Simulation) initQueuer() error {
 			return fmt.Errorf("invalid char in rotation %v; %v", v.SequenceChar, v)
 		}
 		s.cfg.Rotation[i].LastQueued = -1
+		s.cfg.Rotation[i].NumQueued = 0
 	}
 	s.C.Log.NewEvent(
 		"setting queue",


Test patch:
diff --git a/internal/characters/beidou/beidou_test.go b/internal/characters/beidou/beidou_test.go
index 8776541bb..33d97692e 100644
--- a/internal/characters/beidou/beidou_test.go
+++ b/internal/characters/beidou/beidou_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 450)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 450+4)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/bennett/bennett_test.go b/internal/characters/bennett/bennett_test.go
index 0280d76ae..68f878edc 100644
--- a/internal/characters/bennett/bennett_test.go
+++ b/internal/characters/bennett/bennett_test.go
@@ -111,7 +111,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 300-60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 300-60+14)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/eula/eula_test.go b/internal/characters/eula/eula_test.go
index 9d858a39f..372071987 100644
--- a/internal/characters/eula/eula_test.go
+++ b/internal/characters/eula/eula_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 240)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 240+16) // +16 from cd delay
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/fischl/fischl_test.go b/internal/characters/fischl/fischl_test.go
index 1c290270c..da9d7bc65 100644
--- a/internal/characters/fischl/fischl_test.go
+++ b/internal/characters/fischl/fischl_test.go
@@ -29,7 +29,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 25*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 25*60+18)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/ganyu/ganyu_test.go b/internal/characters/ganyu/ganyu_test.go
index e87baa48e..866337d3d 100644
--- a/internal/characters/ganyu/ganyu_test.go
+++ b/internal/characters/ganyu/ganyu_test.go
@@ -29,7 +29,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 600)
+		err = testhelper.TestSkillCDSingleCharge(c, x, 600+10)
 	if err != nil {
 		t.Error(err)
 	}
@@ -43,7 +43,7 @@ func TestC2CD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDDoubleCharge(c, x, []int{600, 600})
+	err = testhelper.TestSkillCooldown(c, x, []int{600+10, 600})
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/hutao/hutao_test.go b/internal/characters/hutao/hutao_test.go
index 8b3c32c3b..05d0e2368 100644
--- a/internal/characters/hutao/hutao_test.go
+++ b/internal/characters/hutao/hutao_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 960)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 960+14)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/jean/jean_test.go b/internal/characters/jean/jean_test.go
index 616fb22e2..c87334410 100644
--- a/internal/characters/jean/jean_test.go
+++ b/internal/characters/jean/jean_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 360)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 360+19)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/raiden/raiden_test.go b/internal/characters/raiden/raiden_test.go
index 3e1cf093e..68d6e4cee 100644
--- a/internal/characters/raiden/raiden_test.go
+++ b/internal/characters/raiden/raiden_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 10*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 10*60+6)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/rosaria/rosaria_test.go b/internal/characters/rosaria/rosaria_test.go
index 81782a8f2..0333f0a23 100644
--- a/internal/characters/rosaria/rosaria_test.go
+++ b/internal/characters/rosaria/rosaria_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 6*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 6*60+23)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/venti/venti_test.go b/internal/characters/venti/venti_test.go
index 55785d4b2..a6187df7f 100644
--- a/internal/characters/venti/venti_test.go
+++ b/internal/characters/venti/venti_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 6*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 6*60 + 21)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/xiangling/xiangling_test.go b/internal/characters/xiangling/xiangling_test.go
index 835e4dcec..49eb907ab 100644
--- a/internal/characters/xiangling/xiangling_test.go
+++ b/internal/characters/xiangling/xiangling_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 12*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 12*60+13)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/xingqiu/xingqiu_test.go b/internal/characters/xingqiu/xingqiu_test.go
index 75a11ebba..3a49ed678 100644
--- a/internal/characters/xingqiu/xingqiu_test.go
+++ b/internal/characters/xingqiu/xingqiu_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 21*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 21*60+10)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/yoimiya/yoimiya_test.go b/internal/characters/yoimiya/yoimiya_test.go
index 6f7173f6e..d4af750df 100644
--- a/internal/characters/yoimiya/yoimiya_test.go
+++ b/internal/characters/yoimiya/yoimiya_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 18*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 18*60+11)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/internal/characters/zhongli/zhongli_test.go b/internal/characters/zhongli/zhongli_test.go
index 561201ab9..d0641ddca 100644
--- a/internal/characters/zhongli/zhongli_test.go
+++ b/internal/characters/zhongli/zhongli_test.go
@@ -28,7 +28,7 @@ func TestCD(t *testing.T) {
 		t.Error(err)
 		t.FailNow()
 	}
-	err = testhelper.TestSkillCDSingleCharge(c, x, 4*60)
+	err = testhelper.TestSkillCDSingleCharge(c, x, 4*60+22)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go
index cf5b4492f..b221dd672 100644
--- a/pkg/parse/parse_test.go
+++ b/pkg/parse/parse_test.go
@@ -241,8 +241,8 @@ options debug=true iteration=300 duration=60 workers=24;
 bennett char lvl=70/80 cons=2 talent=6,8,8;
 bennett add weapon="favoniussword" lvl=90/90 refine=1;
 bennett add set="noblesseoblige" count=4;
-bennett add stats hp=4780 atk=311 er=0.518 pyro%=0.466 cr=0.311 ;
-bennett add stats hp=717 hp%=0.057999999999999996 atk=78 atk%=0.663 def=118 em=42 er=0.221 cr=0.039 cd=0.475 ;
+bennett add stats hp=4780 atk=311 er=0.518 pyro%=0.466 cr=0.311 label=main ;
+bennett add stats hp=717 hp%=0.057999999999999996 atk=78 atk%=0.663 def=118 em=42 er=0.221 cr=0.039 cd=0.475 label=subs ;
 
 raiden char lvl=90/90 cons=1 talent=10,10,10;
 raiden add weapon="engulfinglightning" lvl=90/90 refine=1;
@@ -349,3 +349,64 @@ active raiden;
  
 raiden skill;
 `
+
+var statLabelTest = `
+options debug=true iteration=300 duration=60 workers=24;
+
+bennett char lvl=70/80 cons=2 talent=6,8,8;
+bennett add weapon="favoniussword" lvl=90/90 refine=1;
+bennett add set="noblesseoblige" count=4;
+bennett add stats hp=4780 atk=311 er=0.518 pyro%=0.466 cr=0.311 label=main ;
+bennett add stats hp=717 hp%=0.057999999999999996 atk=78 atk%=0.663 def=118 em=42 er=0.221 cr=0.039 cd=0.475 label=subs ;
+bennett add stats roll=avg rarity=5 label=rolltest
+def%=1
+def=1
+hp=1
+hp%=1
+atk=1
+atk%=1
+er=1
+em=1
+cr=1
+cd=1
+heal=1
+pyro%=1
+hydro%=1
+cryo%=1
+electro%=1
+anemo%=1
+geo%=1
+phys%=1
+;
+
+active bennett;
+target lvl=100 resist=0.1;
+
+
+`
+
+func TestStatsLabel(t *testing.T) {
+	p := New("test", statLabelTest)
+	a, err := p.Parse()
+	if err != nil {
+		t.Error(err)
+	}
+	fmt.Println("characters:")
+	for _, v := range a.Characters.Profile {
+		fmt.Println(v.Base.Key.String())
+		//basic stats:
+		fmt.Println("\t basics", v.Base)
+		fmt.Println("\t char params", v.Params)
+		fmt.Println("\t weapons", v.Weapon)
+		fmt.Println("\t talents", v.Talents)
+		fmt.Println("\t sets", v.Sets)
+		fmt.Println("\t set params", v.SetParams)
+		//pretty print stats
+		fmt.Println("\t stats", v.Stats)
+		//print stat map
+		for k, vv := range v.StatsByLabel {
+			fmt.Printf("\t stats for %v: %v\n", k, vv)
+		}
+	}
+
+}