Skip to content

Commit

Permalink
Implement Hydro MC (genshinsim#1640)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: srliao <[email protected]>
  • Loading branch information
imring and srliao authored Sep 29, 2023
1 parent 779cb2d commit e9058aa
Show file tree
Hide file tree
Showing 28 changed files with 1,507 additions and 7 deletions.
99 changes: 99 additions & 0 deletions internal/characters/traveler/common/hydro/asc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package hydro

import (
"github.com/genshinsim/gcsim/internal/common"
"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/player"
"github.com/genshinsim/gcsim/pkg/core/targets"
)

const a1ICDKey = "sourcewater-droplet-icd"

// After the Dewdrop fired by the Hold Mode of the Aquacrest Saber hits an opponent, a Sourcewater Droplet will be
// generated near to the Traveler. If the Traveler picks it up, they will restore 7% HP.
// 1 Droplet can be created this way every second, and each use of Aquacrest Saber can create 4 Droplets at most.
func (c *char) makeA1CB() combat.AttackCBFunc {
if c.Base.Ascension < 1 {
return nil
}
count := 0
return func(a combat.AttackCB) {
if count >= 4 {
return
}
if a.Target.Type() != targets.TargettableEnemy {
return
}
if c.StatusIsActive(a1ICDKey) {
return
}

count++
droplet := c.newDropblet()
c.Core.Combat.AddGadget(droplet)
c.droplets = append(c.droplets, droplet)
c.AddStatus(a1ICDKey, 60, true)
}
}

func (c *char) a1PickUp(count int) {
for _, g := range c.Core.Combat.Gadgets() {
if count == 0 {
return
}

droplet, ok := g.(*common.SourcewaterDroplet)
if !ok {
continue
}
droplet.Kill()
count--

c.Core.Player.Heal(player.HealInfo{
Caller: c.Index,
Target: c.Index,
Message: "Spotless Waters",
Src: c.MaxHP() * 0.07,
Bonus: c.Stat(attributes.Heal),
})

// Picking up a Sourcewater Droplet will restore 2 Energy to the Traveler.
// Requires the Passive Talent "Spotless Waters."
if c.Base.Cons >= 1 {
c.AddEnergy("travelerhydro-c1", 2)
}

if c.Base.Cons >= 6 {
c.c6()
}
}
}

func (c *char) newDropblet() *common.SourcewaterDroplet {
player := c.Core.Combat.Player()
pos := geometry.CalcRandomPointFromCenter(
geometry.CalcOffsetPoint(
player.Pos(),
geometry.Point{Y: 3.5},
player.Direction(),
),
0.3,
3,
c.Core.Rand,
)

droplet := common.NewSourcewaterDroplet(c.Core, pos)
remove := func() {
for i, g := range c.droplets {
if g.Key() == droplet.Key() {
c.droplets = append(c.droplets[:i], c.droplets[i+1:]...) // delete from the array
break
}
}
}
droplet.OnExpiry = remove
droplet.OnKill = remove
return droplet
}
110 changes: 110 additions & 0 deletions internal/characters/traveler/common/hydro/attack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package hydro

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
attackHitmarks = [][]int{{13, 13, 16, 30, 25}, {16, 10, 19, 23, 14}}
attackHitlagHaltFrame = [][]float64{{0.03, 0.03, 0.06, 0.09, 0.12}, {0.03, 0.03, 0.06, 0.06, 0.10}}
attackHitboxes = [][][]float64{{{1.4, 2.2}, {1.7}, {1.5, 2.2}, {1.7}, {1.75}}, {{1.6}, {1.4, 2.2}, {1.5}, {1.5}, {1.6}}}
attackOffsets = [][]float64{{0, 0.6, 0.4, 0.6, 0.6}, {1, 0, 0.7, 0.7, 1}}
attackFanAngles = [][]float64{{360, 180, 360, 360, 240}, {360, 360, 360, 360, 360}}
)

const normalHitNum = 5

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

// Male
attackFrames[0] = make([][]int, normalHitNum)

attackFrames[0][0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 28) // N1 -> CA
attackFrames[0][0][action.ActionAttack] = 17 // N1 -> N2

attackFrames[0][1] = frames.InitNormalCancelSlice(attackHitmarks[0][1], 28) // N2 -> CA
attackFrames[0][1][action.ActionAttack] = 26 // N2 -> N3

attackFrames[0][2] = frames.InitNormalCancelSlice(attackHitmarks[0][2], 36) // N3 -> CA
attackFrames[0][2][action.ActionAttack] = 32 // N3 -> N4

attackFrames[0][3] = frames.InitNormalCancelSlice(attackHitmarks[0][3], 45) // N4 -> CA
attackFrames[0][3][action.ActionAttack] = 39 // N4 -> N5

attackFrames[0][4] = frames.InitNormalCancelSlice(attackHitmarks[0][4], 69) // N5 -> N1
attackFrames[0][4][action.ActionCharge] = 500 // N5 -> CA, TODO: this action is illegal; need better way to handle it

// Female
attackFrames[1] = make([][]int, normalHitNum)

attackFrames[1][0] = frames.InitNormalCancelSlice(attackHitmarks[1][0], 32) // N1 -> CA
attackFrames[1][0][action.ActionAttack] = 24 // N1 -> N2

attackFrames[1][1] = frames.InitNormalCancelSlice(attackHitmarks[1][1], 23) // N2 -> CA
attackFrames[1][1][action.ActionAttack] = 21 // N2 -> N3

attackFrames[1][2] = frames.InitNormalCancelSlice(attackHitmarks[1][2], 39) // N3 -> CA
attackFrames[1][2][action.ActionAttack] = 27 // N3 -> N4

attackFrames[1][3] = frames.InitNormalCancelSlice(attackHitmarks[1][3], 45) // N4 -> CA
attackFrames[1][3][action.ActionAttack] = 38 // N4 -> N5

attackFrames[1][4] = frames.InitNormalCancelSlice(attackHitmarks[1][4], 64) // N5 -> N1
attackFrames[1][4][action.ActionCharge] = 500 // N5 -> CA, TODO: this action is illegal; need better way to handle it
}

func (c *char) Attack(p map[string]int) (action.Info, error) {
ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: fmt.Sprintf("Normal %v", c.NormalCounter),
AttackTag: attacks.AttackTagNormal,
ICDTag: attacks.ICDTagNormalAttack,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypeSlash,
Element: attributes.Physical,
Durability: 25,
Mult: attack[c.NormalCounter][c.TalentLvlAttack()],
HitlagFactor: 0.01,
HitlagHaltFrames: attackHitlagHaltFrame[c.gender][c.NormalCounter] * 60,
CanBeDefenseHalted: true,
}
ap := combat.NewCircleHitOnTargetFanAngle(
c.Core.Combat.Player(),
geometry.Point{Y: attackOffsets[c.gender][c.NormalCounter]},
attackHitboxes[c.gender][c.NormalCounter][0],
attackFanAngles[c.gender][c.NormalCounter],
)
if (c.gender == 0 && (c.NormalCounter == 0 || c.NormalCounter == 2)) ||
(c.gender == 1 && c.NormalCounter == 1) {
ap = combat.NewBoxHitOnTarget(
c.Core.Combat.Player(),
geometry.Point{Y: attackOffsets[c.gender][c.NormalCounter]},
attackHitboxes[c.gender][c.NormalCounter][0],
attackHitboxes[c.gender][c.NormalCounter][1],
)
}
c.Core.QueueAttack(
ai,
ap,
attackHitmarks[c.gender][c.NormalCounter],
attackHitmarks[c.gender][c.NormalCounter],
)

defer c.AdvanceNormalIndex()

return action.Info{
Frames: frames.NewAttackFunc(c.Character, attackFrames[c.gender]),
AnimationLength: attackFrames[c.gender][c.NormalCounter][action.InvalidAction],
CanQueueAfter: attackHitmarks[c.gender][c.NormalCounter],
State: action.NormalAttackState,
}, nil
}
81 changes: 81 additions & 0 deletions internal/characters/traveler/common/hydro/burst.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package hydro

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

var (
burstFirstHitmark = []int{34, 36}
consumeEnergyFrame = []int{4, 6}

burstFrames [][]int
)

func init() {
burstFrames = make([][]int, 2)

// Male
burstFrames[0] = frames.InitAbilSlice(78) // Q -> E/D/Walk
burstFrames[0][action.ActionAttack] = 76 // Q -> N1
burstFrames[0][action.ActionJump] = 77 // Q -> J
burstFrames[0][action.ActionSwap] = 76 // Q -> Swap

// Female
burstFrames[1] = frames.InitAbilSlice(78) // Q -> Walk
burstFrames[1][action.ActionAttack] = 77 // Q -> N1
burstFrames[1][action.ActionSkill] = 77 // Q -> E
burstFrames[1][action.ActionDash] = 77 // Q -> D
burstFrames[1][action.ActionJump] = 77 // Q -> J
burstFrames[1][action.ActionSwap] = 76 // Q -> Swap
}

func (c *char) Burst(p map[string]int) (action.Info, error) {
ai := combat.AttackInfo{
ActorIndex: c.Index,
Abil: "Rising Waters",
AttackTag: attacks.AttackTagElementalBurst,
ICDTag: attacks.ICDTagElementalBurst,
ICDGroup: attacks.ICDGroupTravelerBurst,
StrikeType: attacks.StrikeTypeDefault,
Element: attributes.Hydro,
Durability: 25,
Mult: burstDot[c.TalentLvlBurst()],
}
snap := c.Snapshot(&ai)

burstTicks := 8 // 4s duration * 0.5s tick
burstSpeed := 1.5
// The Movement SPD of Rising Waters' bubble will be decreased by 30%, and its duration increased by 3s.
if c.Base.Cons >= 2 {
burstTicks = 14 // 7s duration * 0.5s tick
burstSpeed = 1.05
}

firstHitmark := burstFirstHitmark[c.gender]
initialPos := c.Core.Combat.Player().Pos()
initialDirection := c.Core.Combat.Player().Direction()
for i := 0; i < burstTicks; i++ {
nextPos := geometry.CalcOffsetPoint(initialPos.Add(geometry.Point{X: 0.5, Y: 0.5}), geometry.Point{Y: burstSpeed * float64(i)}, initialDirection)
// TODO: Trigger the 0.15m AoE attack for every enemy within 2.5m (estimation) of the calculated pos to emulate the burst triggering its 0.15m AoE attack on collision.
c.Core.QueueAttackWithSnap(ai,
snap,
combat.NewCircleHit(c.Core.Combat.Player(), nextPos, nil, 0.15),
firstHitmark+30*i,
)
}

c.SetCD(action.ActionBurst, 20*60)
c.ConsumeEnergy(consumeEnergyFrame[c.gender])

return action.Info{
Frames: frames.NewAbilFunc(burstFrames[c.gender]),
AnimationLength: burstFrames[c.gender][action.InvalidAction],
CanQueueAfter: burstFrames[c.gender][action.ActionSwap], // earliest cancel
State: action.BurstState,
}, nil
}
63 changes: 63 additions & 0 deletions internal/characters/traveler/common/hydro/charge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package hydro

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

var chargeFrames [][]int
var chargeHitmarks = [][]int{{9, 20}, {14, 25}}

func init() {
chargeFrames = make([][]int, 2)
// Male
chargeFrames[0] = frames.InitAbilSlice(55) // CA -> N1
chargeFrames[0][action.ActionSkill] = 37 // CA -> E
chargeFrames[0][action.ActionBurst] = 36 // CA -> Q
chargeFrames[0][action.ActionDash] = chargeHitmarks[0][len(chargeHitmarks[0])-1] // CA -> D
chargeFrames[0][action.ActionJump] = chargeHitmarks[0][len(chargeHitmarks[0])-1] // CA -> J
chargeFrames[0][action.ActionSwap] = 44 // CA -> Swap

// Female
chargeFrames[1] = frames.InitAbilSlice(58) // CA -> N1
chargeFrames[1][action.ActionSkill] = 34 // CA -> E
chargeFrames[1][action.ActionBurst] = 35 // CA -> Q
chargeFrames[1][action.ActionDash] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> D
chargeFrames[1][action.ActionJump] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> J
chargeFrames[1][action.ActionSwap] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> Swap
}

func (c *char) ChargeAttack(p map[string]int) (action.Info, error) {
ai := combat.AttackInfo{
ActorIndex: c.Index,
AttackTag: attacks.AttackTagExtra,
ICDTag: attacks.ICDTagNormalAttack,
ICDGroup: attacks.ICDGroupDefault,
StrikeType: attacks.StrikeTypeSlash,
Element: attributes.Physical,
Durability: 25,
}

for i, mult := range charge[c.gender] {
ai.Mult = mult[c.TalentLvlAttack()]
ai.Abil = fmt.Sprintf("Charge %v", i)
c.Core.QueueAttack(
ai,
combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 2.2),
chargeHitmarks[c.gender][i],
chargeHitmarks[c.gender][i],
)
}

return action.Info{
Frames: frames.NewAbilFunc(chargeFrames[c.gender]),
AnimationLength: chargeFrames[c.gender][action.InvalidAction],
CanQueueAfter: chargeHitmarks[c.gender][len(chargeHitmarks[c.gender])-1],
State: action.ChargeAttackState,
}, nil
}
Loading

0 comments on commit e9058aa

Please sign in to comment.