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.
Implement Hydro MC (genshinsim#1640)
--------- Co-authored-by: srliao <[email protected]>
- Loading branch information
Showing
28 changed files
with
1,507 additions
and
7 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,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 | ||
} |
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,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 | ||
} |
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,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 | ||
} |
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,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 | ||
} |
Oops, something went wrong.