Skip to content

Commit

Permalink
add lyney (genshinsim#1626)
Browse files Browse the repository at this point in the history
  • Loading branch information
k0l11 authored Aug 26, 2023
1 parent 9d039b4 commit d0e14cd
Show file tree
Hide file tree
Showing 26 changed files with 1,676 additions and 0 deletions.
233 changes: 233 additions & 0 deletions internal/characters/lyney/aimed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package lyney

import (
"github.com/genshinsim/gcsim/internal/frames"
"github.com/genshinsim/gcsim/pkg/core/action"
"github.com/genshinsim/gcsim/pkg/core/attacks"
"github.com/genshinsim/gcsim/pkg/core/attributes"
"github.com/genshinsim/gcsim/pkg/core/combat"
"github.com/genshinsim/gcsim/pkg/core/geometry"
"github.com/genshinsim/gcsim/pkg/core/glog"
"github.com/genshinsim/gcsim/pkg/core/player"
)

var (
aimedFrames []int
aimedPropFrames []int
)

const (
aimedRelease = 72
aimedPropRelease = 103

skillAlignedICDKey = "lyney-aligned-icd"
skillAlignedICD = 6 * 60

grinMalkinHatKey = "lyney-grinmalkinhat"
grinMalkinHatAimedDuration = 238
grinMalkinHatBurstDuration = 245

propSurplusHPDrainThreshold = 0.6
propSurplusHPDrainRatio = 0.2
)

func init() {
aimedFrames = frames.InitAbilSlice(80)
aimedFrames[action.ActionDash] = aimedRelease
aimedFrames[action.ActionJump] = aimedRelease

aimedPropFrames = frames.InitAbilSlice(111)
aimedPropFrames[action.ActionDash] = aimedPropRelease
aimedPropFrames[action.ActionJump] = aimedPropRelease
}

func (c *char) Aimed(p map[string]int) action.ActionInfo {
level, ok := p["level"]
if !ok {
level = 1
}
if level == 1 {
return c.PropAimed(p)
}

travel, ok := p["travel"]
if !ok {
travel = 10
}
weakspot := p["weakspot"]

ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: "Aim (Charged)",
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypePierce,
Element: attributes.Pyro,
Durability: 25,
Mult: fullaim[c.TalentLvlAttack()],
HitWeakPoint: weakspot == 1,
HitlagHaltFrames: 0.12 * 60,
HitlagFactor: 0.01,
HitlagOnHeadshotOnly: true,
IsDeployable: true,
}

c.Core.QueueAttack(
ai,
combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -0.5},
0.1,
1,
),
aimedRelease,
aimedRelease+travel,
c.makeC4CB(),
)

return action.ActionInfo{
Frames: frames.NewAbilFunc(aimedFrames),
AnimationLength: aimedFrames[action.InvalidAction],
CanQueueAfter: aimedRelease,
State: action.AimState,
}
}

func (c *char) PropAimed(p map[string]int) action.ActionInfo {
travel, ok := p["travel"]
if !ok {
travel = 10
}
c6Travel, ok := p["c6_travel"]
if !ok {
c6Travel = 10
}
weakspot := p["weakspot"]

propAI := combat.AttackInfo{
ActorIndex: c.Index,
Abil: "Prop Arrow",
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypePierce,
Element: attributes.Pyro,
Durability: 25,
Mult: prop[c.TalentLvlAttack()],
HitWeakPoint: weakspot == 1,
HitlagHaltFrames: 0.12 * 60,
HitlagFactor: 0.01,
HitlagOnHeadshotOnly: true,
IsDeployable: true,
}
c.QueueCharTask(func() {

c.c6(c6Travel)
target := c.Core.Combat.PrimaryTarget()
c.Core.QueueAttack(
propAI,
combat.NewBoxHit(
c.Core.Combat.Player(),
target,
geometry.Point{Y: -0.5},
0.1,
1,
),
0,
travel,
c.makeC4CB(),
)

// hp drain should happen right after prop arrow snapshot to avoid getting the newly gained mh stack on it
// https://youtu.be/QblKD2-9WNE?si=xcd4NAl2Wq-46fQI
hpDrained := c.propSurplus()

c.QueueCharTask(c.makeGrinMalkinHat(target.Pos(), hpDrained), travel)
c.QueueCharTask(c.skillAligned(target.Pos()), travel)
}, aimedPropRelease)

return action.ActionInfo{
Frames: frames.NewAbilFunc(aimedPropFrames),
AnimationLength: aimedPropFrames[action.InvalidAction],
CanQueueAfter: aimedPropRelease,
State: action.AimState,
}
}

// not implemented: The effect will be removed after the character spends 30s out of combat.
func (c *char) propSurplus() bool {
// When firing the Prop Arrow, and when Lyney has more than 60% HP,
// he will consume a portion of his HP to obtain 1 Prop Surplus stack.
if c.CurrentHPRatio() <= propSurplusHPDrainThreshold {
return false
}

currentHP := c.CurrentHP()
maxHP := c.MaxHP()
hpdrain := propSurplusHPDrainRatio * maxHP
// The lowest Lyney can drop to through this method is 60% of his Max HP.
if (currentHP-hpdrain)/maxHP <= propSurplusHPDrainThreshold {
hpdrain = currentHP - propSurplusHPDrainThreshold*maxHP
}
c.Core.Player.Drain(player.DrainInfo{
ActorIndex: c.Index,
Abil: "Prop Surplus",
Amount: hpdrain,
})

c.increasePropSurplusStacks(1 + c.c1StackIncrease())
return true
}

func (c *char) increasePropSurplusStacks(increase int) {
c.propSurplusStacks += increase
if c.propSurplusStacks > 5 {
c.propSurplusStacks = 5
}
c.Core.Log.NewEvent("Lyney Prop Surplus stack added", glog.LogCharacterEvent, c.Index).Write("prop_surplus_stacks", c.propSurplusStacks)
}

func (c *char) skillAligned(pos geometry.Point) func() {
return func() {
if c.StatusIsActive(skillAlignedICDKey) {
return
}
c.AddStatus(skillAlignedICDKey, skillAlignedICD, true)

propAlignedAI := combat.AttackInfo{
ActorIndex: c.Index,
Abil: "Spiritbreath Thorn (" + c.Base.Key.Pretty() + ")",
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypePierce,
Element: attributes.Pyro,
Durability: 0,
Mult: propAligned[c.TalentLvlAttack()],
}
c.Core.QueueAttack(
propAlignedAI,
combat.NewCircleHitOnTarget(pos, nil, 2),
42,
42,
c.makeC4CB(),
)
}
}

func (c *char) makeGrinMalkinHat(pos geometry.Point, hpDrained bool) func() {
return func() {
hatIncrease := 1 + c.c1HatIncrease()
for i := 0; i < hatIncrease; i++ {
// kill existing hat if reached limit
if len(c.hats) == c.maxHatCount {
c.hats[0].Kill()
}
g := c.newGrinMalkinHat(pos, hpDrained, grinMalkinHatAimedDuration)
c.hats = append(c.hats, g)
c.Core.Combat.AddGadget(g)
}
}
}
77 changes: 77 additions & 0 deletions internal/characters/lyney/asc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package lyney

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/player/character"
"github.com/genshinsim/gcsim/pkg/core/targets"
"github.com/genshinsim/gcsim/pkg/modifier"
)

// If Lyney consumes HP when firing off a Prop Arrow,
// the Grin-Malkin hat summoned by the arrow will, upon hitting an opponent,
// restore 3 Energy to Lyney and increase DMG dealt by 80% of his ATK.
func (c *char) makeA1CB(hpDrained bool) combat.AttackCBFunc {
if c.Base.Ascension < 1 || !hpDrained {
return nil
}
done := false
return func(a combat.AttackCB) {
if a.Target.Type() != targets.TargettableEnemy {
return
}
if done {
return
}
done = true
c.AddEnergy("lynette-a1", 3)
}
}

func (c *char) addA1(ai *combat.AttackInfo, hpDrained bool) {
if c.Base.Ascension < 1 || !hpDrained {
return
}
ai.Mult += 0.8
}

// The DMG Lyney deals to opponents affected by Pyro will receive the following buffs:
// - Increases the DMG dealt by 60%.
// - Each Pyro party member other than Lyney will cause the DMG dealt to increase by an additional 20%.
// Lyney can deal up to 100% increased DMG to opponents affected by Pyro in this way.
func (c *char) a4() {
if c.Base.Ascension < 4 {
return
}

// count up all pyro chars in team
pyroCount := 0
for _, char := range c.Core.Player.Chars() {
if char.Base.Element == attributes.Pyro {
pyroCount++
}
}

// calc a4 dmg% value
a4Dmg := 0.6 + float64(pyroCount-1)*0.2
if a4Dmg > 1 {
a4Dmg = 1
}

m := make([]float64, attributes.EndStatType)
m[attributes.DmgP] = a4Dmg
c.AddAttackMod(character.AttackMod{
Base: modifier.NewBase("lyney-a4", -1),
Amount: func(atk *combat.AttackEvent, t combat.Target) ([]float64, bool) {
r, ok := t.(core.Reactable)
if !ok {
return nil, false
}
if !r.AuraContains(attributes.Pyro) {
return nil, false
}
return m, true
},
})
}
82 changes: 82 additions & 0 deletions internal/characters/lyney/attack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package lyney

import (
"fmt"

"github.com/genshinsim/gcsim/internal/frames"
"github.com/genshinsim/gcsim/pkg/core/action"
"github.com/genshinsim/gcsim/pkg/core/attacks"
"github.com/genshinsim/gcsim/pkg/core/attributes"
"github.com/genshinsim/gcsim/pkg/core/combat"
"github.com/genshinsim/gcsim/pkg/core/geometry"
)

var (
attackFrames [][]int
attackReleases = [][]int{{17}, {12}, {20, 29}, {29}}
)

const normalHitNum = 4

func init() {
attackFrames = make([][]int, normalHitNum)

attackFrames[0] = frames.InitNormalCancelSlice(attackReleases[0][0], 32) // N1 -> Walk
attackFrames[0][action.ActionAttack] = 22
attackFrames[0][action.ActionAim] = 22

attackFrames[1] = frames.InitNormalCancelSlice(attackReleases[1][0], 34) // N2 -> CA
attackFrames[1][action.ActionAttack] = 24
attackFrames[1][action.ActionWalk] = 31

attackFrames[2] = frames.InitNormalCancelSlice(attackReleases[2][1], 86) // N3 -> Walk
attackFrames[2][action.ActionAttack] = 39
attackFrames[2][action.ActionAim] = 81

attackFrames[3] = frames.InitNormalCancelSlice(attackReleases[3][0], 66) // N4 -> Walk
attackFrames[3][action.ActionAttack] = 59
attackFrames[3][action.ActionAim] = 500 // TODO: this action is illegal; need better way to handle it
}

func (c *char) Attack(p map[string]int) action.ActionInfo {
travel, ok := p["travel"]
if !ok {
travel = 10
}

ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: fmt.Sprintf("Normal %v", c.NormalCounter),
AttackTag: attacks.AttackTagNormal,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypePierce,
Element: attributes.Physical,
Durability: 25,
}

for i, mult := range attack[c.NormalCounter] {
ai.Mult = mult[c.TalentLvlAttack()]
c.Core.QueueAttack(
ai,
combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -0.5},
0.1,
1,
),
attackReleases[c.NormalCounter][i],
attackReleases[c.NormalCounter][i]+travel,
)
}

defer c.AdvanceNormalIndex()

return action.ActionInfo{
Frames: frames.NewAttackFunc(c.Character, attackFrames),
AnimationLength: attackFrames[c.NormalCounter][action.InvalidAction],
CanQueueAfter: attackReleases[c.NormalCounter][len(attackReleases[c.NormalCounter])-1],
State: action.NormalAttackState,
}
}
Loading

0 comments on commit d0e14cd

Please sign in to comment.