Skip to content

Commit

Permalink
Sethos dev (#2241)
Browse files Browse the repository at this point in the history
* Initial sethos impl

* Updated sethos Q NAs to name to Dusk Bolt

* update frames

* add docs

* Adjusted sethos attack frames

---------

Co-authored-by: imring <[email protected]>
  • Loading branch information
Charlie-Zheng and imring authored Oct 14, 2024
1 parent d6444c1 commit 9644ddb
Show file tree
Hide file tree
Showing 29 changed files with 1,543 additions and 11 deletions.
191 changes: 191 additions & 0 deletions internal/characters/sethos/aimed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package sethos

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"
"github.com/genshinsim/gcsim/pkg/core/glog"
)

var aimedFrames [][]int

var aimedHitmarks = []int{16, 74, 368}
var startCharge = aimedHitmarks[0]

const shadowPierceShotAil = "Shadowpiercing Shot"

func init() {
// outside of E status
aimedFrames = make([][]int, 3)

// Aimed Shot
aimedFrames[0] = frames.InitAbilSlice(26)
aimedFrames[0][action.ActionDash] = aimedHitmarks[0]
aimedFrames[0][action.ActionJump] = aimedHitmarks[0]

// Fully-Charged Aimed Shot
aimedFrames[1] = frames.InitAbilSlice(83)
aimedFrames[1][action.ActionDash] = aimedHitmarks[1]
aimedFrames[1][action.ActionJump] = aimedHitmarks[1]

// Shadowpiercing Shot
aimedFrames[2] = frames.InitAbilSlice(379)
aimedFrames[2][action.ActionDash] = aimedHitmarks[2]
aimedFrames[2][action.ActionJump] = aimedHitmarks[2]
}

func (c *char) Aimed(p map[string]int) (action.Info, error) {
if c.StatusIsActive(burstBuffKey) {
return action.Info{}, fmt.Errorf("%v: Cannot aim while in burst", c.Base.Key)
}

hold, ok := p["hold"]
if !ok {
// is this a good default? it's gonna take 6s to do without energy
hold = attacks.AimParamLv2
}
switch hold {
case attacks.AimParamPhys:
case attacks.AimParamLv1:
case attacks.AimParamLv2:
return c.ShadowPierce(p)
default:
return action.Info{}, fmt.Errorf("invalid hold param supplied, got %v", hold)
}

skip, energy := c.a1Calc()
if skip > aimedHitmarks[hold]-startCharge {
skip = aimedHitmarks[hold] - startCharge
}

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

c.QueueCharTask(func() {
ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: "Fully-Charged Aimed Shot",
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypePierce,
Element: attributes.Electro,
Durability: 25,
Mult: fullaim[c.TalentLvlAttack()],
HitWeakPoint: weakspot == 1,
HitlagHaltFrames: 0.12 * 60,
HitlagFactor: 0.01,
HitlagOnHeadshotOnly: true,
IsDeployable: true,
}
if hold < attacks.AimParamLv1 {
ai.Abil = "Aimed Shot"
ai.Element = attributes.Physical
ai.Mult = aim[c.TalentLvlAttack()]
}

c.Core.QueueAttack(
ai,
combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -0.5},
0.1,
1,
),
0,
travel,
)
c.a1Consume(energy, hold)
}, aimedHitmarks[hold]-skip)

return action.Info{
Frames: func(next action.Action) int { return aimedFrames[hold][next] - skip },
AnimationLength: aimedFrames[hold][action.InvalidAction] - skip,
CanQueueAfter: aimedHitmarks[hold] - skip,
State: action.AimState,
}, nil
}

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

skip, energy := c.a1Calc()
if skip > aimedHitmarks[2]-startCharge {
skip = aimedHitmarks[2] - startCharge
}
hitHaltFrames := 0.0
if weakspot == 1 {
hitHaltFrames = 0.12 * 60
}

c.QueueCharTask(func() {
em := c.Stat(attributes.EM)
ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: shadowPierceShotAil,
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNone,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypeDefault,
Element: attributes.Electro,
Durability: 50,
Mult: shadowpierceAtk[c.TalentLvlAttack()],
HitWeakPoint: weakspot == 1,
HitlagHaltFrames: hitHaltFrames,
HitlagFactor: 0.01,
HitlagOnHeadshotOnly: true,
IsDeployable: true,
FlatDmg: shadowpierceEM[c.TalentLvlAttack()] * em,
}

if c.StatusIsActive(a4Key) {
ai.FlatDmg += 7 * em
c.Core.Log.NewEvent("Sethos A4 proc dmg add", glog.LogPreDamageMod, c.Index).
Write("em", em).
Write("ratio", 7.0).
Write("addition", 7*em)
}

deltaPos := c.Core.Combat.Player().Pos().Sub(c.Core.Combat.PrimaryTarget().Pos())
dist := deltaPos.Magnitude()

// simulate piercing. Extends 15 units from player
ap := combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -dist},
0.1,
15,
)
c.a1Consume(energy, attacks.AimParamLv2)
c.Core.QueueAttack(
ai,
ap,
0,
travel,
c.makeA4cb(),
c.makeC4cb(),
c.makeC6cb(energy),
)
}, aimedHitmarks[2]-skip)

return action.Info{
Frames: func(next action.Action) int { return aimedFrames[2][next] - skip },
AnimationLength: aimedFrames[2][action.InvalidAction] - skip,
CanQueueAfter: aimedHitmarks[2] - skip,
State: action.AimState,
}, nil
}
81 changes: 81 additions & 0 deletions internal/characters/sethos/asc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package sethos

import (
"github.com/genshinsim/gcsim/pkg/core/attacks"
"github.com/genshinsim/gcsim/pkg/core/combat"
"github.com/genshinsim/gcsim/pkg/core/targets"
)

const a1Key = "sethos-a1"

// returns the amount of time to save, and the amount of energy considered
func (c *char) a1Calc() (int, float64) {
if c.Base.Ascension < 1 {
return 0, 0
}
energy := min(c.Energy, 20)
// floor or round the skip?
return int(0.285 * energy * 60), energy
}

func (c *char) a1Consume(energy float64, holdLevel int) {
switch holdLevel {
default:
return
case attacks.AimParamLv1:
c.AddEnergy(a1Key, -energy*0.5)
case attacks.AimParamLv2:
c.AddEnergy(a1Key, -energy)
}
c.c2AddStack(c2ConsumingKey)
}

const a4Key = "sethos-a4"
const a4IcdKey = "sethos-a4-icd"
const a4Icd = 15 * 60

func (c *char) a4() {
if c.Base.Ascension < 4 {
return
}
// buff stays active until a4 is proc'd
c.AddStatus(a4Key, -1, true)
c.a4Count = 0
}

func (c *char) makeA4cb() combat.AttackCBFunc {
if c.Base.Ascension < 4 {
return nil
}

done := false
return func(a combat.AttackCB) {
if a.Target.Type() != targets.TargettableEnemy {
return
}
if done {
return
}
if !c.StatusIsActive(a4Key) {
return
}
done = true
if c.a4Count == 0 {
// overwrite the expiry of the a4 buff to be 5s after
c.AddStatus(a4Key, 5*60, true)
c.startA4Icd()
}
c.a4Count += 1
if c.a4Count >= 4 {
c.DeleteStatus(a4Key)
}
}
}

func (c *char) startA4Icd() {
if c.StatusIsActive(a4IcdKey) {
return
}
c.AddStatus(a4IcdKey, a4Icd, true)
c.QueueCharTask(c.a4, a4Icd)
}
103 changes: 103 additions & 0 deletions internal/characters/sethos/attack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package sethos

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"
)

const normalHitNum = 3

var (
attackFrames [][]int
attackHitmarks = [][]int{{10}, {12, 12 + 3}, {39}}
)

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

attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 19) // N1 -> Walk
attackFrames[0][action.ActionAttack] = 17
attackFrames[0][action.ActionAim] = 15

attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1][1], 52-17) // N2 -> Walk
attackFrames[1][action.ActionAttack] = 32
attackFrames[1][action.ActionAim] = 45 - 17

attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2][0], 118-32-17) // N3 -> Walk
attackFrames[2][action.ActionAttack] = 63
}

func (c *char) Attack(p map[string]int) (action.Info, error) {
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,
}

ap := combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -0.5},
0.1,
1,
)

for i, mult := range attack[c.NormalCounter] {
c.QueueCharTask(func() {
var c4cb combat.AttackCBFunc
if c.StatusIsActive(burstBuffKey) {
ai.Abil = fmt.Sprintf("Dusk Bolt %v", c.NormalCounter)
ai.AttackTag = attacks.AttackTagExtra
ai.ICDTag = attacks.ICDTagElementalBurst
ai.Element = attributes.Electro
ai.FlatDmg += burstEM[c.TalentLvlBurst()] * c.Stat(attributes.EM)

deltaPos := c.Core.Combat.Player().Pos().Sub(c.Core.Combat.PrimaryTarget().Pos())
dist := deltaPos.Magnitude()

// simulate piercing. Extends 15 units from player
ap = combat.NewBoxHit(
c.Core.Combat.Player(),
c.Core.Combat.PrimaryTarget(),
geometry.Point{Y: -dist},
0.1,
15,
)
c4cb = c.makeC4cb()
}
ai.Mult = mult[c.TalentLvlAttack()]
c.Core.QueueAttack(
ai,
ap,
0,
travel,
c4cb,
)
}, attackHitmarks[c.NormalCounter][i])
}

defer c.AdvanceNormalIndex()

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

0 comments on commit 9644ddb

Please sign in to comment.