Repository: genshinsim/gcsim
Pull request #528 (2022-05-10 03:49:53)

Update Kokomi frames
sheet: https://docs.google.com/spreadsheets/d/1j3Hzo25qSqo-ZbQBiBqcejR1aa91igSVTxH1xoKYrfc/edit


Patch:
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 6baafbc2d..e09cbd49e 100644
--- a/internal/characters/bennett/bennett.go
+++ b/internal/characters/bennett/bennett.go
@@ -89,7 +89,7 @@ func (c *char) Skill(p map[string]int) (int, int) {
 
 	switch p["hold"] {
 	case 1:
-		c.skillHoldShort(false)
+		c.skillHoldShort()
 		cd = 450 - 90
 		cdDelay = 43
 	case 2:
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/raiden/frames.go b/internal/characters/raiden/frames.go
index bd3305029..69f6f53f2 100644
--- a/internal/characters/raiden/frames.go
+++ b/internal/characters/raiden/frames.go
@@ -122,29 +122,30 @@ func (c *char) ActionInterruptableDelay(next core.ActionType, p map[string]int)
 func burstNormalCancels(next core.ActionType, NormalCounter int) int {
 	switch next {
 	case core.ActionAttack:
-		switch NormalCounter {
-		case 0:
-			return 19 - 12
+		switch NormalCounter { //counter for the next attack
 		case 1:
-			return 16 - 13
+			return 19 - 12
 		case 2:
-			return 16 - 11
+			return 16 - 13
 		case 3:
-			return 44 - 33
+			return 16 - 11
 		case 4:
+			return 44 - 33
+		case 0:
 			return 59 - 33
+
 		}
 	case core.ActionCharge:
 		switch NormalCounter {
-		case 0:
-			return 24 - 12
 		case 1:
-			return 26 - 13
+			return 24 - 12
 		case 2:
-			return 34 - 11
+			return 26 - 13
 		case 3:
-			return 67 - 33
+			return 34 - 11
 		case 4:
+			return 67 - 33
+		case 0:
 			return 83 - 33
 		}
 	}
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 855e8c24e..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
@@ -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
+}


Test patch:
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)
 	}