diff --git a/internal/characters/jahoda/aimed.go b/internal/characters/jahoda/aimed.go new file mode 100644 index 0000000000..41d02b1f09 --- /dev/null +++ b/internal/characters/jahoda/aimed.go @@ -0,0 +1,97 @@ +package jahoda + +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/info" +) + +var aimedFrames [][]int + +var aimedHitmarks = []int{15, 85} // {Aim -> Dash, Aim -> Jump} + +func init() { + aimedFrames = make([][]int, 3) + + // Aimed Shot + aimedFrames[0] = frames.InitAbilSlice(aimedHitmarks[0]) + + // Fully-Charged Aimed Shot + aimedFrames[1] = frames.InitAbilSlice(aimedHitmarks[1]) +} + +func (c *char) Aimed(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return action.Info{ + Frames: frames.NewAbilFunc(skillCancelFrames), + AnimationLength: skillCancelFrames[action.InvalidAction], + CanQueueAfter: skillCancelFrames[action.ActionDash], // earliest cancel + State: action.SkillState, + }, nil + } + + hold, ok := p["hold"] + if !ok { + hold = attacks.AimParamLv1 + } + + switch hold { + case attacks.AimParamPhys: + case attacks.AimParamLv1: + default: + return action.Info{}, fmt.Errorf("invalid hold param supplied, got %v", hold) + } + travel, ok := p["travel"] + if !ok { + travel = 10 + } + weakspot := p["weakspot"] + + ai := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Fully-Charged Aimed Shot", + AttackTag: attacks.AttackTagExtra, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypePierce, + Element: attributes.Anemo, + 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(), + info.Point{Y: -0.5}, + 0.1, + 1, + ), + aimedHitmarks[hold], + aimedHitmarks[hold]+travel, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(aimedFrames[hold]), + AnimationLength: aimedFrames[hold][action.InvalidAction], + CanQueueAfter: aimedHitmarks[hold], + State: action.AimState, + }, nil +} diff --git a/internal/characters/jahoda/asc.go b/internal/characters/jahoda/asc.go new file mode 100644 index 0000000000..47b28f0c2a --- /dev/null +++ b/internal/characters/jahoda/asc.go @@ -0,0 +1,117 @@ +package jahoda + +import ( + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/glog" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +func (c *char) a1Init() { + eleCountMap := c.countElements() + + priority := []attributes.Element{ + attributes.Pyro, + attributes.Hydro, + attributes.Electro, + attributes.Cryo, + } + + highestEleCount := 0 + + for _, ele := range priority { + if eleCountMap[ele] > highestEleCount { + highestEleCount = eleCountMap[ele] + c.a1HighestEle = ele + } + } + + if highestEleCount == 0 { + c.a1HighestEle = attributes.NoElement + } + + if c.Base.Cons >= 2 && c.Core.Player.GetMoonsignLevel() > 2 { + secondHighestEleCount := 0 + + for _, ele := range priority { + if ele == c.a1HighestEle { + continue + } + + if eleCountMap[ele] > secondHighestEleCount { + secondHighestEleCount = eleCountMap[ele] + c.c2NextHighestEle = ele + } + } + + if secondHighestEleCount == 0 { + c.c2NextHighestEle = attributes.NoElement + } + } +} + +func (c *char) a1() { + if c.Base.Ascension < 1 { + return + } + + c.applyA1Buff(c.a1HighestEle) + + if c.Base.Cons >= 2 && c.Core.Player.GetMoonsignLevel() > 2 { + c.applyA1Buff(c.c2NextHighestEle) + } +} + +func (c *char) countElements() map[attributes.Element]int { + count := map[attributes.Element]int{ + attributes.Pyro: 0, + attributes.Hydro: 0, + attributes.Electro: 0, + attributes.Cryo: 0, + } + + for _, ch := range c.Core.Player.Chars() { + if ch == nil { + continue + } + + switch ch.Base.Element { + case attributes.Pyro, + attributes.Hydro, + attributes.Electro, + attributes.Cryo: + count[ch.Base.Element]++ + } + } + + return count +} + +func (c *char) applyA1Buff(ele attributes.Element) { + switch ele { + case attributes.Pyro: + c.robotAi.FlatDmg *= 1.3 + case attributes.Hydro: + c.robotHi.Src *= 1.2 + case attributes.Electro: + c.robotCount += 1 + case attributes.Cryo: + c.robotHitmarkInterval *= 0.9 + } +} + +func (c *char) a4() { + if c.Base.Ascension < 4 { + return + } + + c.Core.Player.ActiveChar().AddStatMod(character.StatMod{ + Base: modifier.NewBase("jahoda-a4", 6*60), + AffectedStat: attributes.EM, + Amount: func() ([]float64, bool) { + return c.a4Buff, true + }, + }) + + c.Core.Log.NewEvent("jahoda a4 triggered", glog.LogCharacterEvent, c.Index()).Write("em snapshot", c.a4Buff[attributes.EM]).Write("expiry", c.Core.F+6*60) +} diff --git a/internal/characters/jahoda/attack.go b/internal/characters/jahoda/attack.go new file mode 100644 index 0000000000..b75f2a282e --- /dev/null +++ b/internal/characters/jahoda/attack.go @@ -0,0 +1,85 @@ +package jahoda + +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/info" +) + +var ( + attackFrames [][]int + attackHitmarks = [][]int{{14}, {15, 29}, {40}} +) + +const normalHitNum = 3 + +func init() { + // NA cancels + attackFrames = make([][]int, normalHitNum) + + attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 35) // N1 -> Walk + attackFrames[0][action.ActionAttack] = 30 + attackFrames[0][action.ActionAim] = 30 + + attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1][1], 52) // N2 -> Walk + attackFrames[1][action.ActionAttack] = 48 + attackFrames[1][action.ActionAim] = 47 + + attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2][0], 99) // N3 -> Walk + attackFrames[2][action.ActionAttack] = 88 + attackFrames[2][action.ActionAim] = 89 +} + +func (c *char) Attack(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return action.Info{ + Frames: frames.NewAbilFunc(skillCancelFrames), + AnimationLength: skillCancelFrames[action.InvalidAction], + CanQueueAfter: skillCancelFrames[action.ActionDash], // earliest cancel + State: action.SkillState, + }, nil + } + + travel, ok := p["travel"] + if !ok { + travel = 10 + } + + for i, mult := range attack[c.NormalCounter] { + ai := info.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, + Mult: mult[c.TalentLvlAttack()], + HitlagFactor: 0.01, + } + + ap := combat.NewBoxHit(c.Core.Combat.Player(), c.Core.Combat.PrimaryTarget(), info.Point{Y: -0.5}, 0.1, 1) + c.Core.QueueAttack( + ai, + ap, + attackHitmarks[c.NormalCounter][i], + attackHitmarks[c.NormalCounter][i]+travel, + ) + } + + 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 +} diff --git a/internal/characters/jahoda/burst.go b/internal/characters/jahoda/burst.go new file mode 100644 index 0000000000..cb5c7b646d --- /dev/null +++ b/internal/characters/jahoda/burst.go @@ -0,0 +1,262 @@ +package jahoda + +import ( + "errors" + "sort" + + "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/glog" + "github.com/genshinsim/gcsim/pkg/core/info" +) + +var burstFrames []int + +const ( + burstDuration = 790 + firstAbsorbtionDelay = 6 + absorptionInterval = 41 + firstrobotHitmark = 45 + robotDelay = 94 + firsHealTickDelay = 12 + healInterval = 87 + burstCD = 18 * 60 + burstKey = "jahoda-burst-dot" +) + +func init() { + burstFrames = frames.InitAbilSlice(48) // Q -> N1 + burstFrames[action.ActionSkill] = 53 // Q -> Skill + burstFrames[action.ActionAim] = 55 // Q -> Aim + burstFrames[action.ActionDash] = 54 // Q -> D + burstFrames[action.ActionJump] = 54 // Q -> J + burstFrames[action.ActionWalk] = 55 // Q -> W + burstFrames[action.ActionSwap] = 36 // Q -> Swap +} + +func (c *char) Burst(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + return action.Info{}, errors.New("burst called in skill state") + } + + c.robotHitmarkInterval = 140 + c.burstSrc = c.Core.F + src := c.burstSrc + c.burstAbsorbCheckLocation = combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 1.2) // Couldn't find anywhere in dm, assume top be the same as Sayu + + c.AddStatus(burstKey, burstDuration, false) + + // Initial hit damage + ai := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Hidden Aces: Seven Tools of the Hunter", + AttackTag: attacks.AttackTagElementalBurst, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.Anemo, + Durability: 25, + Mult: burst[c.TalentLvlBurst()], + } + + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.Player(), info.Point{Y: 1}, 5), + 0, + 0) + + // Define Info + c.robotAi = info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Purrsonal Coordinated Assistance Robot DMG", + AttackTag: attacks.AttackTagElementalBurst, + ICDTag: attacks.ICDTagElementalBurst, + ICDGroup: attacks.ICDGroupJahodaBurst, // special icd, 15s/4 hits + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.NoElement, + Durability: 25, + FlatDmg: burstSkill[c.TalentLvlBurst()] * c.TotalAtk(), + } + + heal := burstHealFlat[c.TalentLvlBurst()] + burstHealPP[c.TalentLvlBurst()]*c.TotalAtk() + c.robotHi = info.HealInfo{ + Caller: c.Index(), + Target: c.Core.Player.Active(), + Message: "Purrsonal Coordinated Assistance Robot Healing", + Src: heal, + Bonus: c.Stat(attributes.Heal), + } + + c.robotCount = 2 + + // Apply A1 buff + c.a1() + + // Heal ticks + c.QueueCharTask(func() { + for i := 0; i < burstDuration-firsHealTickDelay; i += healInterval { + c.Core.Tasks.Add(func() { + if src != c.burstSrc { + return + } + + c.Core.Player.Heal(c.robotHi) + + if c.Core.Player.ActiveChar().CurrentHPRatio() > 0.7 { + c.a4() + + low := c.lowestHPChar() + if low >= 0 { + healOffField := burstAdditionalHealFlat[c.TalentLvlBurst()] + burstAdditionalHealPP[c.TalentLvlBurst()]*c.TotalAtk() + + c.Core.Player.Heal(info.HealInfo{ + Caller: c.Index(), + Target: low, + Message: "Additional Healing", + Src: healOffField, + Bonus: c.Stat(attributes.Heal), + }) + } + + } + }, i) + } + }, firsHealTickDelay) + + // Dmg ticks + if c.Core.Player.GetMoonsignLevel() >= 2 { + c.Core.Tasks.Add(c.absorbCheck(src, 0, burstDuration/absorptionInterval), firstAbsorbtionDelay+firstrobotHitmark) + } + + c.SetCDWithDelay(action.ActionBurst, burstCD, 1) + c.ConsumeEnergy(13) + + return action.Info{ + Frames: frames.NewAbilFunc(burstFrames), + AnimationLength: burstFrames[action.InvalidAction], + CanQueueAfter: burstFrames[action.ActionSwap], // Earliest cancel + State: action.BurstState, + }, nil +} + +func (c *char) lowestHPChar() int { + lowestIdx := -1 + lowestPct := 2.0 // > 1 + + for i := 0; i < len(c.Core.Player.Chars()); i++ { + ch := c.Core.Player.Chars()[i] + if ch == nil { + continue + } + + if ch.CurrentHP() <= 0 { + continue + } + + if ch.CurrentHPRatio() < lowestPct { + lowestPct = ch.CurrentHPRatio() + lowestIdx = i + } + } + + return lowestIdx +} + +func (c *char) absorbCheck(src, count, maxcount int) func() { + return func() { + if src != c.burstSrc { + return + } + + if count == maxcount { + return + } + + c.robotAi.Element = c.Core.Combat.AbsorbCheck(c.Index(), c.burstAbsorbCheckLocation, attributes.Pyro, attributes.Hydro, attributes.Electro, attributes.Cryo) + if c.robotAi.Element != attributes.NoElement { + switch c.robotAi.Element { + case attributes.Pyro: + c.robotAi.ICDTag = attacks.ICDTagElementalBurstPyro + case attributes.Hydro: + c.robotAi.ICDTag = attacks.ICDTagElementalBurstHydro + case attributes.Electro: + c.robotAi.ICDTag = attacks.ICDTagElementalBurstElectro + case attributes.Cryo: + c.robotAi.ICDTag = attacks.ICDTagElementalBurstCryo + } + + c.Core.Log.NewEventBuildMsg(glog.LogCharacterEvent, c.Index(), + "jahoda burst absorbed ", c.robotAi.Element.String(), + ) + + c.c4() + + for i := 0; i < burstDuration-firstrobotHitmark; i += int(c.robotHitmarkInterval) { + c.Core.Tasks.Add(c.robotAtkTick(src), i) + } + + return + } + c.Core.Tasks.Add(c.absorbCheck(src, count+1, maxcount), absorptionInterval) + } +} + +func (c *char) robotAtkTick(src int) func() { + return func() { + if src != c.burstSrc { + return + } + + // For each robot, trigger an instance of damage on 3 closest enemies + for i := 0; i < c.robotCount; i++ { + c.queueOn3Closest(c.Core.Combat.Player().Pos(), c.robotAi, robotDelay*i) + } + } +} + +// Helper to sort 3 closest enemies and attack them simultaneously +func (c *char) queueOn3Closest(origin info.Point, ai info.AttackInfo, hitDelay int) { + enemies := c.Core.Combat.Enemies() + type cand struct { + t info.Target + d float64 + } + cands := make([]cand, 0, len(enemies)) + + // Compute distance + for _, e := range enemies { + if e == nil { + continue + } + if e.Type() != info.TargettableEnemy { + continue + } + if !e.IsAlive() { + continue + } + + p := e.Pos() + dx := p.X - origin.X + dy := p.Y - origin.Y + d := dx*dx + dy*dy // squared distance is enough for sorting (no sqrt) + + cands = append(cands, cand{t: e, d: d}) + } + + // sort + sort.Slice(cands, func(i, j int) bool { return cands[i].d < cands[j].d }) + + // queue on up to 3 + n := 3 + if len(cands) < n { + n = len(cands) + } + for i := 0; i < n; i++ { + t := cands[i].t + ap := combat.NewCircleHitOnTarget(t, nil, 1.2) // Couldn't find anywhere in dm + c.Core.QueueAttack(ai, ap, hitDelay, hitDelay) + } +} diff --git a/internal/characters/jahoda/config.yml b/internal/characters/jahoda/config.yml new file mode 100644 index 0000000000..973e17693a --- /dev/null +++ b/internal/characters/jahoda/config.yml @@ -0,0 +1,57 @@ +package_name: jahoda +genshin_id: 10000124 +key: jahoda +action_param_keys: + attack: + - param: "travel" + aim: + - param: "hold" + - param: "travel" + - param: "weakspot" + skill: + - param: "travel" +icd_groups: + - group_name: ICDGroupJahodaBurst + reset_timer: 900 + ele_app_sequence: [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0] + damage_sequence: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + - group_name: ICDGroupJahodaCons + reset_timer: 900 + ele_app_sequence: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] + damage_sequence: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +icd_tags: + - ICDTagJahodaCons +skill_data_mapping: + attack: + attack_1: + - 0 # 1-Hit DMG|{param0:F1P} + attack_2: + - 1 # 2-Hit DMG|{param1:F1P} + - 1 # 2-Hit DMG|{param1:F1P} + attack_3: + - 2 # 3-Hit DMG|{param2:F1P} + aim: + - 3 # Aimed Shot|{param3:F1P} + fullaim: + - 4 # Fully-Charged Aimed Shot|{param4:P} + skill: + unfilledFlask: + - 1 # Unfilled Treasure Flask DMG|{param1:F1P} + filledFlask: + - 2 # Filled Treasure Flask DMG|{param2:F1P} + meowball: + - 4 # Meowball DMG|{param4:F1P} + burst: + burst: + - 0 # Skill DMG|{param0:F1P} + burstSkill: + - 1 # Purrsonal Coordinated Assistance Robot DMG|{param1:F1P} + burstHealPP: + - 3 # Purrsonal Coordinated Assistance Robot Healing|{param3:F1P} ATK+{param4:I} + burstHealFlat: + - 4 # Purrsonal Coordinated Assistance Robot Healing|{param3:F1P} ATK+{param4:I} + burstAdditionalHealPP: + - 5 # Lowest HP Character Additional Healing|{param5:F1P} ATK+{param6:I} + burstAdditionalHealFlat: + - 6 # Lowest HP Character Additional Healing|{param5:F1P} ATK+{param6:I} + diff --git a/internal/characters/jahoda/cons.go b/internal/characters/jahoda/cons.go new file mode 100644 index 0000000000..de6e400b21 --- /dev/null +++ b/internal/characters/jahoda/cons.go @@ -0,0 +1,52 @@ +package jahoda + +import ( + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/glog" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +const ( + c4Key = "jahoda-c4-flat-energy" + c6Key = "jahoda-c6" +) + +func (c *char) c4() { + if c.Base.Cons < 4 { + return + } + + c.AddEnergy(c4Key, 4) +} + +func (c *char) c6() { + if c.Base.Cons < 6 { + return + } + + c.c6Buff = make([]float64, attributes.EndStatType) + c.c6Buff = make([]float64, attributes.EndStatType) + + c.c6Buff[attributes.CR] = 0.05 + c.c6Buff[attributes.CD] = 0.40 + + for _, char := range c.Core.Player.Chars() { + char.AddStatMod(character.StatMod{ + Base: modifier.NewBase(c6Key, 20*60), + AffectedStat: attributes.CR, + Amount: func() ([]float64, bool) { + if char.Moonsign < 1 { + return nil, false + } + return c.c6Buff, true + }, + }) + + c.Core.Log.NewEvent("jahoda c6 triggered", glog.LogCharacterEvent, c.Index()). + Write("cr", c.c6Buff[attributes.CR]). + Write("cd", c.c6Buff[attributes.CD]). + Write("expiry", c.Core.F+20*60) + + } +} diff --git a/internal/characters/jahoda/dash.go b/internal/characters/jahoda/dash.go new file mode 100644 index 0000000000..f2f9a03e48 --- /dev/null +++ b/internal/characters/jahoda/dash.go @@ -0,0 +1,21 @@ +package jahoda + +import ( + "github.com/genshinsim/gcsim/internal/frames" + "github.com/genshinsim/gcsim/pkg/core/action" +) + +func init() { +} + +func (c *char) Dash(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return action.Info{ + Frames: frames.NewAbilFunc(skillCancelFrames), + AnimationLength: skillCancelFrames[action.InvalidAction], + CanQueueAfter: skillCancelFrames[action.ActionDash], // earliest cancel + }, nil + } + return c.Character.Dash(p) +} diff --git a/internal/characters/jahoda/data_gen.textproto b/internal/characters/jahoda/data_gen.textproto new file mode 100644 index 0000000000..e5517626b2 --- /dev/null +++ b/internal/characters/jahoda/data_gen.textproto @@ -0,0 +1,151 @@ +id: 10000124 +key: "jahoda" +rarity: QUALITY_PURPLE +body: BODY_GIRL +region: ASSOC_TYPE_NODKRAI +element: Wind +weapon_class: WEAPON_BOW +icon_name: "UI_AvatarIcon_Jahoda" +stats: { + base_hp: 808.7586 + base_atk: 18.6984 + base_def: 48.64125 + hp_curve: GROW_CURVE_HP_S4 + atk_curve: GROW_CURVE_ATTACK_S4 + def_cruve: GROW_CURVE_HP_S4 + 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_HEAL_ADD + } + } + promo_data: { + max_level: 40 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 604.1841 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 36.3375 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 13.96899 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + } + } + promo_data: { + max_level: 50 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 1033.4728 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 62.15625 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 23.894325 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + value: 0.0462 + } + } + promo_data: { + max_level: 60 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 1605.8577 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 96.58125 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 37.128105 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + value: 0.0923 + } + } + promo_data: { + max_level: 70 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2035.1465 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 122.4 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 47.05344 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + value: 0.0923 + } + } + promo_data: { + max_level: 80 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2464.435 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 148.21875 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 56.978775 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + value: 0.1385 + } + } + promo_data: { + max_level: 90 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2893.7239 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 174.0375 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 66.90411 + } + add_props: { + prop_type: FIGHT_PROP_HEAL_ADD + value: 0.1846 + } + } +} +skill_details: { + skill: 11242 + burst: 11245 + attack: 11241 + burst_energy_cost: 70 +} +name_text_hash_map: 1438964522 diff --git a/internal/characters/jahoda/jahoda.go b/internal/characters/jahoda/jahoda.go new file mode 100644 index 0000000000..e056f05b1a --- /dev/null +++ b/internal/characters/jahoda/jahoda.go @@ -0,0 +1,79 @@ +package jahoda + +import ( + tmpl "github.com/genshinsim/gcsim/internal/template/character" + "github.com/genshinsim/gcsim/pkg/core" + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/core/keys" + "github.com/genshinsim/gcsim/pkg/core/player/character" +) + +func init() { + core.RegisterCharFunc(keys.Jahoda, NewChar) +} + +type char struct { + *tmpl.Character + flaskAbsorbCheckLocation info.AttackPattern + flaskAbsorb attributes.Element + flaskGauge int + flaskGaugeMax int + pursuitDuration int + skillSrc int + skillTravel int + burstAbsorbCheckLocation info.AttackPattern + burstSrc int + a1HighestEle attributes.Element + robotAi info.AttackInfo + robotHi info.HealInfo + robotCount int + robotHitmarkInterval float64 + c2NextHighestEle attributes.Element + a4Buff []float64 + c6Buff []float64 +} + +func NewChar(s *core.Core, w *character.CharWrapper, _ info.CharacterProfile) error { + c := char{} + c.Character = tmpl.NewWithWrapper(s, w) + + c.EnergyMax = 70 + c.NormalHitNum = normalHitNum + c.BurstCon = 3 + c.SkillCon = 5 + + c.Moonsign = 1 + c.HasArkhe = false + + c.flaskAbsorb = attributes.NoElement + c.flaskGauge = 0 + c.flaskGaugeMax = 100 + + c.a1HighestEle = attributes.NoElement + + c.c2NextHighestEle = attributes.NoElement + + w.Character = &c + + return nil +} + +func (c *char) Init() error { + c.a1Init() + + c.a4Buff = make([]float64, attributes.EndStatType) + c.a4Buff[attributes.EM] = 100 + + return nil +} + +func (c *char) AnimationStartDelay(k info.AnimationDelayKey) int { + if k == info.AnimationXingqiuN0StartDelay { + return 13 + } + if k == info.AnimationYelanN0StartDelay { + return 11 + } + return c.Character.AnimationStartDelay(k) +} diff --git a/internal/characters/jahoda/jahoda_gen.go b/internal/characters/jahoda/jahoda_gen.go new file mode 100644 index 0000000000..be38b38d41 --- /dev/null +++ b/internal/characters/jahoda/jahoda_gen.go @@ -0,0 +1,332 @@ +// Code generated by "pipeline"; DO NOT EDIT. +package jahoda + +import ( + _ "embed" + + "fmt" + "slices" + + "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" +) + +//go:embed data_gen.textproto +var pbData []byte +var base *model.AvatarData +var paramKeysValidation = map[action.Action][]string{ + 1: {"travel"}, + 3: {"travel"}, + 7: {"hold", "travel", "weakspot"}, +} + +func init() { + base = &model.AvatarData{} + err := prototext.Unmarshal(pbData, base) + if err != nil { + panic(err) + } + validation.RegisterCharParamValidationFunc(keys.Jahoda, 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}, + } +) + +var ( + // attack: aim = [3] + aim = []float64{ + 0.4386, + 0.4743, + 0.51, + 0.561, + 0.5967, + 0.6375, + 0.6936, + 0.7497, + 0.8058, + 0.867, + 0.9282, + 0.9894, + 1.0506, + 1.1118, + 1.173, + } + // attack: attack_1 = [0] + attack_1 = []float64{ + 0.416739, + 0.450659, + 0.48458, + 0.533038, + 0.566959, + 0.605725, + 0.659029, + 0.712333, + 0.765636, + 0.823786, + 0.881936, + 0.940085, + 0.998235, + 1.056384, + 1.114534, + } + // attack: attack_2 = [1 1] + attack_2 = [][]float64{ + { + 0.192313, + 0.207967, + 0.22362, + 0.245982, + 0.261635, + 0.279525, + 0.304123, + 0.328721, + 0.35332, + 0.380154, + 0.406988, + 0.433823, + 0.460657, + 0.487492, + 0.514326, + }, + { + 0.192313, + 0.207967, + 0.22362, + 0.245982, + 0.261635, + 0.279525, + 0.304123, + 0.328721, + 0.35332, + 0.380154, + 0.406988, + 0.433823, + 0.460657, + 0.487492, + 0.514326, + }, + } + // attack: attack_3 = [2] + attack_3 = []float64{ + 0.511975, + 0.553648, + 0.59532, + 0.654852, + 0.696524, + 0.74415, + 0.809635, + 0.87512, + 0.940606, + 1.012044, + 1.083482, + 1.154921, + 1.226359, + 1.297798, + 1.369236, + } + // attack: fullaim = [4] + fullaim = []float64{ + 1.24, + 1.333, + 1.426, + 1.55, + 1.643, + 1.736, + 1.86, + 1.984, + 2.108, + 2.232, + 2.356, + 2.48, + 2.635, + 2.79, + 2.945, + } + // skill: filledFlask = [2] + filledFlask = []float64{ + 2.12, + 2.279, + 2.438, + 2.65, + 2.809, + 2.968, + 3.18, + 3.392, + 3.604, + 3.816, + 4.028, + 4.24, + 4.505, + 4.77, + 5.035, + } + // skill: meowball = [4] + meowball = []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, + } + // skill: unfilledFlask = [1] + unfilledFlask = []float64{ + 1.908, + 2.0511, + 2.1942, + 2.385, + 2.5281, + 2.6712, + 2.862, + 3.0528, + 3.2436, + 3.4344, + 3.6252, + 3.816, + 4.0545, + 4.293, + 4.5315, + } + // burst: burst = [0] + burst = []float64{ + 2.072, + 2.2274, + 2.3828, + 2.59, + 2.7454, + 2.9008, + 3.108, + 3.3152, + 3.5224, + 3.7296, + 3.9368, + 4.144, + 4.403, + 4.662, + 4.921, + } + // burst: burstAdditionalHealFlat = [6] + burstAdditionalHealFlat = []float64{ + 192.59721, + 211.8596, + 232.7272, + 255.2, + 279.27798, + 304.96118, + 332.2496, + 361.1432, + 391.642, + 423.74597, + 457.45517, + 492.76956, + 529.68915, + 568.214, + 608.34393, + } + // burst: burstAdditionalHealPP = [5] + burstAdditionalHealPP = []float64{ + 0.3072, + 0.33024, + 0.35328, + 0.384, + 0.40704, + 0.43008, + 0.4608, + 0.49152, + 0.52224, + 0.55296, + 0.58368, + 0.6144, + 0.6528, + 0.6912, + 0.7296, + } + // burst: burstHealFlat = [4] + burstHealFlat = []float64{ + 500.73764, + 550.81836, + 605.0725, + 663.5, + 726.1009, + 792.8752, + 863.8229, + 938.944, + 1018.23846, + 1101.7063, + 1189.3477, + 1281.1622, + 1377.1503, + 1477.3118, + 1581.6466, + } + // burst: burstHealPP = [3] + burstHealPP = []float64{ + 0.79872, + 0.858624, + 0.918528, + 0.9984, + 1.058304, + 1.118208, + 1.19808, + 1.277952, + 1.357824, + 1.437696, + 1.517568, + 1.59744, + 1.69728, + 1.79712, + 1.89696, + } + // burst: burstSkill = [1] + burstSkill = []float64{ + 0.172664, + 0.185614, + 0.198564, + 0.21583, + 0.22878, + 0.24173, + 0.258996, + 0.276262, + 0.293529, + 0.310795, + 0.328062, + 0.345328, + 0.366911, + 0.388494, + 0.410077, + } +) diff --git a/internal/characters/jahoda/jump.go b/internal/characters/jahoda/jump.go new file mode 100644 index 0000000000..9dfbe14ad1 --- /dev/null +++ b/internal/characters/jahoda/jump.go @@ -0,0 +1,22 @@ +package jahoda + +import ( + "github.com/genshinsim/gcsim/internal/frames" + "github.com/genshinsim/gcsim/pkg/core/action" +) + +func init() { +} + +func (c *char) Jump(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return action.Info{ + Frames: frames.NewAbilFunc(skillCancelFrames), + AnimationLength: skillCancelFrames[action.InvalidAction], + CanQueueAfter: skillCancelFrames[action.ActionDash], // earliest cancel + State: action.SkillState, + }, nil + } + return c.Character.Jump(p) +} diff --git a/internal/characters/jahoda/skill.go b/internal/characters/jahoda/skill.go new file mode 100644 index 0000000000..82cbf7cefc --- /dev/null +++ b/internal/characters/jahoda/skill.go @@ -0,0 +1,300 @@ +package jahoda + +import ( + "math" + + "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/glog" + "github.com/genshinsim/gcsim/pkg/core/info" +) + +var ( + skillFrames []int + skillCancelFrames []int +) + +const ( + skillWindup = 30 + shadowPursuitMaxDuration = 334 + firstFillFlaskDelay = 19 + fillFlaskInterval = 29 + drainFlask = 22 + unfillHitmark = 4 + fillHitmark = 2 + firstMeowballFirstHitmark = 129 + meowballHitmarkInterval = 116 + skillCD = 15 * 60 + c1BounceHitmark = 32 + shadowPursuitKey = "jahoda-shadow-pursuit" + meowballKey = "jahoda-meowball" + meowballFlatEnergyKey = "jahoda-meowball-flat-energy" + meowballFlatEnergyICDKey = "jahoda-meowball-flat-energy-icd" + particleICDKey = "jahoda-particle-icd" +) + +func init() { + skillFrames = frames.InitAbilSlice(12 + skillWindup) // E -> E + + skillCancelFrames = frames.InitAbilSlice(43) // E -> E -> N1 + skillCancelFrames[action.ActionAim] = 42 // E -> E -> Aim + skillCancelFrames[action.ActionBurst] = 42 // E -> E -> Q + skillCancelFrames[action.ActionDash] = 41 // E -> E -> D + skillCancelFrames[action.ActionJump] = 48 // E -> E -> J + skillCancelFrames[action.ActionWalk] = 44 // E -> E -> W + skillCancelFrames[action.ActionSwap] = 41 // E -> E -> Swap +} + +func (c *char) Skill(p map[string]int) (action.Info, error) { + if c.StatusIsActive(shadowPursuitKey) { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return action.Info{ + Frames: frames.NewAbilFunc(skillCancelFrames), + AnimationLength: skillCancelFrames[action.InvalidAction], + CanQueueAfter: skillCancelFrames[action.ActionDash], // earliest cancel + State: action.SkillState, + }, nil + } + + c.Core.Player.SwapCD = math.MaxInt16 + travel, ok := p["travel"] + if !ok { + travel = 13 + } + + c.skillTravel = travel + + c.skillSrc = c.Core.F + c.flaskAbsorb = attributes.NoElement + c.flaskGauge = 0 + c.flaskGaugeMax = 100 + c.flaskAbsorbCheckLocation = combat.NewCircleHitOnTarget(c.Core.Combat.Player(), nil, 4) + + // Enter shadow pursuit + c.pursuitDuration = shadowPursuitMaxDuration + c.Core.Tasks.Add(func() { + c.AddStatus(shadowPursuitKey, shadowPursuitMaxDuration, false) + c.Core.Player.SwapCD = math.MaxInt16 + c.Core.Tasks.Add( + c.fillFlask(c.skillSrc), + firstFillFlaskDelay, + ) + }, skillWindup) + + return action.Info{ + Frames: frames.NewAbilFunc(skillFrames), + AnimationLength: skillFrames[action.InvalidAction], + CanQueueAfter: skillFrames[action.ActionSkill], + State: action.SkillState, + }, nil +} + +func (c *char) ParticleCB(a info.AttackCB) { + if a.Target.Type() != info.TargettableEnemy { + return + } + if c.StatusIsActive(particleICDKey) { + return + } + c.AddStatus(particleICDKey, 0.5*60, false) // Couldn't find anywhere in dm, assume to be the same as Sayu + c.Core.QueueParticle(c.Base.Key.String(), 4, attributes.Anemo, c.ParticleDelay) +} + +func (c *char) meowballEnergyCB(a info.AttackCB) { + if a.Target.Type() != info.TargettableEnemy { + return + } + + if c.StatusIsActive(meowballFlatEnergyICDKey) { + return + } + + c.AddEnergy(meowballFlatEnergyKey, 2) + c.AddStatus(meowballFlatEnergyICDKey, int(3.5*60), true) +} + +func (c *char) fillFlask(src int) func() { + return func() { + if src != c.skillSrc { + return + } + + c.pursuitDuration = c.Core.F - c.skillSrc + + // If the flask is full OR the max duration of the state is reached, drain the flask + if c.flaskGauge >= c.flaskGaugeMax || !c.StatusIsActive(shadowPursuitKey) || c.Core.F >= shadowPursuitMaxDuration+c.skillSrc { + c.Core.Tasks.Add(c.drainFlask(c.skillSrc), 0) + return + } + + // Check elemental aura in the area + objectElem := c.Core.Combat.AbsorbCheck(c.Index(), c.flaskAbsorbCheckLocation, attributes.Pyro, attributes.Hydro, attributes.Electro, attributes.Cryo) + if objectElem != attributes.NoElement { + if c.flaskAbsorb == attributes.NoElement { + // If there are an element to absorb AND the flask has not absorbed any elements, the flask absorb that element and increase its gauge + c.flaskAbsorb = objectElem + c.Core.Tasks.Add(c.changeFlaskGauge(c.flaskGaugeMax/4), 0) + } else if objectElem == c.flaskAbsorb { + // Else if there are an element to absorb AND if that element is the same as that the flask has absorbed the flask absorb that element + // and increase its gauge + c.Core.Tasks.Add(c.changeFlaskGauge(c.flaskGaugeMax/4), 0) + } + // Otherwise nothing happend since the flask will not change its elemental absorption mid way + + c.Core.Log.NewEventBuildMsg(glog.LogCharacterEvent, c.Index(), + "jahoda flask absorbed ", c.flaskAbsorb.String(), + ) + } + c.Core.Tasks.Add(c.fillFlask(c.skillSrc), fillFlaskInterval) + } +} + +func (c *char) changeFlaskGauge(amount int) func() { + return func() { + prevFlaskGauge := c.flaskGauge + c.flaskGauge += amount + c.Core.Log.NewEvent("Flask Gauge Change", glog.LogCharacterEvent, c.Index()). + Write("previous flask gauge", prevFlaskGauge). + Write("current flask gauge", c.flaskGauge) + } +} + +func (c *char) drainFlask(src int) func() { + return func() { + if src != c.skillSrc { + return + } + + c.cancelPursuit() // Exit state + + if c.flaskGauge >= c.flaskGaugeMax { + c.flaskGauge = c.flaskGaugeMax + + // If the flask is full, do filled flask damage + ai := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Filled Treasure Flask", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagElementalArt, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.Anemo, + Durability: 25, + Mult: filledFlask[c.TalentLvlSkill()], + } + + c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), info.Point{Y: 2.5}, 5), 0, drainFlask+fillHitmark, c.ParticleCB) + + // If in Ascendent Gleam, do meowball damage + if c.Core.Player.GetMoonsignLevel() >= 2 { + c.c6() // Apply buff from C6 + + ticks := c.flaskGaugeMax / 10 + + for i := range ticks { + c.Core.Tasks.Add( + c.meowballTick(c.skillSrc), + firstMeowballFirstHitmark+i*meowballHitmarkInterval, + ) + c.Core.Tasks.Add(c.changeFlaskGauge(-10), firstMeowballFirstHitmark+i*meowballHitmarkInterval) + } + + c.AddStatus( + meowballKey, + firstMeowballFirstHitmark+(ticks-1)*meowballHitmarkInterval, + false, + ) + } + + } else { + // If the flask is not full (early cancel or no furation expired), do unfill damage + ai := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Unfilled Treasure Flask", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagElementalArt, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.Anemo, + Durability: 25, + Mult: unfilledFlask[c.TalentLvlSkill()], + } + + c.Core.QueueAttack(ai, combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), info.Point{Y: 2.5}, 5), 0, unfillHitmark, c.ParticleCB) + } + } +} + +func (c *char) cancelPursuit() { + if !c.StatusIsActive(shadowPursuitKey) { + return + } + c.SetCD(action.ActionSkill, skillCD+skillWindup) + c.DeleteStatus(shadowPursuitKey) + c.Core.Player.SwapCD = skillCancelFrames[action.ActionSwap] + c.skillSrc = -1 +} + +func (c *char) meowballTick(src int) func() { + return func() { + if src != c.skillSrc { + return + } + + if c.flaskGauge <= 0 { + return + } + + if c.flaskGauge < 0 { + c.flaskGauge = 0 + } + + ai := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Meowball", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: c.flaskAbsorb, + Durability: 25, + Mult: meowball[c.TalentLvlSkill()], + } + + c.Core.QueueAttack( + ai, + combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, 4), + 0, + c.skillTravel, + c.meowballEnergyCB, + ) + + // 50% hit twice + if c.Base.Cons >= 1 { + if c.Core.Rand.Float64() < 0.5 { + aiC1 := info.AttackInfo{ + ActorIndex: c.Index(), + Abil: "Meowball (C1)", + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagJahodaCons, + ICDGroup: attacks.ICDGroupJahodaCons, + StrikeType: attacks.StrikeTypeDefault, + Element: c.flaskAbsorb, + Durability: 25, + Mult: meowball[c.TalentLvlSkill()], + } + + c.Core.QueueAttack( + aiC1, + combat.NewCircleHitOnTarget(c.Core.Combat.PrimaryTarget(), nil, 4), + 0, + c.skillTravel+c1BounceHitmark, + nil, + ) + } + } + } +} diff --git a/internal/services/assets/avatars_gen.go b/internal/services/assets/avatars_gen.go index 65ec479d6d..a17a506a37 100644 --- a/internal/services/assets/avatars_gen.go +++ b/internal/services/assets/avatars_gen.go @@ -41,6 +41,7 @@ var avatarMap = map[string]string{ "heizou": "UI_AvatarIcon_Heizo", "hutao": "UI_AvatarIcon_Hutao", "itto": "UI_AvatarIcon_Itto", + "jahoda": "UI_AvatarIcon_Jahoda", "jean": "UI_AvatarIcon_Qin", "kaeya": "UI_AvatarIcon_Kaeya", "kaveh": "UI_AvatarIcon_Kaveh", diff --git a/pkg/core/attacks/icd_groups_gen.go b/pkg/core/attacks/icd_groups_gen.go index 56e9656c71..bce366e81c 100644 --- a/pkg/core/attacks/icd_groups_gen.go +++ b/pkg/core/attacks/icd_groups_gen.go @@ -27,6 +27,8 @@ const ( ICDGroupEscoffierSkill ICDGroupFischl ICDGroupFurinaSalonSolitaire + ICDGroupJahodaBurst + ICDGroupJahodaCons ICDGroupKinichLoopShot ICDGroupKinichScalespikerCannon ICDGroupLanyanRingAttack @@ -150,6 +152,14 @@ func init() { ICDGroupEleApplicationSequence[ICDGroupFurinaSalonSolitaire] = []float64{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0} ICDGroupDamageSequence[ICDGroupFurinaSalonSolitaire] = []float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + ICDGroupResetTimer[ICDGroupJahodaBurst] = 900 + ICDGroupEleApplicationSequence[ICDGroupJahodaBurst] = []float64{1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0} + ICDGroupDamageSequence[ICDGroupJahodaBurst] = []float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + + ICDGroupResetTimer[ICDGroupJahodaCons] = 900 + ICDGroupEleApplicationSequence[ICDGroupJahodaCons] = []float64{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0} + ICDGroupDamageSequence[ICDGroupJahodaCons] = []float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + ICDGroupResetTimer[ICDGroupKinichLoopShot] = 120 ICDGroupEleApplicationSequence[ICDGroupKinichLoopShot] = []float64{1, 0, 0, 0} ICDGroupDamageSequence[ICDGroupKinichLoopShot] = []float64{1, 1, 1, 1, 1, 1, 1, 1} diff --git a/pkg/core/attacks/icd_tags_gen.go b/pkg/core/attacks/icd_tags_gen.go index 179b7a2196..d1dcfcb747 100644 --- a/pkg/core/attacks/icd_tags_gen.go +++ b/pkg/core/attacks/icd_tags_gen.go @@ -19,6 +19,7 @@ const ( ICDTagEmilieLumidouce ICDTagFurinaChevalmarin ICDTagFurinaUsher + ICDTagJahodaCons ICDTagKinichLoopShot ICDTagKinichScalespikerCannon ICDTagKleeFireDamage diff --git a/pkg/core/keys/keys_char_gen.go b/pkg/core/keys/keys_char_gen.go index 34db090fc4..42c868136c 100644 --- a/pkg/core/keys/keys_char_gen.go +++ b/pkg/core/keys/keys_char_gen.go @@ -46,6 +46,7 @@ const ( Heizou Hutao Itto + Jahoda Jean Kaeya Kaveh @@ -264,6 +265,10 @@ func init() { charPrettyName[Itto] = "Itto" CharKeyToEle[Itto] = attributes.Geo + charNames[Jahoda] = "jahoda" + charPrettyName[Jahoda] = "Jahoda" + CharKeyToEle[Jahoda] = attributes.Anemo + charNames[Jean] = "jean" charPrettyName[Jean] = "Jean" CharKeyToEle[Jean] = attributes.Anemo diff --git a/pkg/core/player/player.go b/pkg/core/player/player.go index 01d6797a94..850f8a03cb 100644 --- a/pkg/core/player/player.go +++ b/pkg/core/player/player.go @@ -342,3 +342,11 @@ func (h *Handler) Airborne() AirborneSource { const ( XianyunAirborneBuff = "xianyun-airborne-buff" ) + +func (h *Handler) GetMoonsignLevel() int { + count := 0 + for _, c := range h.Chars() { + count += c.Moonsign + } + return count +} diff --git a/pkg/shortcut/characters.go b/pkg/shortcut/characters.go index ceaf33645e..de785c660b 100644 --- a/pkg/shortcut/characters.go +++ b/pkg/shortcut/characters.go @@ -62,6 +62,7 @@ var CharNameToKey = map[string]keys.Char{ "hutao": keys.Hutao, "tao": keys.Hutao, "ht": keys.Hutao, + "jahoda": keys.Jahoda, "jean": keys.Jean, "kaedeharakazuha": keys.Kazuha, "kazuha": keys.Kazuha, diff --git a/pkg/simulation/imports_char_gen.go b/pkg/simulation/imports_char_gen.go index c3d36ea340..4f04b772f0 100644 --- a/pkg/simulation/imports_char_gen.go +++ b/pkg/simulation/imports_char_gen.go @@ -41,6 +41,7 @@ import ( _ "github.com/genshinsim/gcsim/internal/characters/heizou" _ "github.com/genshinsim/gcsim/internal/characters/hutao" _ "github.com/genshinsim/gcsim/internal/characters/itto" + _ "github.com/genshinsim/gcsim/internal/characters/jahoda" _ "github.com/genshinsim/gcsim/internal/characters/jean" _ "github.com/genshinsim/gcsim/internal/characters/kaeya" _ "github.com/genshinsim/gcsim/internal/characters/kaveh" diff --git a/ui/packages/components/src/Editor/mode-gcsim.js b/ui/packages/components/src/Editor/mode-gcsim.js index f100edc3ff..3cc3f4b65c 100644 --- a/ui/packages/components/src/Editor/mode-gcsim.js +++ b/ui/packages/components/src/Editor/mode-gcsim.js @@ -148,6 +148,7 @@ ace.define( 'ht', 'hutao', 'itto', + 'jahoda', 'jean', 'kabukimono', 'kaedeharakazuha', diff --git a/ui/packages/db/src/Data/char_data.generated.json b/ui/packages/db/src/Data/char_data.generated.json index b002a6625f..6c3a806d2b 100644 --- a/ui/packages/db/src/Data/char_data.generated.json +++ b/ui/packages/db/src/Data/char_data.generated.json @@ -771,6 +771,23 @@ }, "name_text_hash_map ": "3068316954" }, + "jahoda": { + "id": 10000124, + "key": "jahoda", + "rarity": "QUALITY_PURPLE", + "body": "BODY_GIRL", + "region": "ASSOC_TYPE_NODKRAI", + "element": "Wind", + "weapon_class": "WEAPON_BOW", + "icon_name": "UI_AvatarIcon_Jahoda", + "skill_details": { + "skill": 11242, + "burst": 11245, + "attack": 11241, + "burst_energy_cost": 70 + }, + "name_text_hash_map ": "1438964522" + }, "jean": { "id": 10000003, "key": "jean", diff --git a/ui/packages/docs/docs/reference/characters/jahoda.md b/ui/packages/docs/docs/reference/characters/jahoda.md new file mode 100644 index 0000000000..cc8a6d087c --- /dev/null +++ b/ui/packages/docs/docs/reference/characters/jahoda.md @@ -0,0 +1,44 @@ +--- +title: Jahoda +--- + +import HitlagTable from "@site/src/components/Hitlag/HitlagTable"; +import FieldsTable from "@site/src/components/Fields/FieldsTable"; +import ParamsTable from "@site/src/components/Params/ParamsTable"; +import FramesTable from "@site/src/components/Frames/FramesTable"; +import IssuesTable from "@site/src/components/Issues/IssuesTable"; +import AoETable from "@site/src/components/AoE/AoETable"; +import NamesList from "@site/src/components/Names/NamesList"; +import ActionsTable from "@site/src/components/Actions/ActionsTable"; + +## Frames + + + +## Hitlag Data + + + +## AoE Data + + + +## Known issues + + + +## Names + + + +## Legal Actions + + + +## Params + + + +## Fields + + diff --git a/ui/packages/docs/src/components/Actions/character_data.json b/ui/packages/docs/src/components/Actions/character_data.json index dd59a9cb0c..a24ff070b8 100644 --- a/ui/packages/docs/src/components/Actions/character_data.json +++ b/ui/packages/docs/src/components/Actions/character_data.json @@ -1355,6 +1355,40 @@ "legal": true } ], + "jahoda": [ + { + "ability": "attack", + "legal": true + }, + { + "ability": "aim", + "legal": true + }, + { + "ability": "skill", + "legal": true + }, + { + "ability": "burst", + "notes": "Burst in Shadow Pursuit state is not legal." + }, + { + "ability": "dash", + "legal": true + }, + { + "ability": "jump", + "legal": true + }, + { + "ability": "walk", + "legal": true + }, + { + "ability": "swap", + "notes": "Swapping in Shadow Pursuit state is not legal." + } + ], "jean": [ { "ability": "attack", diff --git a/ui/packages/docs/src/components/AoE/character_data.json b/ui/packages/docs/src/components/AoE/character_data.json index 082345319f..8e0a448f9c 100644 --- a/ui/packages/docs/src/components/AoE/character_data.json +++ b/ui/packages/docs/src/components/AoE/character_data.json @@ -4436,6 +4436,135 @@ } ] }, + "jahoda": { + "cons": [ + { + "ability": "C1-Bounce", + "shape": "Circle", + "center": "PrimaryTarget", + "radius": 4 + } + ], + "normal": [ + { + "ability": "N1", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1 + }, + { + "ability": "N2-1", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1 + }, + { + "ability": "N2-2", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1 + }, + { + "ability": "N3", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1 + } + ], + "aim": [ + { + "ability": "Aim", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1.0 + }, + { + "ability": "Aim-Head", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1.0 + }, + { + "ability": "FullAim", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1.0 + }, + { + "ability": "FullAim-Head", + "shape": "Box", + "center": "PrimaryTarget", + "offsetY": -0.5, + "boxX": 0.1, + "boxY": 1.0 + } + ], + "skill": [ + { + "ability": "E-Unfill", + "shape": "Circle", + "center": "PrimaryTarget", + "offsetY": 2.5, + "radius": 5 + }, + { + "ability": "E-Fill", + "shape": "Circle", + "center": "PrimaryTarget", + "offsetY": 2.5, + "radius": 5 + }, + { + "ability": "E-DoT", + "shape": "Circle", + "center": "PrimaryTarget", + "radius": 4 + }, + { + "ability": "E-AbsorbCheck", + "shape": "Circle", + "center": "Player", + "radius": 4 + } + ], + "burst": [ + { + "ability": "Q-Cast", + "shape": "Circle", + "center": "Player", + "offsetY": 1, + "radius": 5 + }, + { + "ability": "Q-DoT", + "shape": "Circle", + "center": "GlobalValue", + "radius": 1.2, + "notes": "Spawns on 3 closest enemies to the player. The detection area is not implemented. The exact radius of the hitbox is not known." + }, + { + "ability": "Q-AbsorbCheck", + "shape": "Circle", + "center": "Player", + "radius": 1.2, + "notes": "The exact radius of the hitbox is not known." + } + ] + }, "jean": { "normal": [ { diff --git a/ui/packages/docs/src/components/Frames/character_data.json b/ui/packages/docs/src/components/Frames/character_data.json index b0dec484e5..a603f06b50 100644 --- a/ui/packages/docs/src/components/Frames/character_data.json +++ b/ui/packages/docs/src/components/Frames/character_data.json @@ -825,6 +825,20 @@ "count": "https://docs.google.com/spreadsheets/d/16ZDmEBZOD4xeUcodxFJVuLC7IIqM9LGLilkct65mElg/edit?usp=sharing" } ], + "jahoda": [ + { + "vid_credit": "yamitoka", + "count_credit": "yamitoka", + "vid": "https://www.youtube.com/z4zaCN_dWwA", + "count": "https://docs.google.com/spreadsheets/d/18YvIoV1Ym1jDhlcTK_DOnKoKFRjkCyElpFCD8p8jncY/edit?usp=sharing" + }, + { + "vid_credit": "yamitoka", + "count_credit": "yamitoka", + "vid": "https://www.youtube.com/ypF3osO9S4o", + "count": "https://docs.google.com/spreadsheets/d/18YvIoV1Ym1jDhlcTK_DOnKoKFRjkCyElpFCD8p8jncY/edit?usp=sharing" + } + ], "jean": [ { "vid_credit": "Kolibri#7675", diff --git a/ui/packages/docs/src/components/Issues/character_data.json b/ui/packages/docs/src/components/Issues/character_data.json index 0cf55241f5..584d92daeb 100644 --- a/ui/packages/docs/src/components/Issues/character_data.json +++ b/ui/packages/docs/src/components/Issues/character_data.json @@ -5,6 +5,9 @@ "chasca": [ "Cannot jump while in Nightsoul Blessing" ], + "jahoda": [ + "Smoke Bomb skill is not implemented, since it only trigger when no enemies are nearby." + ], "kinich": [ "The actual Kinich's circular movement is not simulated, so Loop Shots/Scalespiker Cannon will damage the Primary target, which does not depend on attack angle." ], diff --git a/ui/packages/docs/src/components/Names/character_data.json b/ui/packages/docs/src/components/Names/character_data.json index 9c96ddb892..a23b57c944 100644 --- a/ui/packages/docs/src/components/Names/character_data.json +++ b/ui/packages/docs/src/components/Names/character_data.json @@ -60,6 +60,7 @@ "tao", "ht" ], + "jahoda": [], "jean": [], "kazuha": [ "kaedeharakazuha", diff --git a/ui/packages/docs/src/components/Params/character_data.json b/ui/packages/docs/src/components/Params/character_data.json index facb35f280..094201e6d7 100644 --- a/ui/packages/docs/src/components/Params/character_data.json +++ b/ui/packages/docs/src/components/Params/character_data.json @@ -672,6 +672,33 @@ "param": "collision", "desc": "0 for no collision dmg (default), 1 for collision dmg." } + ], + "jahoda": [ + { + "ability": "attack", + "param": "travel", + "desc": "Projectile travel time. Default 10 frames." + }, + { + "ability": "aim", + "param": "hold", + "desc": "0 for Physical Aimed Shot, 1 for Fully-Charged Aimed Shot (default)." + }, + { + "ability": "aim", + "param": "travel", + "desc": "Projectile travel time. Default 10 frames." + }, + { + "ability": "aim", + "param": "weakspot", + "desc": "Hit weakspot with aimed shot. Default 0 (false), 1 for true." + }, + { + "ability": "skill", + "param": "travel", + "desc": "Projectile travel time for Meowball. Default 13 frames." + } ], "jean": [ { diff --git a/ui/packages/ui/src/Data/char_data.generated.json b/ui/packages/ui/src/Data/char_data.generated.json index b002a6625f..6c3a806d2b 100644 --- a/ui/packages/ui/src/Data/char_data.generated.json +++ b/ui/packages/ui/src/Data/char_data.generated.json @@ -771,6 +771,23 @@ }, "name_text_hash_map ": "3068316954" }, + "jahoda": { + "id": 10000124, + "key": "jahoda", + "rarity": "QUALITY_PURPLE", + "body": "BODY_GIRL", + "region": "ASSOC_TYPE_NODKRAI", + "element": "Wind", + "weapon_class": "WEAPON_BOW", + "icon_name": "UI_AvatarIcon_Jahoda", + "skill_details": { + "skill": 11242, + "burst": 11245, + "attack": 11241, + "burst_energy_cost": 70 + }, + "name_text_hash_map ": "1438964522" + }, "jean": { "id": 10000003, "key": "jean", diff --git a/ui/packages/ui/src/util/mode-gcsim.js b/ui/packages/ui/src/util/mode-gcsim.js index f100edc3ff..3cc3f4b65c 100644 --- a/ui/packages/ui/src/util/mode-gcsim.js +++ b/ui/packages/ui/src/util/mode-gcsim.js @@ -148,6 +148,7 @@ ace.define( 'ht', 'hutao', 'itto', + 'jahoda', 'jean', 'kabukimono', 'kaedeharakazuha',