From 6ef74fb9d833cbbf6589598f3ffde9fbe720c71e Mon Sep 17 00:00:00 2001 From: koli <98557316+k0l11@users.noreply.github.com> Date: Wed, 15 Feb 2023 18:51:14 +0100 Subject: [PATCH] refactor some char events into callbacks (#1286) * refactor ayato c6 into a callback * refactor beidou c4 into a callback * refactor chongyun c4 into a callback * refactor cyno c2 and c6 into a callback * refactor dori c6 into a callback * refactor eula stack gain into a callback - convert stack gain icd into a status * refactor keqing c2 into a callback * refactor klee a4 into a callback * refactor kokomi q heal, c2/c4/c6 into a callback * refactor mona c6 ca reset into a callback * refactor nahida c6 into a callback * refactor rosaria c1, c4 and c6 * refactor shenhe c4 reset into a callback * refactor xinyan c1 and c4 * refactor yanfei a4 into a callback * refactor yoimiya a1 and c2 * adjust eula burst stack cb --- internal/characters/ayato/attack.go | 1 + internal/characters/ayato/ayato.go | 6 +- internal/characters/ayato/cons.go | 33 ++++---- internal/characters/ayato/skill.go | 2 +- internal/characters/beidou/attack.go | 2 +- internal/characters/beidou/beidou.go | 2 +- internal/characters/beidou/cons.go | 37 +++++---- internal/characters/chongyun/attack.go | 4 +- internal/characters/chongyun/burst.go | 4 + internal/characters/chongyun/chongyun.go | 3 - internal/characters/chongyun/cons.go | 40 ++++----- internal/characters/chongyun/skill.go | 6 +- internal/characters/cyno/attack.go | 15 +++- internal/characters/cyno/burst.go | 5 +- internal/characters/cyno/cons.go | 89 ++++++++++---------- internal/characters/cyno/cyno.go | 9 +- internal/characters/cyno/skill.go | 8 +- internal/characters/dori/attack.go | 3 +- internal/characters/dori/cons.go | 40 ++++----- internal/characters/dori/dori.go | 6 -- internal/characters/dori/skill.go | 2 +- internal/characters/eula/asc.go | 1 + internal/characters/eula/attack.go | 3 +- internal/characters/eula/burst.go | 68 +++++++-------- internal/characters/eula/eula.go | 2 - internal/characters/eula/skill.go | 4 + internal/characters/keqing/attack.go | 3 +- internal/characters/keqing/charge.go | 2 + internal/characters/keqing/cons.go | 32 ++++--- internal/characters/keqing/keqing.go | 3 - internal/characters/klee/asc.go | 27 +++--- internal/characters/klee/charge.go | 1 + internal/characters/klee/klee.go | 1 - internal/characters/kokomi/attack.go | 2 + internal/characters/kokomi/burst.go | 35 ++++---- internal/characters/kokomi/charge.go | 2 + internal/characters/kokomi/cons.go | 20 +++-- internal/characters/kokomi/kokomi.go | 7 -- internal/characters/mona/charge.go | 1 + internal/characters/mona/cons.go | 31 +++---- internal/characters/mona/mona.go | 3 - internal/characters/nahida/attack.go | 1 + internal/characters/nahida/charge.go | 1 + internal/characters/nahida/cons.go | 44 ++++------ internal/characters/nahida/nahida.go | 4 - internal/characters/rosaria/attack.go | 4 +- internal/characters/rosaria/burst.go | 10 ++- internal/characters/rosaria/charge.go | 1 + internal/characters/rosaria/cons.go | 101 +++++++++++------------ internal/characters/rosaria/rosaria.go | 5 -- internal/characters/rosaria/skill.go | 19 +++-- internal/characters/shenhe/cons.go | 36 ++++---- internal/characters/shenhe/shenhe.go | 2 +- internal/characters/shenhe/skill.go | 4 +- internal/characters/xinyan/attack.go | 8 +- internal/characters/xinyan/burst.go | 11 ++- internal/characters/xinyan/cons.go | 62 +++++++------- internal/characters/xinyan/skill.go | 11 ++- internal/characters/xinyan/xinyan.go | 4 - internal/characters/yanfei/asc.go | 37 ++++----- internal/characters/yanfei/charge.go | 4 +- internal/characters/yanfei/yanfei.go | 2 - internal/characters/yoimiya/aimed.go | 3 + internal/characters/yoimiya/asc.go | 59 ++++++------- internal/characters/yoimiya/attack.go | 6 ++ internal/characters/yoimiya/burst.go | 3 +- internal/characters/yoimiya/cons.go | 30 ++++--- internal/characters/yoimiya/skill.go | 2 +- internal/characters/yoimiya/yoimiya.go | 8 +- 69 files changed, 520 insertions(+), 527 deletions(-) diff --git a/internal/characters/ayato/attack.go b/internal/characters/ayato/attack.go index a5a58ca7b..e8de67df8 100644 --- a/internal/characters/ayato/attack.go +++ b/internal/characters/ayato/attack.go @@ -117,6 +117,7 @@ func (c *char) SoukaiKanka(p map[string]int) action.ActionInfo { shunsuikenHitmark, c.particleCB, c.skillStacks, + c.makeC6CB(), ) defer c.AdvanceNormalIndex() diff --git a/internal/characters/ayato/ayato.go b/internal/characters/ayato/ayato.go index e40dd7bf5..6b172e73d 100644 --- a/internal/characters/ayato/ayato.go +++ b/internal/characters/ayato/ayato.go @@ -20,7 +20,7 @@ type char struct { stacks int stacksMax int shunsuikenCounter int - c6ready bool + c6Ready bool } const ( @@ -37,7 +37,6 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) c.NormalHitNum = normalHitNum c.shunsuikenCounter = 3 - c.c6ready = false c.stacksMax = 4 if c.Base.Cons >= 2 { @@ -58,9 +57,6 @@ func (c *char) Init() error { if c.Base.Cons >= 2 { c.c2() } - if c.Base.Cons >= 6 { - c.c6() - } return nil } diff --git a/internal/characters/ayato/cons.go b/internal/characters/ayato/cons.go index b07de50d9..93dad7f3a 100644 --- a/internal/characters/ayato/cons.go +++ b/internal/characters/ayato/cons.go @@ -3,7 +3,6 @@ package ayato import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/glog" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/enemy" @@ -45,18 +44,26 @@ func (c *char) c2() { }) } -func (c *char) c6() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - if c.Core.Player.Active() != c.Index { - return false +// After using Kamisato Art: Kyouka, Ayato's next Shunsuiken attack will create +// 2 extra Shunsuiken strikes when they hit opponents, each one dealing 450% of Ayato's ATK as DMG. +// Both these Shunsuiken attacks will not be affected by Namisen. +func (c *char) makeC6CB() combat.AttackCBFunc { + if c.Base.Cons < 6 || !c.c6Ready { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - if !c.c6ready { - return false + if c.Core.Player.Active() != c.Index { + return } - atk := args[1].(*combat.AttackEvent) - if atk.Info.AttackTag != combat.AttackTagNormal { - return false + if !c.c6Ready { + return } + c.c6Ready = false + + c.Core.Log.NewEvent("ayato c6 proc'd", glog.LogCharacterEvent, c.Index) ai := combat.AttackInfo{ Abil: "Ayato C6", ActorIndex: c.Index, @@ -80,9 +87,5 @@ func (c *char) c6() { 20+i*2, ) } - - c.Core.Log.NewEvent("ayato c6 proc'd", glog.LogCharacterEvent, c.Index) - c.c6ready = false - return false - }, "ayato-c6") + } } diff --git a/internal/characters/ayato/skill.go b/internal/characters/ayato/skill.go index 3dc625904..65bcae393 100644 --- a/internal/characters/ayato/skill.go +++ b/internal/characters/ayato/skill.go @@ -49,7 +49,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { c.AddStatus(SkillBuffKey, 6*60, true) // figure out atk buff if c.Base.Cons >= 6 { - c.c6ready = true + c.c6Ready = true } c.SetCD(action.ActionSkill, 12*60) diff --git a/internal/characters/beidou/attack.go b/internal/characters/beidou/attack.go index 697fde77a..08066e355 100644 --- a/internal/characters/beidou/attack.go +++ b/internal/characters/beidou/attack.go @@ -59,7 +59,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { attackHitboxes[c.NormalCounter][1], ) } - c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter]) + c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter], c.makeC4Callback()) defer c.AdvanceNormalIndex() diff --git a/internal/characters/beidou/beidou.go b/internal/characters/beidou/beidou.go index 941c2294b..12334a013 100644 --- a/internal/characters/beidou/beidou.go +++ b/internal/characters/beidou/beidou.go @@ -33,7 +33,7 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) func (c *char) Init() error { c.burstProc() if c.Base.Cons >= 4 { - c.c4() + c.c4Init() } return nil } diff --git a/internal/characters/beidou/cons.go b/internal/characters/beidou/cons.go index 502997851..58f3de8e8 100644 --- a/internal/characters/beidou/cons.go +++ b/internal/characters/beidou/cons.go @@ -8,9 +8,10 @@ import ( "github.com/genshinsim/gcsim/pkg/core/player" ) -const c4key = "beidou-c4" +const c4Key = "beidou-c4" -func (c *char) c4() { +// Upon being attacked, Beidou's Normal Attacks gain an additional instance of 20% Electro DMG for 10s. +func (c *char) c4Init() { c.Core.Events.Subscribe(event.OnPlayerHPDrain, func(args ...interface{}) bool { di := args[0].(player.DrainInfo) if !di.External { @@ -22,23 +23,26 @@ func (c *char) c4() { if c.Core.Player.Active() != c.Index { return false } - c.Core.Status.Add("beidouc4", 600) + c.Core.Status.Add(c4Key, 600) c.Core.Log.NewEvent("c4 triggered on damage", glog.LogCharacterEvent, c.Index). Write("expiry", c.Core.F+600) return false - }, "beidouc4") + }, c4Key) +} - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - t := args[0].(combat.Target) - ae := args[1].(*combat.AttackEvent) - if ae.Info.ActorIndex != c.Index { - return false - } - if ae.Info.AttackTag != combat.AttackTagNormal && ae.Info.AttackTag != combat.AttackTagExtra { - return false +// TODO: this should also be added to her CA +// Beidou's Normal Attacks gain an additional instance of 20% Electro DMG for 10s. +func (c *char) makeC4Callback() combat.AttackCBFunc { + if c.Base.Cons < 4 { + return nil + } + return func(a combat.AttackCB) { + trg := a.Target + if trg.Type() != combat.TargettableEnemy { + return } - if !c.StatusIsActive(c4key) { - return false + if !c.StatusIsActive(c4Key) { + return } c.Core.Log.NewEvent("c4 proc'd on attack", glog.LogCharacterEvent, c.Index). @@ -54,7 +58,6 @@ func (c *char) c4() { Durability: 25, Mult: 0.2, } - c.Core.QueueAttack(ai, combat.NewSingleTargetHit(t.Key()), 0, 1) - return false - }, "beidou-c4") + c.Core.QueueAttack(ai, combat.NewSingleTargetHit(trg.Key()), 0, 1) + } } diff --git a/internal/characters/chongyun/attack.go b/internal/characters/chongyun/attack.go index c663baa5a..3f5762f75 100644 --- a/internal/characters/chongyun/attack.go +++ b/internal/characters/chongyun/attack.go @@ -58,7 +58,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { attackHitboxes[c.NormalCounter][1], ) } - c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter]) + c4CB := c.makeC4Callback() + c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter], c4CB) if c.Base.Cons >= 1 && c.NormalCounter == 3 { ai := combat.AttackInfo{ @@ -84,6 +85,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ), attackHitmarks[c.NormalCounter]+i*5, attackHitmarks[c.NormalCounter]+i*5, + c4CB, ) } } diff --git a/internal/characters/chongyun/burst.go b/internal/characters/chongyun/burst.go index bd92ca81e..e2fb344ce 100644 --- a/internal/characters/chongyun/burst.go +++ b/internal/characters/chongyun/burst.go @@ -34,6 +34,8 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { Mult: burst[c.TalentLvlBurst()], } + c4CB := c.makeC4Callback() + // Spirit Blade 1-3 for _, hitmark := range burstHitmarks { c.Core.QueueAttack( @@ -41,6 +43,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, 3.5), hitmark, hitmark, + c4CB, ) } @@ -51,6 +54,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, 3.5), burstHitmarkC6, burstHitmarkC6, + c4CB, ) } diff --git a/internal/characters/chongyun/chongyun.go b/internal/characters/chongyun/chongyun.go index d7159d51e..70c67b05d 100644 --- a/internal/characters/chongyun/chongyun.go +++ b/internal/characters/chongyun/chongyun.go @@ -38,9 +38,6 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) func (c *char) Init() error { c.onSwapHook() - if c.Base.Cons >= 4 { - c.c4() - } if c.Base.Cons >= 6 && c.Core.Combat.DamageMode { c.c6() } diff --git a/internal/characters/chongyun/cons.go b/internal/characters/chongyun/cons.go index b6e68e56d..3d9b20204 100644 --- a/internal/characters/chongyun/cons.go +++ b/internal/characters/chongyun/cons.go @@ -1,45 +1,41 @@ package chongyun import ( - "github.com/genshinsim/gcsim/pkg/core" "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/glog" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/enemy" "github.com/genshinsim/gcsim/pkg/modifier" ) -func (c *char) c4() { - const icdKey = "chongyun-c4-icd" - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - t, ok := args[0].(core.Reactable) +const c4ICDKey = "chongyun-c4-icd" + +// Chongyun regenerates 1 Energy every time he hits an opponent affected by Cryo. +// This effect can only occur once every 2s. +func (c *char) makeC4Callback() combat.AttackCBFunc { + if c.Base.Cons < 4 { + return nil + } + return func(a combat.AttackCB) { + e, ok := a.Target.(*enemy.Enemy) if !ok { - return false - } - if atk.Info.ActorIndex != c.Index { - return false + return } if c.Core.Player.Active() != c.Index { - return false + return } - if c.StatusIsActive(icdKey) { - return false + if !e.AuraContains(attributes.Cryo) { + return } - if !t.AuraContains(attributes.Cryo) { - return false + if c.StatusIsActive(c4ICDKey) { + return } - + c.AddStatus(c4ICDKey, 2*60, true) c.AddEnergy("chongyun-c4", 2) - c.Core.Log.NewEvent("chongyun c4 recovering 2 energy", glog.LogCharacterEvent, c.Index). Write("final energy", c.Energy) - c.AddStatus(icdKey, 120, true) - - return false - }, "chongyun-c4") + } } func (c *char) c6() { diff --git a/internal/characters/chongyun/skill.go b/internal/characters/chongyun/skill.go index 9f1992fce..792aaaab3 100644 --- a/internal/characters/chongyun/skill.go +++ b/internal/characters/chongyun/skill.go @@ -50,12 +50,14 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { CanBeDefenseHalted: false, } c.skillArea = combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 1.5}, 8) + c4CB := c.makeC4Callback() c.Core.QueueAttack( ai, combat.NewCircleHitOnTarget(c.skillArea.Shape.Pos(), nil, 2.5), 0, skillHitmark, c.particleCB, + c4CB, ) ai = combat.AttackInfo{ @@ -69,7 +71,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { Durability: 25, Mult: skill[c.TalentLvlSkill()], } - cb := func(a combat.AttackCB) { + a4CB := func(a combat.AttackCB) { e, ok := a.Target.(*enemy.Enemy) if !ok { return @@ -106,7 +108,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { Info: ai, Snapshot: snap, } - c.a4Snap.Callbacks = append(c.a4Snap.Callbacks, cb) + c.a4Snap.Callbacks = append(c.a4Snap.Callbacks, a4CB, c4CB) // A4 delayed damage + cryo resist shred // TODO: assuming this is NOT affected by hitlag since it should be tied to deployable? diff --git a/internal/characters/cyno/attack.go b/internal/characters/cyno/attack.go index 84048b3c3..824ae3d15 100644 --- a/internal/characters/cyno/attack.go +++ b/internal/characters/cyno/attack.go @@ -40,6 +40,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { if c.StatusIsActive(BurstKey) { return c.attackB(p) // go to burst mode attacks } + c2CB := c.makeC2CB() + c6CB := c.makeC6CB() for i, mult := range attack[c.NormalCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -72,7 +74,14 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { attackHitboxes[c.NormalCounter][1], ) } - c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter][i], attackHitmarks[c.NormalCounter][i]) + c.Core.QueueAttack( + ai, + ap, + attackHitmarks[c.NormalCounter][i], + attackHitmarks[c.NormalCounter][i], + c2CB, + c6CB, + ) } defer c.AdvanceNormalIndex() @@ -118,6 +127,8 @@ func init() { func (c *char) attackB(p map[string]int) action.ActionInfo { c.tryBurstPPSlide(attackBHitmarks[c.normalBCounter][len(attackBHitmarks[c.normalBCounter])-1]) + c2CB := c.makeC2CB() + c6CB := c.makeC6CB() for i, mult := range attackB[c.normalBCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -152,7 +163,7 @@ func (c *char) attackB(p map[string]int) action.ActionInfo { ) } c.QueueCharTask(func() { - c.Core.QueueAttack(ai, ap, 0, 0) + c.Core.QueueAttack(ai, ap, 0, 0, c2CB, c6CB) }, attackBHitmarks[c.normalBCounter][i]) } diff --git a/internal/characters/cyno/burst.go b/internal/characters/cyno/burst.go index ff25cadf5..574154f9c 100644 --- a/internal/characters/cyno/burst.go +++ b/internal/characters/cyno/burst.go @@ -57,10 +57,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { if c.Base.Cons >= 1 { c.c1() } - if c.Base.Cons >= 6 { // constellation 6 giving 4 stacks on burst - c.c6Stacks = 4 - c.AddStatus(c6Key, 480, true) // 8s*60 - } + c.c6Init() return action.ActionInfo{ Frames: frames.NewAbilFunc(burstFrames), diff --git a/internal/characters/cyno/cons.go b/internal/characters/cyno/cons.go index a1d72354d..3f7d6cf76 100644 --- a/internal/characters/cyno/cons.go +++ b/internal/characters/cyno/cons.go @@ -34,45 +34,43 @@ func (c *char) c1() { }) } +const c2Key = "cyno-c2" +const c2ICD = "cyno-c2-icd" + // When Cyno's Normal Attacks hit opponents, his Electro DMG Bonus will // increase by 10% for 4s. This effect can be triggered once every 0.1s. Max 5 // stacks. -func (c *char) c2() { - const c2Key = "cyno-c2" - const c2Icd = "cyno-c2-icd" - stacks := 0 - m := make([]float64, attributes.EndStatType) - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if atk.Info.ActorIndex != c.Index { - return false +func (c *char) makeC2CB() combat.AttackCBFunc { + if c.Base.Cons < 2 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - if c.StatusIsActive(c2Icd) { - return false - } - if atk.Info.AttackTag != combat.AttackTagNormal { - return false + if c.StatusIsActive(c2ICD) { + return } + c.AddStatus(c2ICD, 0.1*60, true) if !c.StatModIsActive(c2Key) { - stacks = 0 + c.c2Stacks = 0 } - stacks++ - if stacks > 5 { - stacks = 5 + c.c2Stacks++ + if c.c2Stacks > 5 { + c.c2Stacks = 5 } + m := make([]float64, attributes.EndStatType) c.AddStatMod(character.StatMod{ - Base: modifier.NewBaseWithHitlag(c2Key, 240), // 4s + Base: modifier.NewBaseWithHitlag(c2Key, 4*60), AffectedStat: attributes.ElectroP, Amount: func() ([]float64, bool) { - m[attributes.ElectroP] = 0.1 * float64(stacks) + m[attributes.ElectroP] = 0.1 * float64(c.c2Stacks) return m, true }, }) - c.AddStatus(c2Icd, 6, true) // 0.1s icd - return false - }, "cyno-c2") + } } // When Cyno is in the Pactsworn Pathclearer state triggered by Sacred Rite: @@ -117,31 +115,41 @@ func (c *char) c4() { } // After using Sacred Rite: Wolf's Swiftness or triggering the Judication effect of the Passive Talent "Featherfall Judgment," -// Cyno will gain 4 stacks of the "Day of the Jackal" effect. When he hits opponents with Normal Attacks, -// he will consume 1 stack of "Day of the Jackal" to fire off one Duststalker Bolt. +// Cyno will gain 4 stacks of the "Day of the Jackal" effect. +func (c *char) c6Init() { + if c.Base.Cons < 6 { + return + } + c.AddStatus(c6Key, 8*60, true) + c.c6Stacks += 4 + if c.c6Stacks > 8 { + c.c6Stacks = 8 + } +} + +// When he hits opponents with Normal Attacks, he will consume 1 stack of "Day of the Jackal" to fire off one Duststalker Bolt. // "Day of the Jackal" lasts for 8s. Max 8 stacks. It will be canceled once Pactsworn Pathclearer ends. // A maximum of 1 Duststalker Bolt can be unleashed this way every 0.4s. // You must first unlock the Passive Talent "Featherfall Judgment." -func (c *char) c6() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if atk.Info.ActorIndex != c.Index { - return false +func (c *char) makeC6CB() combat.AttackCBFunc { + if c.Base.Cons < 6 || c.c6Stacks == 0 || !c.StatusIsActive(c6Key) { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } if c.c6Stacks == 0 { - return false - } - if atk.Info.AttackTag != combat.AttackTagNormal { - return false + return } if !c.StatusIsActive(c6Key) { - return false + return } if c.StatusIsActive(c6ICDKey) { - return false + return } - - c.AddStatus(c6ICDKey, 24, true) + c.AddStatus(c6ICDKey, 0.4*60, true) + c.c6Stacks-- // technically should use ICDGroupCynoC6, but it's just reskinned standard ICD ai := combat.AttackInfo{ @@ -169,8 +177,5 @@ func (c *char) c6() { 0, 0, ) - - c.c6Stacks-- - return false - }, "cyno-c6") + } } diff --git a/internal/characters/cyno/cyno.go b/internal/characters/cyno/cyno.go index 92cb226e7..96181d02c 100644 --- a/internal/characters/cyno/cyno.go +++ b/internal/characters/cyno/cyno.go @@ -17,6 +17,7 @@ type char struct { burstExtension int burstSrc int lastSkillCast int + c2Stacks int c4Counter int c6Stacks int a1Extended bool @@ -41,18 +42,10 @@ func (c *char) Init() error { c.onExitField() c.a1Extension() - if c.Base.Cons >= 2 { - c.c2() - } - if c.Base.Cons >= 4 { c.c4() } - if c.Base.Cons >= 6 { - c.c6() - } - return nil } diff --git a/internal/characters/cyno/skill.go b/internal/characters/cyno/skill.go index ab80f86f5..cb717600c 100644 --- a/internal/characters/cyno/skill.go +++ b/internal/characters/cyno/skill.go @@ -103,13 +103,7 @@ func (c *char) skillB() action.ActionInfo { if c.Base.Cons >= 1 && c.StatusIsActive(c1Key) { c.c1() } - if c.Base.Cons >= 6 { // constellation 6 giving 4 stacks on judication - c.c6Stacks += 4 - c.AddStatus(c6Key, 480, true) // 8s*60 - if c.c6Stacks > 8 { - c.c6Stacks = 8 - } - } + c.c6Init() c.Core.QueueAttack(ai, ap, skillBHitmark, skillBHitmark, particleCB) // Apply the extra hit diff --git a/internal/characters/dori/attack.go b/internal/characters/dori/attack.go index 11ffe5ef0..b171c1b0f 100644 --- a/internal/characters/dori/attack.go +++ b/internal/characters/dori/attack.go @@ -27,6 +27,7 @@ func init() { } func (c *char) Attack(p map[string]int) action.ActionInfo { + c6CB := c.makeC6CB() for i, mult := range auto[c.NormalCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -48,7 +49,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { 2, ) c.QueueCharTask(func() { - c.Core.QueueAttack(ai, ap, 0, 0) + c.Core.QueueAttack(ai, ap, 0, 0, c6CB) }, attackHitmarks[c.NormalCounter][i]) } diff --git a/internal/characters/dori/cons.go b/internal/characters/dori/cons.go index b0aeabc62..3d1c20eea 100644 --- a/internal/characters/dori/cons.go +++ b/internal/characters/dori/cons.go @@ -3,7 +3,6 @@ package dori import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/player" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/modifier" @@ -68,30 +67,33 @@ func (c *char) c4() { } } +const c6ICD = "dori-c6-heal-icd" +const c6Key = "dori-c6" + // Dori gains the following effects for 3s after using Spirit-Warding Lamp: Troubleshooter Cannon: -// ·Electro Infusion. -// ·When Normal Attacks hit opponents, all nearby party members will heal HP equivalent to 4% of Dori's Max HP. +// - Electro Infusion. +// - When Normal Attacks hit opponents, all nearby party members will heal HP equivalent to 4% of Dori's Max HP. // This type of healing can occur once every 0.1s. -func (c *char) c6() { - const c6icd = "dori-c6-heal-icd" - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if atk.Info.ActorIndex != c.Index { - return false +func (c *char) makeC6CB() combat.AttackCBFunc { + if c.Base.Cons < 6 || !c.Core.Player.WeaponInfuseIsActive(c.Index, c6Key) { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - if !c.Core.Player.WeaponInfuseIsActive(c.Index, c6key) { - return false + if c.Core.Player.Active() != c.Index { + return } - if c.StatusIsActive(c6icd) { - return false + if !c.Core.Player.WeaponInfuseIsActive(c.Index, c6Key) { + return } - if atk.Info.AttackTag != combat.AttackTagNormal { - return false + if c.StatusIsActive(c6ICD) { + return } + c.AddStatus(c6ICD, 0.1*60, true) - c.AddStatus(c6icd, 6, true) // 0.1s*60 icd // heal party members - c.Core.Player.Heal(player.HealInfo{ Caller: c.Index, Target: -1, @@ -99,7 +101,5 @@ func (c *char) c6() { Src: 0.04 * c.MaxHP(), Bonus: c.Stat(attributes.Heal), }) - - return false - }, "dori-c6") + } } diff --git a/internal/characters/dori/dori.go b/internal/characters/dori/dori.go index 76c967c43..8f83ed63b 100644 --- a/internal/characters/dori/dori.go +++ b/internal/characters/dori/dori.go @@ -17,8 +17,6 @@ type char struct { afterCount int } -const c6key = "dori-c6" - func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error { c := char{} c.Character = tmpl.NewWithWrapper(s, w) @@ -39,9 +37,5 @@ func (c *char) Init() error { if c.Base.Cons >= 1 { c.c1() } - - if c.Base.Cons >= 6 { - c.c6() - } return nil } diff --git a/internal/characters/dori/skill.go b/internal/characters/dori/skill.go index fcc614363..4bfd24c45 100644 --- a/internal/characters/dori/skill.go +++ b/internal/characters/dori/skill.go @@ -45,7 +45,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { if c.Base.Cons >= 6 { c.Core.Player.AddWeaponInfuse( c.Index, - c6key, + c6Key, attributes.Electro, 228, // 3s + 0.8s according to dm true, diff --git a/internal/characters/eula/asc.go b/internal/characters/eula/asc.go index f81d15adc..fec51dded 100644 --- a/internal/characters/eula/asc.go +++ b/internal/characters/eula/asc.go @@ -33,6 +33,7 @@ func (c *char) a1() { combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 2}, 6.5), a1Hitmark-(skillHoldHitmark+1), a1Hitmark-(skillHoldHitmark+1), + c.burstStackCB, ) }, skillHoldHitmark+1) } diff --git a/internal/characters/eula/attack.go b/internal/characters/eula/attack.go index 219f452cf..e9469a439 100644 --- a/internal/characters/eula/attack.go +++ b/internal/characters/eula/attack.go @@ -32,7 +32,6 @@ func init() { } func (c *char) Attack(p map[string]int) action.ActionInfo { - for i, mult := range auto[c.NormalCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -63,7 +62,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ) } c.QueueCharTask(func() { - c.Core.QueueAttack(ai, ap, 0, 0) + c.Core.QueueAttack(ai, ap, 0, 0, c.burstStackCB) }, attackHitmarks[c.NormalCounter][i]) } diff --git a/internal/characters/eula/burst.go b/internal/characters/eula/burst.go index bb3e8d531..c2fdc972e 100644 --- a/internal/characters/eula/burst.go +++ b/internal/characters/eula/burst.go @@ -24,7 +24,8 @@ func init() { } const ( - burstKey = "eula-q" + burstKey = "eula-q" + burstStackICDKey = "eula-q-stack-icd" ) // ult 365 to 415, 60fps = 120 @@ -117,44 +118,37 @@ func (c *char) triggerBurst() { c.burstCounter = 0 } -func (c *char) burstStacks() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - dmg := args[2].(float64) - if c.Core.Status.Duration(burstKey) == 0 { - return false - } - if atk.Info.ActorIndex != c.Index { - return false - } - //TODO: this looks like the icd is dependent on gadget timer. need to double check - if c.burstCounterICD > c.Core.F { - return false - } - switch atk.Info.AttackTag { - case combat.AttackTagElementalArt: - case combat.AttackTagElementalBurst: - case combat.AttackTagNormal: - default: - return false - } - if dmg == 0 { - return false - } - - //add to counter +// When Eula's own Normal Attack, Elemental Skill, and Elemental Burst deal DMG to opponents, +// they will charge the Lightfall Sword, which can gain an energy stack once every 0.1s. +func (c *char) burstStackCB(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if c.Core.Player.Active() != c.Index { + return + } + if c.Core.Status.Duration(burstKey) == 0 { + return + } + if a.Damage == 0 { + return + } + if c.StatusIsActive(burstStackICDKey) { + return + } + //TODO: looks like the icd is dependent on gadget timer. need to double check + c.AddStatus(burstStackICDKey, 0.1*60, false) + + // add to counter + c.burstCounter++ + c.Core.Log.NewEvent("eula burst add stack", glog.LogCharacterEvent, c.Index). + Write("stack count", c.burstCounter) + // check for c6 + if c.Base.Cons == 6 && c.Core.Rand.Float64() < 0.5 { c.burstCounter++ - c.Core.Log.NewEvent("eula burst add stack", glog.LogCharacterEvent, c.Index). + c.Core.Log.NewEvent("eula c6 add additional stack", glog.LogCharacterEvent, c.Index). Write("stack count", c.burstCounter) - //check for c6 - if c.Base.Cons == 6 && c.Core.Rand.Float64() < 0.5 { - c.burstCounter++ - c.Core.Log.NewEvent("eula c6 add additional stack", glog.LogCharacterEvent, c.Index). - Write("stack count", c.burstCounter) - } - c.burstCounterICD = c.Core.F + 6 - return false - }, "eula-burst-counter") + } } func (c *char) onExitField() { diff --git a/internal/characters/eula/eula.go b/internal/characters/eula/eula.go index 59350e7cf..170c573fa 100644 --- a/internal/characters/eula/eula.go +++ b/internal/characters/eula/eula.go @@ -16,7 +16,6 @@ func init() { type char struct { *tmpl.Character burstCounter int - burstCounterICD int grimheartStacks int c1buff []float64 particleDone bool @@ -41,7 +40,6 @@ func (c *char) Init() error { c.c1buff = make([]float64, attributes.EndStatType) c.c1buff[attributes.PhyP] = 0.3 } - c.burstStacks() c.onExitField() if c.Base.Cons >= 4 { c.c4() diff --git a/internal/characters/eula/skill.go b/internal/characters/eula/skill.go index 69969d1cb..51bd6d957 100644 --- a/internal/characters/eula/skill.go +++ b/internal/characters/eula/skill.go @@ -112,6 +112,7 @@ func (c *char) pressSkill(p map[string]int) action.ActionInfo { skillPressHitmark, cb, c.pressParticleCB, + c.burstStackCB, ) c.SetCDWithDelay(action.ActionSkill, 60*4, 16) @@ -165,6 +166,7 @@ func (c *char) holdSkill(p map[string]int) action.ActionInfo { skillHoldHitmark, skillHoldHitmark, c.holdParticleCB, + c.burstStackCB, ) v := c.currentGrimheartStacks() @@ -217,6 +219,7 @@ func (c *char) holdSkill(p map[string]int) action.ActionInfo { icewhirlHitmarks[i], icewhirlHitmarks[i], shredCB, + c.burstStackCB, ) } else { c.QueueCharTask(func() { @@ -227,6 +230,7 @@ func (c *char) holdSkill(p map[string]int) action.ActionInfo { 0, 0, shredCB, + c.burstStackCB, ) }, icewhirlHitmarks[i]) } diff --git a/internal/characters/keqing/attack.go b/internal/characters/keqing/attack.go index 517ec8e76..668bc2205 100644 --- a/internal/characters/keqing/attack.go +++ b/internal/characters/keqing/attack.go @@ -45,6 +45,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { if c.NormalCounter == 4 { centerTarget = c.Core.Combat.PrimaryTarget() // N5 is a bullet } + c2CB := c.makeC2CB() for i, mult := range attack[c.NormalCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -76,7 +77,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ) } c.QueueCharTask(func() { - c.Core.QueueAttack(ai, ap, 0, 0) + c.Core.QueueAttack(ai, ap, 0, 0, c2CB) }, attackHitmarks[c.NormalCounter][i]) } diff --git a/internal/characters/keqing/charge.go b/internal/characters/keqing/charge.go index e94812c38..fac2305d4 100644 --- a/internal/characters/keqing/charge.go +++ b/internal/characters/keqing/charge.go @@ -35,6 +35,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { Element: attributes.Physical, Durability: 25, } + c2CB := c.makeC2CB() for i, mult := range charge { ai.Mult = mult[c.TalentLvlAttack()] ai.Abil = fmt.Sprintf("Charge %v", i) @@ -47,6 +48,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { ), chargeHitmarks[i], chargeHitmarks[i], + c2CB, ) } diff --git a/internal/characters/keqing/cons.go b/internal/characters/keqing/cons.go index 3c4eba9d6..5f2358c4b 100644 --- a/internal/characters/keqing/cons.go +++ b/internal/characters/keqing/cons.go @@ -13,35 +13,33 @@ import ( const c2ICDKey = "keqing-c2-icd" -func (c *char) c2() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - e, ok := args[0].(*enemy.Enemy) +// When Keqing's Normal and Charged Attacks hit opponents affected by Electro, +// they have a 50% chance of producing an Elemental Particle. +// This effect can only occur once every 5s. +func (c *char) makeC2CB() combat.AttackCBFunc { + if c.Base.Cons < 2 { + return nil + } + return func(a combat.AttackCB) { + e, ok := a.Target.(*enemy.Enemy) if !ok { - return false - } - if atk.Info.AttackTag != combat.AttackTagNormal && atk.Info.AttackTag != combat.AttackTagExtra { - return false - } - if atk.Info.ActorIndex != c.Index { - return false + return } if c.Core.Player.Active() != c.Index { - return false + return } if !e.AuraContains(attributes.Electro) { - return false + return } if c.StatusIsActive(c2ICDKey) { - return false + return } if c.Core.Rand.Float64() < 0.5 { - c.AddStatus(c2ICDKey, 300, true) + c.AddStatus(c2ICDKey, 5*60, true) c.Core.QueueParticle("keqing-c2", 1, attributes.Electro, c.ParticleDelay) c.Core.Log.NewEvent("keqing c2 proc'd", glog.LogCharacterEvent, c.Index) } - return false - }, "keqing-c2") + } } func (c *char) c4() { diff --git a/internal/characters/keqing/keqing.go b/internal/characters/keqing/keqing.go index 822b7dac4..4607db4ff 100644 --- a/internal/characters/keqing/keqing.go +++ b/internal/characters/keqing/keqing.go @@ -40,9 +40,6 @@ func (c *char) Init() error { c.a4buff[attributes.CR] = 0.15 c.a4buff[attributes.ER] = 0.15 - if c.Base.Cons >= 2 { - c.c2() - } if c.Base.Cons >= 4 { c.c4buff = make([]float64, attributes.EndStatType) c.c4buff[attributes.ATKP] = 0.25 diff --git a/internal/characters/klee/asc.go b/internal/characters/klee/asc.go index 939968cd8..dcca657b4 100644 --- a/internal/characters/klee/asc.go +++ b/internal/characters/klee/asc.go @@ -2,7 +2,6 @@ package klee import ( "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/glog" ) @@ -27,26 +26,26 @@ func (c *char) makeA1CB() combat.AttackCBFunc { } } +const a4ICDKey = "klee-a4-icd" + // When Klee's Charged Attack results in a CRIT Hit, all party members gain 2 Elemental Energy. -func (c *char) a4() { +func (c *char) makeA4CB() combat.AttackCBFunc { if c.Base.Ascension < 4 { - return + return nil } - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - crit := args[3].(bool) - if atk.Info.ActorIndex != c.Index { - return false + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - if atk.Info.AttackTag != combat.AttackTagExtra { - return false + if !a.IsCrit { + return } - if !crit { - return false + if c.StatusIsActive(a4ICDKey) { + return } + c.AddStatus(a4ICDKey, 0.6*60, true) for _, x := range c.Core.Player.Chars() { x.AddEnergy("klee-a4", 2) } - return false - }, "kleea1") + } } diff --git a/internal/characters/klee/charge.go b/internal/characters/klee/charge.go index fa323f8b9..7e867536b 100644 --- a/internal/characters/klee/charge.go +++ b/internal/characters/klee/charge.go @@ -62,6 +62,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { snap, combat.NewCircleHit(c.Core.Combat.Player(), c.Core.Combat.PrimaryTarget(), nil, 3), chargeHitmark-windup+travel, + c.makeA4CB(), ) c.c1(chargeHitmark - windup + travel) diff --git a/internal/characters/klee/klee.go b/internal/characters/klee/klee.go index 3af020025..807e1f72e 100644 --- a/internal/characters/klee/klee.go +++ b/internal/characters/klee/klee.go @@ -36,7 +36,6 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) } func (c *char) Init() error { - c.a4() c.onExitField() return nil } diff --git a/internal/characters/kokomi/attack.go b/internal/characters/kokomi/attack.go index 09874a981..9ee2767cf 100644 --- a/internal/characters/kokomi/attack.go +++ b/internal/characters/kokomi/attack.go @@ -61,6 +61,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { combat.NewCircleHit(c.Core.Combat.Player(), c.Core.Combat.PrimaryTarget(), nil, radius), attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter]+travel, + c.makeBurstHealCB(), + c.makeC4CB(), ) if c.NormalCounter == c.NormalHitNum-1 { c.c1(attackHitmarks[c.NormalCounter], travel) diff --git a/internal/characters/kokomi/burst.go b/internal/characters/kokomi/burst.go index 9eb21353d..3bb263559 100644 --- a/internal/characters/kokomi/burst.go +++ b/internal/characters/kokomi/burst.go @@ -104,24 +104,24 @@ func (c *char) burstDmgBonus(a combat.AttackTag) float64 { } } -// Implements event handler for healing during burst -// Also checks constellations -func (c *char) burstActiveHook() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if atk.Info.ActorIndex != c.Index { - return false +// - implements burst healing, C2 and C6 handling +// +// When her Normal and Charged Attacks hit opponents, +// Kokomi will restore HP for all nearby party members, +// and the amount restored is based on her Max HP. +func (c *char) makeBurstHealCB() combat.AttackCBFunc { + done := false + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - if c.Core.Status.Duration("kokomiburst") == 0 { - return false + return } - - switch atk.Info.AttackTag { - case combat.AttackTagNormal, combat.AttackTagExtra: - default: - return false + if done { + return } + done = true heal := burstHealPct[c.TalentLvlBurst()]*c.MaxHP() + burstHealFlat[c.TalentLvlBurst()] for _, char := range c.Core.Player.Chars() { @@ -145,15 +145,10 @@ func (c *char) burstActiveHook() { }) } - if c.Base.Cons >= 4 { - c.c4() - } if c.Base.Cons >= 6 { c.c6() } - - return false - }, "kokomi-q-healing") + } } // Clears Kokomi burst when she leaves the field diff --git a/internal/characters/kokomi/charge.go b/internal/characters/kokomi/charge.go index 00c1f688a..a2cf17252 100644 --- a/internal/characters/kokomi/charge.go +++ b/internal/characters/kokomi/charge.go @@ -55,6 +55,8 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, radius), chargeHitmark-windup, chargeHitmark-windup, + c.makeBurstHealCB(), + c.makeC4CB(), ) return action.ActionInfo{ diff --git a/internal/characters/kokomi/cons.go b/internal/characters/kokomi/cons.go index 3ffc64957..6e7d0b2b5 100644 --- a/internal/characters/kokomi/cons.go +++ b/internal/characters/kokomi/cons.go @@ -38,15 +38,25 @@ func (c *char) c1(f, travel int) { ) } +const c4ICDKey = "kokomi-c4-icd" + // C4 (Energy piece only) handling // While donning the Ceremonial Garment created by Nereid's Ascension, Sangonomiya Kokomi's Normal Attack SPD is increased by 10%. // and Normal Attacks that hit opponents will restore 0.8 Energy for her. This effect can occur once every 0.2s. -func (c *char) c4() { - if c.Core.F < c.c4ICDExpiry { - return +func (c *char) makeC4CB() combat.AttackCBFunc { + if c.Base.Cons < 4 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if c.StatusIsActive(c4ICDKey) { + return + } + c.AddStatus(c4ICDKey, 0.2*60, true) + c.AddEnergy("kokomi-c4", 0.8) } - c.c4ICDExpiry = c.Core.F + 12 - c.AddEnergy("kokomi-c4", 0.8) } // C6 handling diff --git a/internal/characters/kokomi/kokomi.go b/internal/characters/kokomi/kokomi.go index ff9882dca..e90b1d6f2 100644 --- a/internal/characters/kokomi/kokomi.go +++ b/internal/characters/kokomi/kokomi.go @@ -17,7 +17,6 @@ type char struct { skillFlatDmg float64 skillLastUsed int swapEarlyF int - c4ICDExpiry int } func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error { @@ -29,11 +28,6 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) c.BurstCon = 3 c.SkillCon = 5 - c.skillFlatDmg = 0 - c.skillLastUsed = 0 - c.swapEarlyF = 0 - c.c4ICDExpiry = 0 - w.Character = &c return nil @@ -43,6 +37,5 @@ func (c *char) Init() error { c.a4() c.passive() c.onExitField() - c.burstActiveHook() return nil } diff --git a/internal/characters/mona/charge.go b/internal/characters/mona/charge.go index 548a11603..14a91ea21 100644 --- a/internal/characters/mona/charge.go +++ b/internal/characters/mona/charge.go @@ -51,6 +51,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { ), chargeHitmark-windup, chargeHitmark-windup, + c.makeC6CAResetCB(), ) return action.ActionInfo{ diff --git a/internal/characters/mona/cons.go b/internal/characters/mona/cons.go index 0bf752afd..19fa8f12b 100644 --- a/internal/characters/mona/cons.go +++ b/internal/characters/mona/cons.go @@ -172,26 +172,21 @@ func (c *char) c6(src int) func() { } } -func (c *char) c6CAReset() { - // handle C6 stack reset if CA used before c6 buff expires - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if c.Core.Player.Active() != c.Index { - return false - } - if atk.Info.ActorIndex != c.Index { - return false - } - if atk.Info.AttackTag != combat.AttackTagExtra { - return false +func (c *char) makeC6CAResetCB() combat.AttackCBFunc { + if c.Base.Cons < 6 || !c.StatusIsActive(c6Key) { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() == combat.TargettableEnemy { + return } - if c.StatusIsActive(c6Key) { - c.c6Stacks = 0 - c.DeleteStatus(c6Key) - c.Core.Log.NewEvent(fmt.Sprintf("%v stacks reset via charge attack", c6Key), glog.LogCharacterEvent, c.Index) + if !c.StatusIsActive(c6Key) { + return } - return false - }, fmt.Sprintf("%v-reset", c6Key)) + c.DeleteStatus(c6Key) + c.c6Stacks = 0 + c.Core.Log.NewEvent(fmt.Sprintf("%v stacks reset via charge attack", c6Key), glog.LogCharacterEvent, c.Index) + } } func (c *char) c6TimerReset() { diff --git a/internal/characters/mona/mona.go b/internal/characters/mona/mona.go index 61ec02f8e..acfca1d76 100644 --- a/internal/characters/mona/mona.go +++ b/internal/characters/mona/mona.go @@ -50,8 +50,5 @@ func (c *char) Init() error { if c.Base.Cons >= 4 { c.c4() } - if c.Base.Cons >= 6 { - c.c6CAReset() - } return nil } diff --git a/internal/characters/nahida/attack.go b/internal/characters/nahida/attack.go index 432737942..2e1b3cd29 100644 --- a/internal/characters/nahida/attack.go +++ b/internal/characters/nahida/attack.go @@ -83,6 +83,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ), attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter], + c.makeC6CB(), ) defer c.AdvanceNormalIndex() diff --git a/internal/characters/nahida/charge.go b/internal/characters/nahida/charge.go index 755d20981..3b6461138 100644 --- a/internal/characters/nahida/charge.go +++ b/internal/characters/nahida/charge.go @@ -50,6 +50,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { ), chargeHitmark-windup, chargeHitmark-windup, + c.makeC6CB(), ) return action.ActionInfo{ diff --git a/internal/characters/nahida/cons.go b/internal/characters/nahida/cons.go index c17929995..4308dce31 100644 --- a/internal/characters/nahida/cons.go +++ b/internal/characters/nahida/cons.go @@ -116,36 +116,29 @@ const ( // is considered Elemental Skill DMG and can be triggered once every 0.2s. This // effect can last up to 10s and will be removed after Nahida has unleashed 6 // instances of Tri-Karma Purification: Karmic Oblivion. -func (c *char) c6() { - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - e, ok := args[0].(*enemy.Enemy) +func (c *char) makeC6CB() combat.AttackCBFunc { + if c.Base.Cons < 6 { + return nil + } + return func(a combat.AttackCB) { + e, ok := a.Target.(*enemy.Enemy) if !ok { - return false + return } - if c.c6Count >= 6 { - return false + if !e.StatusIsActive(skillMarkKey) { + return } - if c.StatusIsActive(c6ICDKey) { - return false + if c.c6Count >= 6 { + return } if !c.StatusIsActive(c6ActiveKey) { - return false - } - ae := args[1].(*combat.AttackEvent) - if ae.Info.ActorIndex != c.Index { - return false + return } - switch ae.Info.AttackTag { - case combat.AttackTagNormal: - case combat.AttackTagExtra: - default: - return false - } - if !e.StatusIsActive(skillMarkKey) { - return false + if c.StatusIsActive(c6ICDKey) { + return } - c.AddStatus(c6ICDKey, 12, true) //TODO: hitlag? - c.c6Count++ + c.AddStatus(c6ICDKey, 0.2*60, true) + ai := combat.AttackInfo{ ActorIndex: c.Index, Abil: "Tri-Karma Purification: Karmic Oblivion", @@ -175,7 +168,6 @@ func (c *char) c6() { ) } - return false - }, "nahida-c6") - + c.c6Count++ + } } diff --git a/internal/characters/nahida/nahida.go b/internal/characters/nahida/nahida.go index 120d9f91e..2cd60ae66 100644 --- a/internal/characters/nahida/nahida.go +++ b/internal/characters/nahida/nahida.go @@ -99,9 +99,5 @@ func (c *char) Init() error { c.c2() } - if c.Base.Cons >= 6 { - c.c6() - } - return nil } diff --git a/internal/characters/rosaria/attack.go b/internal/characters/rosaria/attack.go index 5bf4b1d9b..47a29c734 100644 --- a/internal/characters/rosaria/attack.go +++ b/internal/characters/rosaria/attack.go @@ -43,7 +43,7 @@ func init() { // Normal attack damage queue generator // relatively standard with no major differences versus other characters func (c *char) Attack(p map[string]int) action.ActionInfo { - + c1CB := c.makeC1CB() for i, mult := range attack[c.NormalCounter] { ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -77,7 +77,7 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ) } c.QueueCharTask(func() { - c.Core.QueueAttack(ai, ap, 0, 0) + c.Core.QueueAttack(ai, ap, 0, 0, c1CB) }, attackHitmarks[c.NormalCounter][i]) } diff --git a/internal/characters/rosaria/burst.go b/internal/characters/rosaria/burst.go index 6ad7d68ed..8881ab1c0 100644 --- a/internal/characters/rosaria/burst.go +++ b/internal/characters/rosaria/burst.go @@ -39,6 +39,9 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { CanBeDefenseHalted: false, } + c1CB := c.makeC1CB() + c6CB := c.makeC6CB() + // Hit 1 comes out on frame 15 // 2nd hit comes after lance drop animation finishes // center on player @@ -47,7 +50,8 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 0.5}, 3.5), 15, 15, - c.c6, + c1CB, + c6CB, ) ai.Abil = "Rites of Termination (Hit 2)" @@ -71,7 +75,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { // lance lands at 56f if we exclude hitlag (60f was with hitlag) c.QueueCharTask(func() { // Hit 2 - c.Core.QueueAttack(ai, apHit2, 0, 0, c.c6) + c.Core.QueueAttack(ai, apHit2, 0, 0, c1CB, c6CB) // Burst status c.Core.Status.Add("rosariaburst", dur) @@ -90,7 +94,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { // DoT every 2 seconds after lance lands for i := 120; i < dur; i += 120 { - c.Core.QueueAttack(ai, apTick, 0, i, c.c6) + c.Core.QueueAttack(ai, apTick, 0, i, c1CB, c6CB) } }, 56) diff --git a/internal/characters/rosaria/charge.go b/internal/characters/rosaria/charge.go index 175c1c016..e90f210a3 100644 --- a/internal/characters/rosaria/charge.go +++ b/internal/characters/rosaria/charge.go @@ -46,6 +46,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { ), chargeHitmark, chargeHitmark, + c.makeC1CB(), ) return action.ActionInfo{ diff --git a/internal/characters/rosaria/cons.go b/internal/characters/rosaria/cons.go index 2c445fec5..3d16acf26 100644 --- a/internal/characters/rosaria/cons.go +++ b/internal/characters/rosaria/cons.go @@ -3,86 +3,83 @@ package rosaria import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/enemy" "github.com/genshinsim/gcsim/pkg/modifier" ) -// Adds event checker for C1: Unholy Revelation // When Rosaria deals a CRIT Hit, her ATK Speed increase by 10% and her Normal Attack DMG increases by 10% for 4s (can trigger vs shielded enemies) -// TODO: Description is unclear whether attack speed affects NA + CA - assume that it only affects NA for now -func (c *char) c1() { - c.c1bonus = make([]float64, attributes.EndStatType) - c.c1bonus[attributes.AtkSpd] = 0.1 - c.c1bonus[attributes.DmgP] = 0.1 - // Add hook that monitors for crit hits. Mirrors existing favonius code - // No log value saved as stat mod already shows up in debug view - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - crit := args[3].(bool) - if !crit { - return false +func (c *char) makeC1CB() combat.AttackCBFunc { + if c.Base.Cons < 1 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if c.Core.Player.Active() != c.Index { + return } - if atk.Info.ActorIndex != c.Index { - return false + if !a.IsCrit { + return } - // doesn't work off-field if c.Core.Player.Active() != c.Index { - return false + return } + m := make([]float64, attributes.EndStatType) + m[attributes.AtkSpd] = 0.1 + m[attributes.DmgP] = 0.1 c.AddAttackMod(character.AttackMod{ Base: modifier.NewBaseWithHitlag("rosaria-c1", 240), //4s Amount: func(atk *combat.AttackEvent, _ combat.Target) ([]float64, bool) { if atk.Info.AttackTag != combat.AttackTagNormal { return nil, false } - return c.c1bonus, true + return m, true }, }) - - return false - }, "rosaria-c1") + } } -// Adds event checker for C4 Painful Grace // Ravaging Confession's CRIT Hits regenerate 5 Energy for Rosaria. Can only be triggered once each time Ravaging Confession is cast. -// Only applies when a crit hit is resolved, so can't be handled within skill code directly -// TODO: Since this only is needed for her E, can change this so it spawns a subscription in her E code -// Then it can return true, which kills the callback -// However, would also need a timeout function as well since her E can not crit -// Requires additional work and references - will leave implementation for later -// TODO: conver this into a callback on first skill? -func (c *char) c4(a combat.AttackCB) { - if a.Target.Type() != combat.TargettableEnemy { - return - } - if c.c4completed { - return +func (c *char) makeC4CB() combat.AttackCBFunc { + if c.Base.Cons < 4 { + return nil } - //check for crit - if !a.IsCrit { - return + done := false + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if c.Core.Player.Active() != c.Index { + return + } + if !a.IsCrit { + return + } + if done { + return + } + done = true + c.AddEnergy("rosaria-c4", 5) } - c.AddEnergy("rosaria-c4", 5) - c.c4completed = true } -// Applies C6 effect to enemies hit by it // Rites of Termination's attack decreases opponent's Physical RES by 20% for 10s. -// Takes in a snapshot definition, and returns the same snapshot with an on hit callback added to apply the debuff -func (c *char) c6(a combat.AttackCB) { +func (c *char) makeC6CB() combat.AttackCBFunc { if c.Base.Cons < 6 { - return + return nil } - e, ok := a.Target.(*enemy.Enemy) - if !ok { - return + return func(a combat.AttackCB) { + e, ok := a.Target.(*enemy.Enemy) + if !ok { + return + } + e.AddResistMod(combat.ResistMod{ + Base: modifier.NewBaseWithHitlag("rosaria-c6", 600), + Ele: attributes.Physical, + Value: -0.2, + }) } - e.AddResistMod(combat.ResistMod{ - Base: modifier.NewBaseWithHitlag("rosaria-c6", 600), - Ele: attributes.Physical, - Value: -0.2, - }) } diff --git a/internal/characters/rosaria/rosaria.go b/internal/characters/rosaria/rosaria.go index 74697b654..f091c525e 100644 --- a/internal/characters/rosaria/rosaria.go +++ b/internal/characters/rosaria/rosaria.go @@ -14,8 +14,6 @@ func init() { type char struct { *tmpl.Character - c1bonus []float64 - c4completed bool } func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error { @@ -33,8 +31,5 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) } func (c *char) Init() error { - if c.Base.Cons >= 1 { - c.c1() - } return nil } diff --git a/internal/characters/rosaria/skill.go b/internal/characters/rosaria/skill.go index 9e299ed6a..a9bd7e12f 100644 --- a/internal/characters/rosaria/skill.go +++ b/internal/characters/rosaria/skill.go @@ -42,22 +42,21 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { } // We always assume that A1 procs on hit 1 to simplify - var a1cb combat.AttackCBFunc + var a1CB combat.AttackCBFunc if p["nobehind"] != 1 { - a1cb = c.makeA1CB() - } - var c4cb combat.AttackCBFunc - if c.Base.Cons >= 4 { - c.c4completed = false - c4cb = c.c4 + a1CB = c.makeA1CB() } + c1CB := c.makeC1CB() + c4CB := c.makeC4CB() + c.Core.QueueAttack( ai, combat.NewBoxHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: -1}, 2, 4), skillHitmark, skillHitmark, - a1cb, - c4cb, + a1CB, + c1CB, + c4CB, ) // Rosaria E is dynamic, so requires a second snapshot @@ -84,6 +83,8 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { 0, 0, c.particleCB, // Particles are emitted after the second hit lands + c1CB, + c4CB, ) }, skillHitmark+14) diff --git a/internal/characters/shenhe/cons.go b/internal/characters/shenhe/cons.go index 656b2fd76..eae7b271c 100644 --- a/internal/characters/shenhe/cons.go +++ b/internal/characters/shenhe/cons.go @@ -3,7 +3,6 @@ package shenhe import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/glog" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/modifier" @@ -23,7 +22,12 @@ func (c *char) c2(active *character.CharWrapper, dur int) { }) } -func (c *char) c4() { +// When characters under the effect of Icy Quill applied by Shenhe trigger its DMG Bonus effects, Shenhe will gain a Skyfrost Mantra stack: +// +// - When Shenhe uses Spring Spirit Summoning, she will consume all stacks of Skyfrost Mantra, increasing the DMG of that Spring Spirit Summoning by 5% for each stack consumed. +// +// - Max 50 stacks. Stacks last for 60s. +func (c *char) c4Init() { c.AddAttackMod(character.AttackMod{ Base: modifier.NewBase("shenhe-c4-dmg", -1), Amount: func(atk *combat.AttackEvent, _ combat.Target) ([]float64, bool) { @@ -39,25 +43,11 @@ func (c *char) c4() { return c.c4bonus, true }, }) - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if c.Core.Player.Active() != c.Index { - return false - } - if atk.Info.ActorIndex != c.Index { - return false - } - if atk.Info.AttackTag != combat.AttackTagElementalArt && atk.Info.AttackTag != combat.AttackTagElementalArtHold { - return false - } - c.DeleteStatus(c4BuffKey) - return false - }, "shenhe-c4-reset") } // C4 stacks are gained after the damage has been dealt and not before // https://library.keqingmains.com/evidence/characters/cryo/shenhe?q=shenhe#c4-insight -func (c *char) c4cb(a combat.AttackCB) { +func (c *char) c4CB(a combat.AttackCB) { //reset stacks to zero if all expired if !c.StatusIsActive(c4BuffKey) { c.c4count = 0 @@ -69,3 +59,15 @@ func (c *char) c4cb(a combat.AttackCB) { } c.AddStatus(c4BuffKey, 3600, true) // 60 s } + +func (c *char) makeC4ResetCB() combat.AttackCBFunc { + if c.Base.Cons < 4 { + return nil + } + return func(a combat.AttackCB) { + if c.Core.Player.Active() != c.Index { + return + } + c.DeleteStatus(c4BuffKey) + } +} diff --git a/internal/characters/shenhe/shenhe.go b/internal/characters/shenhe/shenhe.go index bf685495d..71751d9a5 100644 --- a/internal/characters/shenhe/shenhe.go +++ b/internal/characters/shenhe/shenhe.go @@ -58,7 +58,7 @@ func (c *char) Init() error { if c.Base.Cons >= 4 { c.c4bonus = make([]float64, attributes.EndStatType) - c.c4() + c.c4Init() } return nil } diff --git a/internal/characters/shenhe/skill.go b/internal/characters/shenhe/skill.go index 02cb52300..9571d0b1c 100644 --- a/internal/characters/shenhe/skill.go +++ b/internal/characters/shenhe/skill.go @@ -80,6 +80,7 @@ func (c *char) skillPress(p map[string]int) action.ActionInfo { skillPressHitmark, skillPressHitmark, c.makePressParticleCB(), + c.makeC4ResetCB(), ) if c.Base.Ascension >= 4 { @@ -128,6 +129,7 @@ func (c *char) skillHold(p map[string]int) action.ActionInfo { skillHoldHitmark, skillHoldHitmark, c.holdParticleCB, + c.makeC4ResetCB(), ) if c.Base.Ascension >= 4 { @@ -247,7 +249,7 @@ func (c *char) quillDamageMod() { atk.Info.FlatDmg += amt if c.Base.Cons >= 4 { - atk.Callbacks = append(atk.Callbacks, c.c4cb) + atk.Callbacks = append(atk.Callbacks, c.c4CB) } } diff --git a/internal/characters/xinyan/attack.go b/internal/characters/xinyan/attack.go index 46587539f..03f99d6b6 100644 --- a/internal/characters/xinyan/attack.go +++ b/internal/characters/xinyan/attack.go @@ -58,7 +58,13 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { attackHitboxes[c.NormalCounter][1], ) } - c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter]) + c.Core.QueueAttack( + ai, + ap, + attackHitmarks[c.NormalCounter], + attackHitmarks[c.NormalCounter], + c.makeC1CB(), + ) defer c.AdvanceNormalIndex() diff --git a/internal/characters/xinyan/burst.go b/internal/characters/xinyan/burst.go index 4587c12d4..eb24f7cd9 100644 --- a/internal/characters/xinyan/burst.go +++ b/internal/characters/xinyan/burst.go @@ -32,11 +32,13 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { Mult: burstDmg[c.TalentLvlBurst()], CanBeDefenseHalted: true, } + c1CB := c.makeC1CB() c.Core.QueueAttack( ai, combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 3), burstInitialHitmark, burstInitialHitmark, + c1CB, ) // 7 hits @@ -54,7 +56,13 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { } // 1st DoT c.QueueCharTask(func() { - c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 2}, 4), 0, 0) + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 2}, 4), + 0, + 0, + c1CB, + ) ai.CanBeDefenseHalted = false // only the first DoT has hitlag // 2nd DoT onwards c.QueueCharTask(func() { @@ -64,6 +72,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.Player(), combat.Point{Y: 2}, 4), i*17, i*17, + c1CB, ) } }, 17) diff --git a/internal/characters/xinyan/cons.go b/internal/characters/xinyan/cons.go index aad856533..5cebe51c6 100644 --- a/internal/characters/xinyan/cons.go +++ b/internal/characters/xinyan/cons.go @@ -3,7 +3,6 @@ package xinyan import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/enemy" "github.com/genshinsim/gcsim/pkg/modifier" @@ -11,38 +10,36 @@ import ( const c1ICDKey = "xinyan-c1-icd" -func (c *char) c1() { - c.c1Buff = make([]float64, attributes.EndStatType) - c.c1Buff[attributes.AtkSpd] = 0.12 - - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - crit := args[3].(bool) - if atk.Info.ActorIndex != c.Index { - return false +// Upon scoring a CRIT Hit, increases ATK SPD of Xinyan's Normal and Charged Attacks by 12% for 5s. +// Can only occur once every 5s. +func (c *char) makeC1CB() combat.AttackCBFunc { + if c.Base.Cons < 1 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return } - // doesn't work off-field - // https://youtu.be/ybE8g0A7hBk if c.Core.Player.Active() != c.Index { - return false + return } - if !crit { - return false + if !a.IsCrit { + return } if c.StatusIsActive(c1ICDKey) { - return false + return } + c.AddStatus(c1ICDKey, 5*60, true) + m := make([]float64, attributes.EndStatType) + m[attributes.AtkSpd] = 0.12 c.AddAttackMod(character.AttackMod{ Base: modifier.NewBaseWithHitlag("xinyan-c1", 5*60), Amount: func(atk *combat.AttackEvent, t combat.Target) ([]float64, bool) { - return c.c1Buff, true + return m, true }, }) - c.AddStatus(c1ICDKey, 300, true) - - return false - }, "xinyan-c1") + } } // Riff Revolution's Physical DMG has its CRIT Rate increased by 100%, and will form a shield at Shield Level 3: Rave when cast. @@ -62,20 +59,21 @@ func (c *char) c2() { } // Sweeping Fervor's swing DMG decreases opponent's Physical RES by 15% for 12s. -func (c *char) c4(a combat.AttackCB) { +func (c *char) makeC4CB() combat.AttackCBFunc { if c.Base.Cons < 4 { - return + return nil } - - e, ok := a.Target.(*enemy.Enemy) - if !ok { - return + return func(a combat.AttackCB) { + e, ok := a.Target.(*enemy.Enemy) + if !ok { + return + } + e.AddResistMod(combat.ResistMod{ + Base: modifier.NewBaseWithHitlag("xinyan-c4", 12*60), + Ele: attributes.Physical, + Value: -0.15, + }) } - e.AddResistMod(combat.ResistMod{ - Base: modifier.NewBaseWithHitlag("xinyan-c4", 12*60), - Ele: attributes.Physical, - Value: -0.15, - }) } // Decreases the Stamina Consumption of Xinyan's Charged Attacks by 30%. Additionally, Xinyan's Charged Attacks gain an ATK Bonus equal to 50% of her DEF. diff --git a/internal/characters/xinyan/skill.go b/internal/characters/xinyan/skill.go index cd16a0821..352838477 100644 --- a/internal/characters/xinyan/skill.go +++ b/internal/characters/xinyan/skill.go @@ -67,8 +67,9 @@ func (c *char) Skill(p map[string]int) action.ActionInfo { skillHitmark, skillHitmark, cb, - c.c4, c.particleCB, + c.makeC1CB(), + c.makeC4CB(), ) c.SetCDWithDelay(action.ActionSkill, 18*60, 13) @@ -115,7 +116,13 @@ func (c *char) shieldDot(src int) func() { Durability: 25, Mult: skillDot[c.TalentLvlSkill()], } - c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 3), 1, 1) + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 3), + 1, + 1, + c.makeC1CB(), + ) c.Core.Tasks.Add(c.shieldDot(src), 2*60) } diff --git a/internal/characters/xinyan/xinyan.go b/internal/characters/xinyan/xinyan.go index 23cd58f8d..5d6719bb4 100644 --- a/internal/characters/xinyan/xinyan.go +++ b/internal/characters/xinyan/xinyan.go @@ -13,7 +13,6 @@ type char struct { shieldLevel int shieldLevel2Requirement int shieldLevel3Requirement int - c1Buff []float64 c2Buff []float64 shieldTickSrc int } @@ -47,9 +46,6 @@ func (c *char) Init() error { c.a1() c.a4() - if c.Base.Cons >= 1 { - c.c1() - } if c.Base.Cons >= 2 { c.c2() } diff --git a/internal/characters/yanfei/asc.go b/internal/characters/yanfei/asc.go index 5e45200ca..1b4111d80 100644 --- a/internal/characters/yanfei/asc.go +++ b/internal/characters/yanfei/asc.go @@ -3,7 +3,6 @@ package yanfei import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/modifier" ) @@ -29,26 +28,26 @@ func (c *char) a1(stacks int) { // When Yanfei's Charged Attack deals a CRIT Hit to opponents, // she will deal an additional instance of AoE Pyro DMG equal to 80% of her ATK. // This DMG counts as Charged Attack DMG. -func (c *char) a4() { +func (c *char) makeA4CB() combat.AttackCBFunc { if c.Base.Ascension < 4 { - return + return nil } - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - crit := args[3].(bool) - trg := args[0].(combat.Target) - if atk.Info.ActorIndex != c.Index { - return false + done := false + return func(a combat.AttackCB) { + trg := a.Target + if trg.Type() != combat.TargettableEnemy { + return + } + if c.Core.Player.Active() != c.Index { + return } - if atk.Info.Abil == "Blazing Eye (A4)" { - return false + if !a.IsCrit { + return } - if atk.Info.AttackTag != combat.AttackTagExtra || !crit { - return false + if done { + return } - // make it so a4 only applies hitlag once per A4 proc and not everytime an enemy gets hit - defhalt := !c.a4HitlagApplied - c.a4HitlagApplied = true + done = true ai := combat.AttackInfo{ ActorIndex: c.Index, @@ -61,10 +60,8 @@ func (c *char) a4() { Durability: 25, Mult: 0.8, HitlagFactor: 0.05, - CanBeDefenseHalted: defhalt, + CanBeDefenseHalted: true, } c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(trg, nil, 3.5), 10, 10) - - return false - }, "yanfei-a4") + } } diff --git a/internal/characters/yanfei/charge.go b/internal/characters/yanfei/charge.go index d855a9c02..f341ca262 100644 --- a/internal/characters/yanfei/charge.go +++ b/internal/characters/yanfei/charge.go @@ -60,6 +60,7 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, radius), chargeHitmark-windup, chargeHitmark-windup, + c.makeA4CB(), ) c.Core.Log.NewEvent("yanfei charge attack consumed seals", glog.LogCharacterEvent, c.Index). @@ -71,9 +72,6 @@ func (c *char) ChargeAttack(p map[string]int) action.ActionInfo { c.DeleteStatus(sealBuffKey) }, 1) - // needed for a4 hitlag handling - c.a4HitlagApplied = false - return action.ActionInfo{ Frames: func(next action.Action) int { return chargeFrames[next] - windup }, AnimationLength: chargeFrames[action.InvalidAction] - windup, diff --git a/internal/characters/yanfei/yanfei.go b/internal/characters/yanfei/yanfei.go index a17f19bdb..d6825f5f7 100644 --- a/internal/characters/yanfei/yanfei.go +++ b/internal/characters/yanfei/yanfei.go @@ -22,7 +22,6 @@ type char struct { sealCount int burstBuff []float64 a1Buff []float64 - a4HitlagApplied bool } func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error { @@ -53,7 +52,6 @@ func (c *char) Init() error { c.a1Buff = make([]float64, attributes.EndStatType) c.burstBuff = make([]float64, attributes.EndStatType) c.burstBuff[attributes.DmgP] = burstBonus[c.TalentLvlBurst()] - c.a4() c.onExitField() if c.Base.Cons >= 2 { c.c2() diff --git a/internal/characters/yoimiya/aimed.go b/internal/characters/yoimiya/aimed.go index 3464c9699..32c76488f 100644 --- a/internal/characters/yoimiya/aimed.go +++ b/internal/characters/yoimiya/aimed.go @@ -77,6 +77,7 @@ func (c *char) Aimed(p map[string]int) action.ActionInfo { HitlagOnHeadshotOnly: true, IsDeployable: true, } + c2CB := c.makeC2CB() c.Core.QueueAttack( ai, combat.NewBoxHit( @@ -88,6 +89,7 @@ func (c *char) Aimed(p map[string]int) action.ActionInfo { ), aimedHitmarks[kindling], aimedHitmarks[kindling]+travel, + c2CB, ) // Kindling Arrows @@ -119,6 +121,7 @@ func (c *char) Aimed(p map[string]int) action.ActionInfo { ), aimedHitmarks[kindling], aimedHitmarks[kindling]+kindling_travel, + c2CB, ) } } diff --git a/internal/characters/yoimiya/asc.go b/internal/characters/yoimiya/asc.go index 3a0dd18d9..e87d6ba69 100644 --- a/internal/characters/yoimiya/asc.go +++ b/internal/characters/yoimiya/asc.go @@ -3,52 +3,47 @@ package yoimiya import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/combat" - "github.com/genshinsim/gcsim/pkg/core/event" "github.com/genshinsim/gcsim/pkg/core/player/character" "github.com/genshinsim/gcsim/pkg/modifier" ) -const a1Key = "yoimiyaa1" +const a1Key = "yoimiya-a1" // During Niwabi Fire-Dance, shots from Yoimiya's Normal Attack will increase // her Pyro DMG Bonus by 2% on hit. This effect lasts for 3s and can have a // maximum of 10 stacks. -func (c *char) a1() { +func (c *char) makeA1CB() combat.AttackCBFunc { if c.Base.Ascension < 1 { - return + return nil } - // TODO: change this to add mod on each hit instead - c.AddStatMod(character.StatMod{ - Base: modifier.NewBase("yoimiya-a1", -1), - AffectedStat: attributes.PyroP, - Amount: func() ([]float64, bool) { - if c.StatusIsActive(a1Key) { - c.a1Bonus[attributes.PyroP] = float64(c.a1Stack) * 0.02 - return c.a1Bonus, true - } - c.a1Stack = 0 - return nil, false - }, - }) - - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - if atk.Info.ActorIndex != c.Index { - return false + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if c.Core.Player.Active() != c.Index { + return } if !c.StatusIsActive(skillKey) { - return false + return } - if atk.Info.AttackTag != combat.AttackTagNormal { - return false + + if !c.StatusIsActive(a1Key) { + c.a1Stacks = 0 } - // here we can add stacks up to 10 - if c.a1Stack < 10 { - c.a1Stack++ + if c.a1Stacks < 10 { + c.a1Stacks++ } - c.AddStatus(a1Key, 180, true) - return false - }, "yoimiya-a1") + + m := make([]float64, attributes.EndStatType) + c.AddStatMod(character.StatMod{ + Base: modifier.NewBase(a1Key, 3*60), + AffectedStat: attributes.PyroP, + Amount: func() ([]float64, bool) { + m[attributes.PyroP] = float64(c.a1Stacks) * 0.02 + return m, true + }, + }) + } } // Using Ryuukin Saxifrage causes nearby party members (not including Yoimiya) @@ -57,7 +52,7 @@ func (c *char) a1() { // possesses when using Ryuukin Saxifrage. Each stack increases this ATK Bonus // by 1%. func (c *char) a4() { - c.a4Bonus[attributes.ATKP] = 0.1 + float64(c.a1Stack)*0.01 + c.a4Bonus[attributes.ATKP] = 0.1 + float64(c.a1Stacks)*0.01 for _, x := range c.Core.Player.Chars() { if x.Index == c.Index { continue diff --git a/internal/characters/yoimiya/attack.go b/internal/characters/yoimiya/attack.go index fca909787..68292e7c3 100644 --- a/internal/characters/yoimiya/attack.go +++ b/internal/characters/yoimiya/attack.go @@ -51,6 +51,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { if c.StatusIsActive(skillKey) { particleCB = c.particleCB } + a1CB := c.makeA1CB() + c2CB := c.makeC2CB() var totalMV float64 for i, mult := range attack[c.NormalCounter] { @@ -68,6 +70,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { attackHitmarks[c.NormalCounter][i], attackHitmarks[c.NormalCounter][i]+travel, particleCB, + a1CB, + c2CB, ) } @@ -96,6 +100,8 @@ func (c *char) Attack(p map[string]int) action.ActionInfo { ), 0, attackHitmarks[c.NormalCounter][0]+travel+5, + a1CB, + c2CB, ) } diff --git a/internal/characters/yoimiya/burst.go b/internal/characters/yoimiya/burst.go index 6efccd279..54fbe2057 100644 --- a/internal/characters/yoimiya/burst.go +++ b/internal/characters/yoimiya/burst.go @@ -47,6 +47,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo { 0, burstHitmark, c.applyAB, // callback to apply Aurous Blaze + c.makeC2CB(), ) //add cooldown to sim @@ -132,7 +133,7 @@ func (c *char) burstHook() { Durability: 25, Mult: burstExplode[c.TalentLvlBurst()], } - c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(trg, nil, 3), 0, 1) + c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(trg, nil, 3), 0, 1, c.makeC2CB()) trg.AddStatus(abIcdKey, 120, true) // trigger Aurous Blaze ICD diff --git a/internal/characters/yoimiya/cons.go b/internal/characters/yoimiya/cons.go index 4df18c1ba..012de0df9 100644 --- a/internal/characters/yoimiya/cons.go +++ b/internal/characters/yoimiya/cons.go @@ -35,17 +35,25 @@ func (c *char) c1() { }, "yoimiya-c1") } -func (c *char) c2() { - m := make([]float64, attributes.EndStatType) - m[attributes.PyroP] = 0.25 - c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { - atk := args[1].(*combat.AttackEvent) - crit := args[3].(bool) - - if atk.Info.ActorIndex != c.Index || !crit { - return false +// When Yoimiya's Pyro DMG scores a CRIT Hit, Yoimiya will gain a 25% Pyro DMG Bonus for 6s. +// This effect can be triggered even when Yoimiya is not the active character. +func (c *char) makeC2CB() combat.AttackCBFunc { + if c.Base.Cons < 2 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != combat.TargettableEnemy { + return + } + if !a.IsCrit { + return + } + if a.AttackEvent.Info.Element != attributes.Pyro { + return } + m := make([]float64, attributes.EndStatType) + m[attributes.PyroP] = 0.25 c.AddStatMod(character.StatMod{ Base: modifier.NewBase("yoimiya-c2", 360), AffectedStat: attributes.PyroP, @@ -53,7 +61,5 @@ func (c *char) c2() { return m, true }, }) - - return false - }, "yoimiya-c2") + } } diff --git a/internal/characters/yoimiya/skill.go b/internal/characters/yoimiya/skill.go index fff69a423..af221fd80 100644 --- a/internal/characters/yoimiya/skill.go +++ b/internal/characters/yoimiya/skill.go @@ -28,7 +28,7 @@ func init() { func (c *char) Skill(p map[string]int) action.ActionInfo { c.AddStatus(skillKey, 600+skillStart, true) // activate for 10 if !c.StatusIsActive(a1Key) { - c.a1Stack = 0 + c.a1Stacks = 0 } c.SetCDWithDelay(action.ActionSkill, 1080, 11) diff --git a/internal/characters/yoimiya/yoimiya.go b/internal/characters/yoimiya/yoimiya.go index 90285a252..b5eb8379a 100644 --- a/internal/characters/yoimiya/yoimiya.go +++ b/internal/characters/yoimiya/yoimiya.go @@ -17,8 +17,7 @@ func init() { type char struct { *tmpl.Character - a1Stack int - a1Bonus []float64 + a1Stacks int a4Bonus []float64 abApplied bool } @@ -38,17 +37,12 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) } func (c *char) Init() error { - c.a1Bonus = make([]float64, attributes.EndStatType) c.a4Bonus = make([]float64, attributes.EndStatType) - c.a1() c.onExit() c.burstHook() if c.Base.Cons >= 1 { c.c1() } - if c.Base.Cons >= 2 { - c.c2() - } return nil }