forked from genshinsim/gcsim
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
1,676 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.