From 9d5b6062d8220b1537aaba34551782853227eaa7 Mon Sep 17 00:00:00 2001 From: Charlie Zheng Date: Wed, 1 Jan 2025 10:55:36 -0700 Subject: [PATCH] Initial mavuika implementation --- internal/characters/mavuika/asc.go | 60 +++ internal/characters/mavuika/attack.go | 137 +++++ internal/characters/mavuika/burst.go | 137 +++++ internal/characters/mavuika/charge.go | 132 +++++ internal/characters/mavuika/config.yml | 63 +++ internal/characters/mavuika/cons.go | 205 ++++++++ internal/characters/mavuika/dash.go | 36 ++ .../characters/mavuika/data_gen.textproto | 150 ++++++ internal/characters/mavuika/jump.go | 9 + internal/characters/mavuika/mavuika.go | 121 +++++ internal/characters/mavuika/mavuika_gen.go | 482 ++++++++++++++++++ internal/characters/mavuika/plunge.go | 163 ++++++ internal/characters/mavuika/skill.go | 249 +++++++++ internal/services/assets/avatars_gen.go | 1 + pkg/core/attacks/icd_tags_gen.go | 1 + pkg/core/keys/keys_char_gen.go | 5 + pkg/shortcut/characters.go | 2 + pkg/simulation/imports_char_gen.go | 1 + .../db/src/Data/char_data.generated.json | 16 + .../ui/src/Data/char_data.generated.json | 16 + 20 files changed, 1986 insertions(+) create mode 100644 internal/characters/mavuika/asc.go create mode 100644 internal/characters/mavuika/attack.go create mode 100644 internal/characters/mavuika/burst.go create mode 100644 internal/characters/mavuika/charge.go create mode 100644 internal/characters/mavuika/config.yml create mode 100644 internal/characters/mavuika/cons.go create mode 100644 internal/characters/mavuika/dash.go create mode 100644 internal/characters/mavuika/data_gen.textproto create mode 100644 internal/characters/mavuika/jump.go create mode 100644 internal/characters/mavuika/mavuika.go create mode 100644 internal/characters/mavuika/mavuika_gen.go create mode 100644 internal/characters/mavuika/plunge.go create mode 100644 internal/characters/mavuika/skill.go diff --git a/internal/characters/mavuika/asc.go b/internal/characters/mavuika/asc.go new file mode 100644 index 000000000..2646c6a55 --- /dev/null +++ b/internal/characters/mavuika/asc.go @@ -0,0 +1,60 @@ +package mavuika + +import ( + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/combat" + "github.com/genshinsim/gcsim/pkg/core/event" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +const ( + a1Key = "mavuika-a1" +) + +func (c *char) a1() { + if c.Base.Ascension < 1 { + return + } + m := make([]float64, attributes.EndStatType) + m[attributes.ATKP] = 0.3 + c.Core.Events.Subscribe(event.OnNightsoulBurst, func(args ...interface{}) bool { + c.AddStatMod(character.StatMod{ + Base: modifier.NewBaseWithHitlag(a1Key, 10*60), + Amount: func() ([]float64, bool) { + return m, true + }, + }) + return false + }, a1Key) +} + +func (c *char) a4Init() { + if c.Base.Ascension < 4 { + return + } + c.a4buff = make([]float64, attributes.EndStatType) +} + +func (c *char) a4() { + if c.Base.Ascension < 4 { + return + } + started := c.Core.F + for _, char := range c.Core.Player.Chars() { + this := char + this.AddAttackMod(character.AttackMod{ + Base: modifier.NewBase("mavuika-a4", 20*60), + Amount: func(_ *combat.AttackEvent, _ combat.Target) ([]float64, bool) { + // char must be active + if c.Core.Player.Active() != this.Index { + return nil, false + } + dmg := c.burstStacks*0.002 + c.c4BonusVal() + dmg *= 1.0 - float64(c.Core.F-started)*c.c4DecayRate() + c.a4buff[attributes.DmgP] = dmg + return c.a4buff, true + }, + }) + } +} diff --git a/internal/characters/mavuika/attack.go b/internal/characters/mavuika/attack.go new file mode 100644 index 000000000..aa7a57e99 --- /dev/null +++ b/internal/characters/mavuika/attack.go @@ -0,0 +1,137 @@ +package mavuika + +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{{21}, {11, 23}, {10, 18, 26}, {28}} + attackPoiseDMG = []float64{93.33, 92.72, 115.14, 143.17} + attackHitlagHaltFrame = []float64{0.09, 0.10, 0.08, .12} + attackHitboxes = []float64{2.2, 2.3, 1.8, 3} + attackOffsets = []float64{0.5, -1.3, 0.5, -0.8} + + bikeAttackFrames [][]int + bikeAttackHitmarks = []int{21, 22, 27, 14, 41} + bikeAttackPoiseDMG = []float64{76.6, 79.1, 93.6, 93.2, 121.7} + bikeAttackHitlagHaltFrame = []float64{0.09, 0.08, 0.04, 0.03, 0.0} + bikeAttackHitboxes = [][]float64{{3.7}, {4}, {3.7}, {5.5, 4.5}, {4.7}} + bikeAttackOffsets = []float64{0.5, -1.3, 0.5, -0.8, 1} +) + +const normalHitNum = 4 +const bikeHitNum = 5 + +func init() { + attackFrames = make([][]int, normalHitNum) + + attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 35) // N1 -> N2 + attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1][1], 44) // N2 -> N3 + attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2][2], 54) // N3 -> N4 + attackFrames[3] = frames.InitNormalCancelSlice(attackHitmarks[3][0], 42) // N4 -> N1 + + bikeAttackFrames = make([][]int, bikeHitNum) + + bikeAttackFrames[0] = frames.InitNormalCancelSlice(bikeAttackHitmarks[0], 26) // N1 -> N2 + bikeAttackFrames[1] = frames.InitNormalCancelSlice(bikeAttackHitmarks[1], 35) // N2 -> N3 + bikeAttackFrames[2] = frames.InitNormalCancelSlice(bikeAttackHitmarks[2], 34) // N3 -> N4 + bikeAttackFrames[3] = frames.InitNormalCancelSlice(bikeAttackHitmarks[3], 22) // N4 -> N5 + bikeAttackFrames[4] = frames.InitNormalCancelSlice(bikeAttackHitmarks[4], 63) // N5 -> N1 +} + +func (c *char) Attack(p map[string]int) (action.Info, error) { + if c.armamentState == bike && c.nightsoulState.HasBlessing() { + return c.bikeAttack(), nil + } + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: fmt.Sprintf("Normal %v", c.NormalCounter), + AttackTag: attacks.AttackTagNormal, + ICDTag: attacks.ICDTagNormalAttack, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: attackPoiseDMG[c.NormalCounter], + Element: attributes.Physical, + Durability: 25, + Mult: attack[c.NormalCounter][c.TalentLvlAttack()], + HitlagFactor: 0.01, + HitlagHaltFrames: attackHitlagHaltFrame[c.NormalCounter] * 60, + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: attackOffsets[c.NormalCounter]}, + attackHitboxes[c.NormalCounter], + ) + + for _, delay := range attackHitmarks[c.NormalCounter] { + c.Core.QueueAttack(ai, ap, delay, delay) + } + + 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 +} + +func (c *char) bikeAttack() action.Info { + delay := bikeAttackHitmarks[c.NormalCounter] + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: fmt.Sprintf("Flamestrider Normal %v", c.NormalCounter), + AttackTag: attacks.AttackTagNormal, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDTag: attacks.ICDTagMavuikaFlamestrider, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: bikeAttackPoiseDMG[c.NormalCounter], + Element: attributes.Pyro, + Durability: 25, + Mult: skillAttack[c.NormalCounter][c.TalentLvlAttack()], + HitlagFactor: 0.01, + HitlagHaltFrames: bikeAttackHitlagHaltFrame[c.NormalCounter] * 60, + IgnoreInfusion: true, + } + + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: bikeAttackOffsets[c.NormalCounter]}, + bikeAttackHitboxes[c.NormalCounter][0], + ) + + if c.NormalCounter == 3 { + ap = combat.NewBoxHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: bikeAttackOffsets[c.NormalCounter]}, + bikeAttackHitboxes[c.NormalCounter][0], + bikeAttackHitboxes[c.NormalCounter][1], + ) + } + c.QueueCharTask(func() { + ai.FlatDmg = c.burstBuffNA() + c.c2BikeNA() + c.Core.QueueAttack(ai, ap, 0, 0) + c.reduceNightsoulPoints(1) + }, delay) + + defer c.AdvanceNormalIndex() + + return action.Info{ + Frames: frames.NewAttackFunc(c.Character, bikeAttackFrames), + AnimationLength: bikeAttackFrames[c.NormalCounter][action.InvalidAction], + CanQueueAfter: bikeAttackHitmarks[c.NormalCounter], + State: action.NormalAttackState, + } +} + +// TODO: charged attack diff --git a/internal/characters/mavuika/burst.go b/internal/characters/mavuika/burst.go new file mode 100644 index 000000000..0bc57e010 --- /dev/null +++ b/internal/characters/mavuika/burst.go @@ -0,0 +1,137 @@ +package mavuika + +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/event" + "github.com/genshinsim/gcsim/pkg/core/geometry" + "github.com/genshinsim/gcsim/pkg/enemy" +) + +const ( + burstKey = "mavuika-burst" + energyNAICDKey = "mavuika-fighting-spirit-na-icd" + burstDuration = 7.0 * 60 + burstHitmark = 106 +) + +var ( + burstFrames []int +) + +func (c *char) nightsoulConsumptionMul() float64 { + if c.StatusIsActive(burstKey) { + return 0.0 + } + return 1.0 +} + +func init() { + burstFrames = frames.InitAbilSlice(116) // Q -> Swap +} + +func (c *char) Burst(p map[string]int) (action.Info, error) { + c.burstStacks = c.fightingSpirit + c.fightingSpirit = 0 + c.enterBike() + c.QueueCharTask(func() { + c.enterNightsoulOrRegenerate(10) + }, 87) + c.QueueCharTask(func() { + c.AddStatus(burstKey, burstDuration, true) + }, burstHitmark-1) + c.QueueCharTask(func() { + c.a4() + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Sunfell Slice", + AttackTag: attacks.AttackTagElementalBurst, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 150, + Element: attributes.Pyro, + Durability: 25, + Mult: burst[c.TalentLvlBurst()], + FlatDmg: c.burstBuffSunfell() + c.c2BikeQ(), + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + c.Core.QueueAttack(ai, ap, 0, 0) + }, burstHitmark) + + c.SetCDWithDelay(action.ActionBurst, 18*60, 0) + + return action.Info{ + Frames: frames.NewAbilFunc(burstFrames), + AnimationLength: burstFrames[action.InvalidAction], + CanQueueAfter: burstFrames[action.ActionSwap], // earliest cancel + State: action.BurstState, + }, nil +} + +func (c *char) burstBuffCA() float64 { + if !c.StatusIsActive(burstKey) { + return 0.0 + } + return c.burstStacks * burstCABonus[c.TalentLvlBurst()] * c.TotalAtk() +} + +func (c *char) burstBuffNA() float64 { + if !c.StatusIsActive(burstKey) { + return 0.0 + } + return c.burstStacks * burstNABonus[c.TalentLvlBurst()] * c.TotalAtk() +} + +func (c *char) burstBuffSunfell() float64 { + if !c.StatusIsActive(burstKey) { + return 0.0 + } + return c.burstStacks * burstQBonus[c.TalentLvlBurst()] * c.TotalAtk() +} + +func (c *char) gainFightingSpirit(val float64) { + c.fightingSpirit += val * c.c1FightingSpiritEff() + if c.fightingSpirit > 200 { + c.fightingSpirit = 200 + } + c.c1OnFightingSpirit() +} + +func (c *char) burstInit() { + c.fightingSpirit = 200 + c.Core.Events.Subscribe(event.OnNightsoulConsume, func(args ...interface{}) bool { + amount := args[1].(float64) + if amount < 0.0000001 { + return false + } + c.gainFightingSpirit(amount) + return false + }, "mavuika-fighting-spirit-ns") + + c.Core.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool { + ae := args[1].(*combat.AttackEvent) + _, ok := args[0].(*enemy.Enemy) + if !ok { + return false + } + if ae.Info.AttackTag != attacks.AttackTagNormal { + return false + } + if c.StatusIsActive(energyNAICDKey) { + return false + } + c.AddStatus(energyNAICDKey, 0.1*60, true) + c.gainFightingSpirit(1.5) + return false + }, "mavuika-fighting-spirit-na") +} diff --git a/internal/characters/mavuika/charge.go b/internal/characters/mavuika/charge.go new file mode 100644 index 000000000..2220166fc --- /dev/null +++ b/internal/characters/mavuika/charge.go @@ -0,0 +1,132 @@ +package mavuika + +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 chargeFrames []int +var bikeChargeFrames []int +var bikeChargeHitmarks = []int{36, 78, 119, 165, 208, 252, 297, 341} +var bikeChargeFinalHitmark = 421 + +const chargeHitmark = 40 + +func init() { + chargeFrames = frames.InitAbilSlice(48) + chargeFrames[action.ActionBurst] = 50 + chargeFrames[action.ActionDash] = chargeHitmark + chargeFrames[action.ActionJump] = chargeHitmark + chargeFrames[action.ActionSwap] = 50 + chargeFrames[action.ActionWalk] = 60 + + bikeChargeFrames = frames.InitAbilSlice(430) + bikeChargeFrames[action.ActionBurst] = 440 + bikeChargeFrames[action.ActionDash] = bikeChargeFinalHitmark + bikeChargeFrames[action.ActionJump] = bikeChargeFinalHitmark + bikeChargeFrames[action.ActionSwap] = 450 + bikeChargeFrames[action.ActionWalk] = 470 +} + +func (c *char) ChargeAttack(p map[string]int) (action.Info, error) { + if c.armamentState == bike && c.nightsoulState.HasBlessing() { + return c.bikeCharge(), nil + } + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Charge", + AttackTag: attacks.AttackTagExtra, + ICDTag: attacks.ICDTagNormalAttack, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 120.0, + Element: attributes.Physical, + Durability: 25, + Mult: charge[c.TalentLvlAttack()], + } + + c.Core.QueueAttack( + ai, + combat.NewBoxHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: -1.8}, + 2, + 4.5, + ), + chargeHitmark, + chargeHitmark, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(chargeFrames), + AnimationLength: chargeFrames[action.InvalidAction], + CanQueueAfter: chargeHitmark, + State: action.ChargeAttackState, + }, nil +} + +func (c *char) bikeCharge() action.Info { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Flamestride Charge", + AttackTag: attacks.AttackTagExtra, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDTag: attacks.ICDTagMavuikaFlamestrider, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 60.0, + Element: attributes.Pyro, + HitlagFactor: 0.01, + HitlagHaltFrames: 0.03 * 60, + Durability: 25, + Mult: skillCharge[c.TalentLvlSkill()], + IgnoreInfusion: true, + } + + for _, delay := range bikeChargeHitmarks { + c.QueueCharTask(func() { + ai.FlatDmg = c.burstBuffCA() + c.c2BikeCA() + c.Core.QueueAttack( + ai, + combat.NewBoxHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: -1.8}, + 2, + 4.5, + ), + 0, + 0, + ) + }, delay) + } + + c.QueueCharTask(func() { + ai.Abil = "Flamestride Charge (Final)" + ai.PoiseDMG = 120.0 + ai.HitlagHaltFrames = 0.04 * 60 + ai.Mult = skillChargeFinal[c.TalentLvlSkill()] + ai.FlatDmg = c.burstBuffCA() + + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1}, + 4, + ), + 0, + 0, + ) + }, bikeChargeFinalHitmark) + + return action.Info{ + Frames: frames.NewAbilFunc(chargeFrames), + AnimationLength: bikeChargeFrames[action.InvalidAction], + CanQueueAfter: bikeChargeFinalHitmark, + State: action.ChargeAttackState, + } +} diff --git a/internal/characters/mavuika/config.yml b/internal/characters/mavuika/config.yml new file mode 100644 index 000000000..e151a044e --- /dev/null +++ b/internal/characters/mavuika/config.yml @@ -0,0 +1,63 @@ +package_name: mavuika +genshin_id: 10000106 +key: mavuika +icd_tags: + - ICDTagMavuikaFlamestrider +action_param_keys: + skill: + - param: "hold" + - param: "recast" + low_plunge: + - param: "collision" + high_plunge: + - param: "collision" +skill_data_mapping: + attack: # Normal Attack: Flames Weave Life + attack_1: + - 0 # 1-Hit DMG|{param0:F1P} + attack_2: + - 1 # 2-Hit DMG|{param1:F1P}×2 + attack_3: + - 2 # 3-Hit DMG|{param2:F1P}×3 + attack_4: + - 3 # 4-Hit DMG|{param3:F1P} + charge: + - 4 # Charged Attack Final DMG|{param4:P} + collision: + - 6 # Plunge DMG|{param6:F1P} + lowPlunge: + - 7 # Low/High Plunge DMG|{param7:P}/{param8:P} + highPlunge: + - 8 # Low/High Plunge DMG|{param7:P}/{param8:P} + skill: # The Named Moment + skill: + - 0 # Activation DMG|{param0:F1P} + skillRing: + - 1 # Remote Weapon Interval DMG|{param1:F1P} + skillAttack_1: + - 3 # Motorcycle Normal Attack 1-Hit DMG|{param3:F1P} + skillAttack_2: + - 4 # Motorcycle Normal Attack 2-Hit DMG|{param4:F1P} + skillAttack_3: + - 5 # Motorcycle Normal Attack 3-Hit DMG|{param5:F1P} + skillAttack_4: + - 6 # Motorcycle Normal Attack 4-Hit DMG|{param6:F1P} + skillAttack_5: + - 7 # Motorcycle Normal Attack 5-Hit DMG|{param7:F1P} + skillDash: + - 8 # Motorcycle Sprint DMG|{param8:F1P} + skillCharge: + - 9 # Motorcycle Charged Attack Cyclic DMG|{param9:F1P} + skillChargeFinal: + - 10 # Motorcycle Charged Attack Final DMG|{param10:F1P} + skillPlunge: + - 11 # Motorcycle Plunge DMG|{param11:F1P} + burst: # Hour of Burning Skies + burst: + - 0 # Explosion DMG|{param0:F1P} + burstQBonus: + - 2 # Burst DMG Bonus|{param2:F1P} ATK per War God Energy Point + burstNABonus: + - 3 # Motorcycle Normal Attack Bonus|{param3:F2P} ATK per War God Energy Point + burstCABonus: + - 4 # Motorcycle Charged Attack Bonus|{param4:F2P} ATK per War God Energy Point diff --git a/internal/characters/mavuika/cons.go b/internal/characters/mavuika/cons.go new file mode 100644 index 000000000..2fb5e2797 --- /dev/null +++ b/internal/characters/mavuika/cons.go @@ -0,0 +1,205 @@ +package mavuika + +import ( + "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/player/character" + "github.com/genshinsim/gcsim/pkg/core/targets" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +const c6IcdKey = "mavuika-c6-icd" +const c1Key = "mavuika-c1" + +func (c *char) c1Init() { + if c.Base.Cons < 1 { + c.nightsoulState.MaxPoints = 80 + return + } + c.nightsoulState.MaxPoints = 120 + c.c1buff = make([]float64, attributes.EndStatType) + c.c1buff[attributes.ATKP] = 0.4 +} +func (c *char) c1FightingSpiritEff() float64 { + if c.Base.Cons < 1 { + return 1.0 + } + return 1.25 +} +func (c *char) c1OnFightingSpirit() { + if c.Base.Cons < 1 { + return + } + c.AddStatMod(character.StatMod{ + Base: modifier.NewBaseWithHitlag(c1Key, 10*60), + Amount: func() ([]float64, bool) { + return c.c1buff, true + }, + }) +} + +func (c *char) c2Init() { + if c.Base.Cons < 2 { + return + } + m := make([]float64, attributes.EndStatType) + m[attributes.BaseATK] = 200 + c.AddStatMod(character.StatMod{ + Base: modifier.NewBase("mavuika-c2-base-atk", -1), + Amount: func() ([]float64, bool) { + if c.nightsoulState.HasBlessing() { + return m, true + } + return nil, false + }, + }) +} + +func (c *char) c2Ring() { + if c.Base.Cons < 2 { + return + } + if !c.isRingFollowing() { + return + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + for _, e := range c.Core.Combat.EnemiesWithinArea(ap, nil) { + e.AddDefMod(combat.DefMod{ + Base: modifier.NewBaseWithHitlag("mavuika-c2", 30), + Value: -0.2, + }) + } + c.QueueCharTask(c.c2Ring, 18) +} + +func (c *char) c2BikeNA() float64 { + if c.Base.Cons < 2 { + return 0.0 + } + if c.armamentState != bike { + return 0.0 + } + return 0.6 * c.TotalAtk() +} +func (c *char) c2BikeCA() float64 { + if c.Base.Cons < 2 { + return 0.0 + } + if c.armamentState != bike { + return 0.0 + } + return 0.9 * c.TotalAtk() +} + +func (c *char) c2BikeQ() float64 { + if c.Base.Cons < 2 { + return 0.0 + } + if c.armamentState != bike { + return 0.0 + } + return 1.2 * c.TotalAtk() +} + +func (c *char) c4BonusVal() float64 { + if c.Base.Cons < 4 { + return 0.0 + } + return 0.1 +} + +func (c *char) c4DecayRate() float64 { + if c.Base.Cons < 4 { + return 1.0 / (20 * 60) + } + return 0.0 +} + +// this is just used for c2 +func (c *char) isRingFollowing() bool { + if c.Base.Cons < 6 { + return c.armamentState == ring + } + return true +} + +func (c *char) c6RingCB() func(a combat.AttackCB) { + if c.Base.Cons < 6 { + return nil + } + return func(a combat.AttackCB) { + if a.Target.Type() != targets.TargettableEnemy { + return + } + if c.StatusIsActive(c6IcdKey) { + return + } + c.AddStatus(c6IcdKey, 0.5*60, true) + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Flamestrider (C6)", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 75, + Element: attributes.Pyro, + Durability: 0, + Mult: 2.0, + } + ap := combat.NewCircleHitOnTarget( + a.Target, + nil, + 6, + ) + c.Core.QueueAttack(ai, ap, 3, 3) + } +} + +func (c *char) c6Bike() { + if c.Base.Cons < 6 { + return + } + c.c6Src = c.Core.F + c.QueueCharTask(c.c6RingAtk(c.c6Src), 180) +} + +func (c *char) c6RingAtk(src int) func() { + return func() { + if c.c6Src != src { + return + } + if c.armamentState != bike { + return + } + if !c.nightsoulState.HasBlessing() { + return + } + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Rings of Searing Radiance (C6)", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Pyro, + Durability: 0, + Mult: 4.0, + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + c.Core.QueueAttack(ai, ap, 0, 0) + c.QueueCharTask(c.c6RingAtk(src), 180) + } +} diff --git a/internal/characters/mavuika/dash.go b/internal/characters/mavuika/dash.go new file mode 100644 index 000000000..d2dc0cea1 --- /dev/null +++ b/internal/characters/mavuika/dash.go @@ -0,0 +1,36 @@ +package mavuika + +import ( + "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" +) + +func (c *char) Dash(p map[string]int) (action.Info, error) { + if c.armamentState == bike && c.nightsoulState.HasBlessing() { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Flamestrider Sprint", + AttackTag: attacks.AttackTagNone, + ICDTag: attacks.ICDTagMavuikaFlamestrider, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 75.0, + Element: attributes.Pyro, + Durability: 25, + Mult: skillDash[c.TalentLvlSkill()], + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 1.2, + ) + c.Core.QueueAttack(ai, ap, 10, 10) + c.reduceNightsoulPoints(10) + } + + return c.Character.Dash(p) +} diff --git a/internal/characters/mavuika/data_gen.textproto b/internal/characters/mavuika/data_gen.textproto new file mode 100644 index 000000000..b6870436f --- /dev/null +++ b/internal/characters/mavuika/data_gen.textproto @@ -0,0 +1,150 @@ +id: 10000106 +key: "mavuika" +rarity: QUALITY_ORANGE +body: BODY_LADY +region: ASSOC_TYPE_NATLAN +element: Fire +weapon_class: WEAPON_CLAYMORE +icon_name: "UI_AvatarIcon_Mavuika" +stats: { + base_hp: 977.153 + base_atk: 27.93 + base_def: 61.6249 + hp_curve: GROW_CURVE_HP_S5 + atk_curve: GROW_CURVE_ATTACK_S5 + def_cruve: GROW_CURVE_HP_S5 + promo_data: { + max_level: 20 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + } + } + promo_data: { + max_level: 40 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 837.8203 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 52.839 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 23.9457 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + } + } + promo_data: { + max_level: 50 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 1433.1138 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 90.3825 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 40.95975 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + value: 0.096 + } + } + promo_data: { + max_level: 60 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2226.8384 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 140.4405 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 63.64515 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + value: 0.192 + } + } + promo_data: { + max_level: 70 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2822.1316 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 177.984 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 80.6592 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + value: 0.192 + } + } + promo_data: { + max_level: 80 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 3417.425 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 215.5275 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 97.67325 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + value: 0.288 + } + } + promo_data: { + max_level: 90 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 4012.7185 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 253.071 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 114.6873 + } + add_props: { + prop_type: FIGHT_PROP_CRITICAL_HURT + value: 0.384 + } + } +} +skill_details: { + skill: 11062 + burst: 11065 + attack: 11061 +} +name_text_hash_map: 113398050 diff --git a/internal/characters/mavuika/jump.go b/internal/characters/mavuika/jump.go new file mode 100644 index 000000000..688cbcb31 --- /dev/null +++ b/internal/characters/mavuika/jump.go @@ -0,0 +1,9 @@ +package mavuika + +import ( + "github.com/genshinsim/gcsim/pkg/core/action" +) + +func (c *char) Jump(p map[string]int) (action.Info, error) { + return c.Character.Jump(p) +} diff --git a/internal/characters/mavuika/mavuika.go b/internal/characters/mavuika/mavuika.go new file mode 100644 index 000000000..c04a63b5f --- /dev/null +++ b/internal/characters/mavuika/mavuika.go @@ -0,0 +1,121 @@ +package mavuika + +import ( + tmpl "github.com/genshinsim/gcsim/internal/template/character" + "github.com/genshinsim/gcsim/internal/template/nightsoul" + "github.com/genshinsim/gcsim/pkg/core" + "github.com/genshinsim/gcsim/pkg/core/action" + "github.com/genshinsim/gcsim/pkg/core/event" + "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/core/keys" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/model" +) + +type SkillState int + +const ( + ring SkillState = iota + bike +) + +type char struct { + *tmpl.Character + fightingSpirit float64 + nightsoulState *nightsoul.State + nightsoulSrc int + armamentState SkillState + ringSrc int + burstStacks float64 + a4buff []float64 + c1buff []float64 + c6Src int +} + +func init() { + core.RegisterCharFunc(keys.Mavuika, NewChar) +} + +func NewChar(s *core.Core, w *character.CharWrapper, _ info.CharacterProfile) error { + c := char{} + t := tmpl.New(s) + + t.CharWrapper = w + c.Character = t + + c.EnergyMax = 0 + c.BurstCon = 3 + c.SkillCon = 5 + c.NormalHitNum = normalHitNum + + w.Character = &c + c.nightsoulState = nightsoul.New(c.Core, c.CharWrapper) + return nil +} + +func (c *char) Init() error { + c.onExitField() + c.burstInit() + c.a1() + c.c1Init() + c.c2Init() + c.a4Init() + + return nil +} + +func (c *char) ActionStam(a action.Action, p map[string]int) float64 { + if c.armamentState == bike && c.nightsoulState.HasBlessing() { + switch a { + case action.ActionCharge: + return 0 + case action.ActionDash: + return 0 + } + } + + if a == action.ActionCharge { + return 50 + } + return c.Character.ActionStam(a, p) +} + +func (c *char) ActionReady(a action.Action, p map[string]int) (bool, action.Failure) { + switch a { + case action.ActionBurst: + if c.fightingSpirit < 100 { + return false, action.InsufficientEnergy + } + if c.AvailableCDCharge[a] <= 0 { + return false, action.BurstCD + } + return true, action.NoFailure + case action.ActionSkill: + if p["recast"] != 0 { + return !c.StatusIsActive(skillRecastCDKey), action.SkillCD + } + return c.Character.ActionReady(a, p) + } + return c.Character.ActionReady(a, p) +} + +func (c *char) onExitField() { + c.Core.Events.Subscribe(event.OnCharacterSwap, func(_ ...interface{}) bool { + c.DeleteStatus(burstKey) + if c.armamentState == bike && c.nightsoulState.HasBlessing() { + c.exitBike() + } + return false + }, "mavuika-exit") +} + +func (c *char) AnimationStartDelay(k model.AnimationDelayKey) int { + switch k { + case model.AnimationXingqiuN0StartDelay: + return 22 + case model.AnimationYelanN0StartDelay: + return 22 + default: + return c.Character.AnimationStartDelay(k) + } +} diff --git a/internal/characters/mavuika/mavuika_gen.go b/internal/characters/mavuika/mavuika_gen.go new file mode 100644 index 000000000..b6678d0e0 --- /dev/null +++ b/internal/characters/mavuika/mavuika_gen.go @@ -0,0 +1,482 @@ +// Code generated by "pipeline"; DO NOT EDIT. +package mavuika + +import ( + _ "embed" + + "fmt" + "github.com/genshinsim/gcsim/pkg/core/action" + "github.com/genshinsim/gcsim/pkg/core/keys" + "github.com/genshinsim/gcsim/pkg/gcs/validation" + "github.com/genshinsim/gcsim/pkg/model" + "google.golang.org/protobuf/encoding/prototext" + "slices" +) + +//go:embed data_gen.textproto +var pbData []byte +var base *model.AvatarData +var paramKeysValidation = map[action.Action][]string{ + 1: {"hold", "recast"}, + 5: {"collision"}, + 6: {"collision"}, +} + +func init() { + base = &model.AvatarData{} + err := prototext.Unmarshal(pbData, base) + if err != nil { + panic(err) + } + validation.RegisterCharParamValidationFunc(keys.Mavuika, ValidateParamKeys) +} + +func ValidateParamKeys(a action.Action, keys []string) error { + valid, ok := paramKeysValidation[a] + if !ok { + return nil + } + for _, v := range keys { + if !slices.Contains(valid, v) { + return fmt.Errorf("key %v is invalid for action %v", v, a.String()) + } + } + return nil +} + +func (x *char) Data() *model.AvatarData { + return base +} + +var ( + attack = [][]float64{ + attack_1, + attack_2, + attack_3, + attack_4, + } + skillAttack = [][]float64{ + skillAttack_1, + skillAttack_2, + skillAttack_3, + skillAttack_4, + skillAttack_5, + } +) + +var ( + // attack: attack_1 = [0] + attack_1 = []float64{ + 0.80035, + 0.865495, + 0.93064, + 1.023704, + 1.088849, + 1.1633, + 1.26567, + 1.368041, + 1.470411, + 1.582088, + 1.693765, + 1.805442, + 1.917118, + 2.028795, + 2.140472, + } + // attack: attack_2 = [1] + attack_2 = []float64{ + 0.364799, + 0.394492, + 0.424185, + 0.466604, + 0.496296, + 0.530231, + 0.576892, + 0.623552, + 0.670212, + 0.721114, + 0.772017, + 0.822919, + 0.873821, + 0.924723, + 0.975625, + } + // attack: attack_3 = [2] + attack_3 = []float64{ + 0.332232, + 0.359274, + 0.386317, + 0.424948, + 0.45199, + 0.482896, + 0.525391, + 0.567885, + 0.61038, + 0.656738, + 0.703096, + 0.749454, + 0.795812, + 0.84217, + 0.888528, + } + // attack: attack_4 = [3] + attack_4 = []float64{ + 1.161929, + 1.256504, + 1.35108, + 1.486188, + 1.580764, + 1.68885, + 1.837469, + 1.986088, + 2.134706, + 2.296836, + 2.458966, + 2.621095, + 2.783225, + 2.945354, + 3.107484, + } + // attack: charge = [4] + charge = []float64{ + 1.93844, + 2.09622, + 2.254, + 2.4794, + 2.63718, + 2.8175, + 3.06544, + 3.31338, + 3.56132, + 3.8318, + 4.10228, + 4.37276, + 4.64324, + 4.91372, + 5.1842, + } + // attack: collision = [6] + collision = []float64{ + 0.745878, + 0.806589, + 0.8673, + 0.95403, + 1.014741, + 1.084125, + 1.179528, + 1.274931, + 1.370334, + 1.47441, + 1.578486, + 1.682562, + 1.786638, + 1.890714, + 1.99479, + } + // attack: highPlunge = [8] + highPlunge = []float64{ + 1.862889, + 2.01452, + 2.16615, + 2.382765, + 2.534396, + 2.707688, + 2.945964, + 3.184241, + 3.422517, + 3.682455, + 3.942393, + 4.202331, + 4.462269, + 4.722207, + 4.982145, + } + // attack: lowPlunge = [7] + lowPlunge = []float64{ + 1.49144, + 1.612836, + 1.734233, + 1.907656, + 2.029052, + 2.167791, + 2.358556, + 2.549322, + 2.740087, + 2.948195, + 3.156303, + 3.364411, + 3.572519, + 3.780627, + 3.988735, + } + // skill: skill = [0] + skill = []float64{ + 0.744, + 0.7998, + 0.8556, + 0.93, + 0.9858, + 1.0416, + 1.116, + 1.1904, + 1.2648, + 1.3392, + 1.4136, + 1.488, + 1.581, + 1.674, + 1.767, + } + // skill: skillAttack_1 = [3] + skillAttack_1 = []float64{ + 0.572648, + 0.619259, + 0.66587, + 0.732457, + 0.779068, + 0.832337, + 0.905583, + 0.978829, + 1.052075, + 1.131979, + 1.211883, + 1.291788, + 1.371692, + 1.451597, + 1.531501, + } + // skill: skillAttack_2 = [4] + skillAttack_2 = []float64{ + 0.591327, + 0.639459, + 0.68759, + 0.756349, + 0.80448, + 0.859488, + 0.935122, + 1.010757, + 1.086392, + 1.168903, + 1.251414, + 1.333925, + 1.416435, + 1.498946, + 1.581457, + } + // skill: skillAttack_3 = [5] + skillAttack_3 = []float64{ + 0.699868, + 0.756834, + 0.8138, + 0.89518, + 0.952146, + 1.01725, + 1.106768, + 1.196286, + 1.285804, + 1.38346, + 1.481116, + 1.578772, + 1.676428, + 1.774084, + 1.87174, + } + // skill: skillAttack_4 = [6] + skillAttack_4 = []float64{ + 0.697047, + 0.753784, + 0.81052, + 0.891572, + 0.948308, + 1.01315, + 1.102307, + 1.191464, + 1.280622, + 1.377884, + 1.475146, + 1.572409, + 1.669671, + 1.766934, + 1.864196, + } + // skill: skillAttack_5 = [7] + skillAttack_5 = []float64{ + 0.910035, + 0.984107, + 1.05818, + 1.163998, + 1.238071, + 1.322725, + 1.439125, + 1.555525, + 1.671924, + 1.798906, + 1.925888, + 2.052869, + 2.179851, + 2.306832, + 2.433814, + } + // skill: skillCharge = [9] + skillCharge = []float64{ + 0.989, + 1.0695, + 1.15, + 1.265, + 1.3455, + 1.4375, + 1.564, + 1.6905, + 1.817, + 1.955, + 2.093, + 2.231, + 2.369, + 2.507, + 2.645, + } + // skill: skillChargeFinal = [10] + skillChargeFinal = []float64{ + 1.376, + 1.488, + 1.6, + 1.76, + 1.872, + 2, + 2.176, + 2.352, + 2.528, + 2.72, + 2.912, + 3.104, + 3.296, + 3.488, + 3.68, + } + // skill: skillDash = [8] + skillDash = []float64{ + 0.8084, + 0.8742, + 0.94, + 1.034, + 1.0998, + 1.175, + 1.2784, + 1.3818, + 1.4852, + 1.598, + 1.7108, + 1.8236, + 1.9364, + 2.0492, + 2.162, + } + // skill: skillPlunge = [11] + skillPlunge = []float64{ + 1.5996, + 1.7298, + 1.86, + 2.046, + 2.1762, + 2.325, + 2.5296, + 2.7342, + 2.9388, + 3.162, + 3.3852, + 3.6084, + 3.8316, + 4.0548, + 4.278, + } + // skill: skillRing = [1] + skillRing = []float64{ + 1.28, + 1.376, + 1.472, + 1.6, + 1.696, + 1.792, + 1.92, + 2.048, + 2.176, + 2.304, + 2.432, + 2.56, + 2.72, + 2.88, + 3.04, + } + // burst: burst = [0] + burst = []float64{ + 4.448, + 4.7816, + 5.1152, + 5.56, + 5.8936, + 6.2272, + 6.672, + 7.1168, + 7.5616, + 8.0064, + 8.4512, + 8.896, + 9.452, + 10.008, + 10.564, + } + // burst: burstCABonus = [4] + burstCABonus = []float64{ + 0.00516, + 0.00558, + 0.006, + 0.0066, + 0.00702, + 0.0075, + 0.00816, + 0.00882, + 0.00948, + 0.0102, + 0.01092, + 0.01164, + 0.01236, + 0.01308, + 0.0138, + } + // burst: burstNABonus = [3] + burstNABonus = []float64{ + 0.00258, + 0.00279, + 0.003, + 0.0033, + 0.00351, + 0.00375, + 0.00408, + 0.00441, + 0.00474, + 0.0051, + 0.00546, + 0.00582, + 0.00618, + 0.00654, + 0.0069, + } + // burst: burstQBonus = [2] + burstQBonus = []float64{ + 0.016, + 0.0172, + 0.0184, + 0.02, + 0.0212, + 0.0224, + 0.024, + 0.0256, + 0.0272, + 0.0288, + 0.0304, + 0.032, + 0.034, + 0.036, + 0.038, + } +) diff --git a/internal/characters/mavuika/plunge.go b/internal/characters/mavuika/plunge.go new file mode 100644 index 000000000..727374f50 --- /dev/null +++ b/internal/characters/mavuika/plunge.go @@ -0,0 +1,163 @@ +package mavuika + +import ( + "errors" + + "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/player" +) + +var highPlungeFrames []int +var lowPlungeFrames []int + +const lowPlungeHitmark = 38 +const highPlungeHitmark = 41 +const collisionHitmark = lowPlungeHitmark - 6 + +const lowPlungePoiseDMG = 150.0 +const lowPlungeRadius = 3.0 + +const highPlungePoiseDMG = 200.0 +const highPlungeRadius = 5.0 + +func init() { + // low_plunge -> x + lowPlungeFrames = frames.InitAbilSlice(80) + lowPlungeFrames[action.ActionAttack] = 51 + lowPlungeFrames[action.ActionSkill] = 51 + lowPlungeFrames[action.ActionBurst] = 50 + lowPlungeFrames[action.ActionDash] = lowPlungeHitmark + lowPlungeFrames[action.ActionWalk] = 79 + lowPlungeFrames[action.ActionSwap] = 62 + + // high_plunge -> x + highPlungeFrames = frames.InitAbilSlice(83) + highPlungeFrames[action.ActionAttack] = 54 + highPlungeFrames[action.ActionSkill] = 55 + highPlungeFrames[action.ActionBurst] = 53 + highPlungeFrames[action.ActionDash] = highPlungeHitmark + highPlungeFrames[action.ActionWalk] = 82 + highPlungeFrames[action.ActionSwap] = 64 +} + +// Low Plunge attack damage queue generator +// Use the "collision" optional argument if you want to do a falling hit on the way down +// Default = 0 +func (c *char) LowPlungeAttack(p map[string]int) (action.Info, error) { + defer c.Core.Player.SetAirborne(player.Grounded) + switch c.Core.Player.Airborne() { + case player.AirborneXianyun: + return c.lowPlungeXY(p), nil + default: + return action.Info{}, errors.New("low_plunge can only be used while airborne") + } +} + +func (c *char) lowPlungeXY(p map[string]int) action.Info { + collision, ok := p["collision"] + if !ok { + collision = 0 // Whether or not collision hit + } + + if collision > 0 { + c.plungeCollision(collisionHitmark) + } + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Low Plunge", + AttackTag: attacks.AttackTagPlunge, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: lowPlungePoiseDMG, + Element: attributes.Physical, + Durability: 25, + Mult: lowPlunge[c.TalentLvlAttack()], + } + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.Player(), geometry.Point{Y: 1}, lowPlungeRadius), + lowPlungeHitmark, + lowPlungeHitmark, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(lowPlungeFrames), + AnimationLength: lowPlungeFrames[action.InvalidAction], + CanQueueAfter: lowPlungeFrames[action.ActionDash], + State: action.PlungeAttackState, + } +} + +// High Plunge attack damage queue generator +// Use the "collision" optional argument if you want to do a falling hit on the way down +// Default = 0 +func (c *char) HighPlungeAttack(p map[string]int) (action.Info, error) { + defer c.Core.Player.SetAirborne(player.Grounded) + switch c.Core.Player.Airborne() { + case player.AirborneXianyun: + return c.highPlungeXY(p), nil + default: + return action.Info{}, errors.New("high_plunge can only be used while airborne") + } +} + +func (c *char) highPlungeXY(p map[string]int) action.Info { + collision, ok := p["collision"] + if !ok { + collision = 0 // Whether or not collision hit + } + + if collision > 0 { + c.plungeCollision(collisionHitmark) + } + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "High Plunge", + AttackTag: attacks.AttackTagPlunge, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: highPlungePoiseDMG, + Element: attributes.Physical, + Durability: 25, + Mult: highPlunge[c.TalentLvlAttack()], + } + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.Player(), geometry.Point{Y: 1}, highPlungeRadius), + highPlungeHitmark, + highPlungeHitmark, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(highPlungeFrames), + AnimationLength: highPlungeFrames[action.InvalidAction], + CanQueueAfter: highPlungeFrames[action.ActionDash], + State: action.PlungeAttackState, + } +} + +// Plunge normal falling attack damage queue generator +// Standard - Always part of high/low plunge attacks +func (c *char) plungeCollision(delay int) { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Plunge Collision", + AttackTag: attacks.AttackTagPlunge, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeSlash, + Element: attributes.Physical, + Durability: 0, + Mult: collision[c.TalentLvlAttack()], + } + c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(c.Core.Combat.Player(), geometry.Point{Y: 1}, 1), delay, delay) +} diff --git a/internal/characters/mavuika/skill.go b/internal/characters/mavuika/skill.go new file mode 100644 index 000000000..8d32ad769 --- /dev/null +++ b/internal/characters/mavuika/skill.go @@ -0,0 +1,249 @@ +package mavuika + +import ( + "errors" + + "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/targets" +) + +var ( + skillFrames []int + skillRecastFrames []int +) + +const ( + skillHitmark = 16 + particleICDKey = "mavuika-particle-icd" + skillRecastCD = 60 + skillRecastCDKey = "mavuika-skill-recast-cd" +) + +func init() { + skillFrames = frames.InitAbilSlice(16) // E -> N1 + + skillRecastFrames = frames.InitAbilSlice(19) // E -> N1 +} + +func (c *char) nightsoulPointReduceFunc(src int) func() { + return func() { + if c.nightsoulSrc != src { + return + } + val := 0.5 + if c.armamentState == bike { + val += 0.4 + if c.Core.Player.CurrentState() == action.ChargeAttackState { + val += 0.2 + } + } + c.reduceNightsoulPoints(val) + c.Core.Tasks.Add(c.nightsoulPointReduceFunc(src), 6) + } +} + +func (c *char) reduceNightsoulPoints(val float64) { + val *= c.nightsoulConsumptionMul() + if val == 0 { + return + } + c.nightsoulState.ConsumePoints(val) + + // don't exit nightsoul while in NA/Plunge/Charge of Flamestride + if c.armamentState == bike { + switch c.Core.Player.CurrentState() { + case action.NormalAttackState, action.PlungeAttackState, action.ChargeAttackState: + return + } + } + + if c.nightsoulState.Points() < 0.001 { + c.exitNightsoul() + } +} + +func (c *char) exitNightsoul() { + if !c.nightsoulState.HasBlessing() { + return + } + c.nightsoulState.ExitBlessing() + c.nightsoulState.ClearPoints() + c.nightsoulSrc = -1 + c.NormalHitNum = normalHitNum + c.NormalCounter = 0 +} +func (c *char) enterNightsoulOrRegenerate(points float64) { + if !c.nightsoulState.HasBlessing() { + c.nightsoulState.EnterBlessing(points) + c.nightsoulSrc = c.Core.F + c.Core.Tasks.Add(c.nightsoulPointReduceFunc(c.nightsoulSrc), 6) + return + } + c.nightsoulState.GeneratePoints(points) +} +func (c *char) Skill(p map[string]int) (action.Info, error) { + h := p["hold"] + recast := p["recast"] + if recast != 0 { + if h > 0 { + return action.Info{}, errors.New("cannot hold E while recasting") + } + if !c.nightsoulState.HasBlessing() { + return action.Info{}, errors.New("cannot recast E while not in nightsoul blessing") + } + c.skillRecast() + } + + c.enterNightsoulOrRegenerate(c.nightsoulState.MaxPoints) + if h > 0 { + return c.skillHold(), nil + } + return c.skillPress(), nil +} + +func (c *char) enterBike() { + c.Core.Log.NewEvent("switching to bike state", glog.LogCharacterEvent, c.Index) + c.armamentState = bike + c.NormalHitNum = bikeHitNum + c.NormalCounter = 0 + c.c6Bike() +} + +func (c *char) exitBike() { + c.Core.Log.NewEvent("switching to ring state", glog.LogCharacterEvent, c.Index) + c.armamentState = ring + c.NormalHitNum = normalHitNum + c.ringSrc = c.Core.F + + c.QueueCharTask(c.skillRing(c.ringSrc), 120) + c.c2Ring() +} + +func (c *char) skillRecast() action.Info { + switch c.armamentState { + case ring: + c.enterBike() + + default: + c.exitBike() + } + c.AddStatus(skillRecastCDKey, skillRecastCD, false) + return action.Info{ + Frames: frames.NewAbilFunc(skillRecastFrames), + AnimationLength: skillRecastFrames[action.InvalidAction], + CanQueueAfter: skillRecastFrames[action.ActionSwap], + State: action.SkillState, + } +} + +func (c *char) skillHold() action.Info { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "The Named Moment (Flamestrider)", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeBlunt, + PoiseDMG: 75, + Element: attributes.Pyro, + Durability: 25, + Mult: skill[c.TalentLvlSkill()], + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + c.Core.QueueAttack(ai, ap, skillHitmark, skillHitmark, c.particleCB) + c.enterBike() + c.SetCDWithDelay(action.ActionSkill, 15*60, 18) + + return action.Info{ + Frames: frames.NewAbilFunc(skillFrames), + AnimationLength: skillFrames[action.InvalidAction], + CanQueueAfter: skillFrames[action.ActionSwap], + State: action.SkillState, + } +} + +func (c *char) skillPress() action.Info { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "The Named Moment", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Pyro, + Durability: 25, + Mult: skill[c.TalentLvlSkill()], + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + c.Core.QueueAttack(ai, ap, skillHitmark, skillHitmark, c.particleCB) + c.exitBike() + c.SetCDWithDelay(action.ActionSkill, 15*60, 18) + + return action.Info{ + Frames: frames.NewAbilFunc(skillFrames), + AnimationLength: skillFrames[action.InvalidAction], + CanQueueAfter: skillFrames[action.ActionSwap], + State: action.SkillState, + } +} + +func (c *char) skillRing(src int) func() { + return func() { + if c.ringSrc != src { + return + } + if c.armamentState != ring { + return + } + if !c.nightsoulState.HasBlessing() { + return + } + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Rings of Searing Radiance", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Pyro, + Durability: 25, + Mult: skillRing[c.TalentLvlSkill()], + } + ap := combat.NewCircleHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: 1.0}, + 6, + ) + c.Core.QueueAttack(ai, ap, 0, 0, c.c6RingCB()) + c.reduceNightsoulPoints(3) + c.QueueCharTask(c.skillRing(src), 120) + } +} + +func (c *char) particleCB(a combat.AttackCB) { + if a.Target.Type() != targets.TargettableEnemy { + return + } + if c.StatusIsActive(particleICDKey) { + return + } + c.AddStatus(particleICDKey, 0.5*60, true) + c.Core.QueueParticle(c.Base.Key.String(), 5, attributes.Pyro, c.ParticleDelay) +} diff --git a/internal/services/assets/avatars_gen.go b/internal/services/assets/avatars_gen.go index 33f227a12..0d1e6807f 100644 --- a/internal/services/assets/avatars_gen.go +++ b/internal/services/assets/avatars_gen.go @@ -50,6 +50,7 @@ var avatarMap = map[string]string{ "lisa": "UI_AvatarIcon_Lisa", "lynette": "UI_AvatarIcon_Linette", "lyney": "UI_AvatarIcon_Liney", + "mavuika": "UI_AvatarIcon_Mavuika", "mika": "UI_AvatarIcon_Mika", "mona": "UI_AvatarIcon_Mona", "mualani": "UI_AvatarIcon_Mualani", diff --git a/pkg/core/attacks/icd_tags_gen.go b/pkg/core/attacks/icd_tags_gen.go index 5af7d7ff6..4219fbfb3 100644 --- a/pkg/core/attacks/icd_tags_gen.go +++ b/pkg/core/attacks/icd_tags_gen.go @@ -18,6 +18,7 @@ const ( ICDTagLisaElectro ICDTagLyneyEndBoom ICDTagLyneyEndBoomEnhanced + ICDTagMavuikaFlamestrider ICDTagMonaWaterDamage ICDTagNahidaSkill ICDTagNahidaC6 diff --git a/pkg/core/keys/keys_char_gen.go b/pkg/core/keys/keys_char_gen.go index fb042ca36..ac3ba3637 100644 --- a/pkg/core/keys/keys_char_gen.go +++ b/pkg/core/keys/keys_char_gen.go @@ -55,6 +55,7 @@ const ( Lisa Lynette Lyney + Mavuika Mika Mona Mualani @@ -289,6 +290,10 @@ func init() { charPrettyName[Lyney] = "Lyney" CharKeyToEle[Lyney] = attributes.Pyro + charNames[Mavuika] = "mavuika" + charPrettyName[Mavuika] = "Mavuika" + CharKeyToEle[Mavuika] = attributes.Pyro + charNames[Mika] = "mika" charPrettyName[Mika] = "Mika" CharKeyToEle[Mika] = attributes.Cryo diff --git a/pkg/shortcut/characters.go b/pkg/shortcut/characters.go index 864b19d13..627f566cd 100644 --- a/pkg/shortcut/characters.go +++ b/pkg/shortcut/characters.go @@ -173,4 +173,6 @@ var CharNameToKey = map[string]keys.Char{ "xilonen": keys.Xilonen, "xilo": keys.Xilonen, "sigewinne": keys.Sigewinne, + "mavuika": keys.Mavuika, + "mav": keys.Mavuika, } diff --git a/pkg/simulation/imports_char_gen.go b/pkg/simulation/imports_char_gen.go index af8c6bc45..146d8db72 100644 --- a/pkg/simulation/imports_char_gen.go +++ b/pkg/simulation/imports_char_gen.go @@ -50,6 +50,7 @@ import ( _ "github.com/genshinsim/gcsim/internal/characters/lisa" _ "github.com/genshinsim/gcsim/internal/characters/lynette" _ "github.com/genshinsim/gcsim/internal/characters/lyney" + _ "github.com/genshinsim/gcsim/internal/characters/mavuika" _ "github.com/genshinsim/gcsim/internal/characters/mika" _ "github.com/genshinsim/gcsim/internal/characters/mona" _ "github.com/genshinsim/gcsim/internal/characters/mualani" diff --git a/ui/packages/db/src/Data/char_data.generated.json b/ui/packages/db/src/Data/char_data.generated.json index 9aa0c4251..6c9127296 100644 --- a/ui/packages/db/src/Data/char_data.generated.json +++ b/ui/packages/db/src/Data/char_data.generated.json @@ -996,6 +996,22 @@ }, "name_text_hash_map ": "2472444970" }, + "mavuika": { + "id": 10000106, + "key": "mavuika", + "rarity": "QUALITY_ORANGE", + "body": "BODY_LADY", + "region": "ASSOC_TYPE_NATLAN", + "element": "Fire", + "weapon_class": "WEAPON_CLAYMORE", + "icon_name": "UI_AvatarIcon_Mavuika", + "skill_details": { + "skill": 11062, + "burst": 11065, + "attack": 11061 + }, + "name_text_hash_map ": "113398050" + }, "mika": { "id": 10000080, "key": "mika", diff --git a/ui/packages/ui/src/Data/char_data.generated.json b/ui/packages/ui/src/Data/char_data.generated.json index 9aa0c4251..6c9127296 100644 --- a/ui/packages/ui/src/Data/char_data.generated.json +++ b/ui/packages/ui/src/Data/char_data.generated.json @@ -996,6 +996,22 @@ }, "name_text_hash_map ": "2472444970" }, + "mavuika": { + "id": 10000106, + "key": "mavuika", + "rarity": "QUALITY_ORANGE", + "body": "BODY_LADY", + "region": "ASSOC_TYPE_NATLAN", + "element": "Fire", + "weapon_class": "WEAPON_CLAYMORE", + "icon_name": "UI_AvatarIcon_Mavuika", + "skill_details": { + "skill": 11062, + "burst": 11065, + "attack": 11061 + }, + "name_text_hash_map ": "113398050" + }, "mika": { "id": 10000080, "key": "mika",