diff --git a/internal/characters/iansan/asc.go b/internal/characters/iansan/asc.go new file mode 100644 index 0000000000..1ccfddbed8 --- /dev/null +++ b/internal/characters/iansan/asc.go @@ -0,0 +1,118 @@ +package iansan + +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/info" + "github.com/genshinsim/gcsim/pkg/core/player/character" + "github.com/genshinsim/gcsim/pkg/core/targets" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +const ( + a1Status = "precise-movement" + a1ICD = "precise-movement-icd" + a4Status = "warming-up" +) + +func (c *char) a1() { + if c.Base.Ascension < 1 { + return + } + + cb := func(args ...interface{}) bool { + idx := args[0].(int) + if idx != c.Core.Player.Active() { + return false + } + if !c.StatModIsActive(a1Status) { + return false + } + if c.StatusIsActive(a1ICD) { + return false + } + c.AddStatus(a1ICD, 2.8*60, true) + c.a1Increase = true + + return false + } + c.Core.Events.Subscribe(event.OnNightsoulGenerate, cb, "iansan-a1-generate") + c.Core.Events.Subscribe(event.OnNightsoulConsume, cb, "iansan-a1-consume") +} + +func (c *char) a1ATK() { + m := make([]float64, attributes.EndStatType) + m[attributes.ATKP] = 0.2 + c.AddStatMod(character.StatMod{ + Base: modifier.NewBaseWithHitlag(a1Status, 15*60), + Amount: func() ([]float64, bool) { + return m, true + }, + }) +} + +func (c *char) makeA1CB() func(_ combat.AttackCB) { + if c.Base.Ascension < 1 { + return nil + } + + done := false + return func(a combat.AttackCB) { + if done { + return + } + if a.Target.Type() != targets.TargettableEnemy { + return + } + c.a1ATK() + done = true + } +} + +func (c *char) a1Points() float64 { + if c.Base.Ascension < 1 { + return 0.0 + } + if !c.StatModIsActive(a1Status) { + return 0.0 + } + if c.a1Increase { + c.a1Increase = false + return 4.0 + } + return 1.0 +} + +func (c *char) a4() { + if c.Base.Ascension < 4 { + return + } + + c.Core.Events.Subscribe(event.OnNightsoulBurst, func(args ...interface{}) bool { + c.AddStatus(a4Status, 10*60, true) + c.a4Src = c.Core.F + c.a4Task(c.a4Src) + + return false + }, "iansan-a4") +} + +func (c *char) a4Task(src int) { + c.QueueCharTask(func() { + if c.a4Src != src { + return + } + + c.Core.Player.Heal(info.HealInfo{ + Caller: c.Index, + Target: c.Core.Player.Active(), + Message: "Warming Up", + Src: c.TotalAtk() * 0.6, + Bonus: c.Stat(attributes.Heal), + }) + + c.nightsoulState.GeneratePoints(1) + c.a4Task(src) + }, 2.8*60) +} diff --git a/internal/characters/iansan/attack.go b/internal/characters/iansan/attack.go new file mode 100644 index 0000000000..d581bef1c4 --- /dev/null +++ b/internal/characters/iansan/attack.go @@ -0,0 +1,86 @@ +package iansan + +import ( + "fmt" + + "github.com/genshinsim/gcsim/internal/frames" + "github.com/genshinsim/gcsim/pkg/core/action" + "github.com/genshinsim/gcsim/pkg/core/attacks" + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/combat" + "github.com/genshinsim/gcsim/pkg/core/geometry" +) + +var ( + attackFrames [][]int + attackHitmarks = []int{13, 16, 12} + attackHitlagHaltFrame = []float64{0.06, 0.06, 0} + attackHitboxes = [][]float64{{1.2, 3}, {2}, {2}, {2.2}} + attackOffsets = []float64{0, 0.5, 0.5, 0.5, 1.5} + attackFanAngles = []float64{360, 270, 270, 360} +) + +const normalHitNum = 3 + +func init() { + attackFrames = make([][]int, normalHitNum) + + attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0], 28) + attackFrames[0][action.ActionAttack] = 22 + attackFrames[0][action.ActionCharge] = 22 + + attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1], 31) + attackFrames[1][action.ActionAttack] = 22 + attackFrames[1][action.ActionCharge] = 25 + + attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2], 51) + attackFrames[2][action.ActionAttack] = 41 + attackFrames[2][action.ActionCharge] = 500 //TODO: this action is illegal; need better way to handle it +} + +func (c *char) Attack(p map[string]int) (action.Info, error) { + if c.StatusIsActive(fastSkill) { + // TODO: or c.Core.Player.Exec(action.ActionCharge, c.Base.Key, nil) + return c.ChargeAttack(p) + } + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: fmt.Sprintf("Normal %v", c.NormalCounter), + Mult: attack[c.NormalCounter][c.TalentLvlAttack()], + AttackTag: attacks.AttackTagNormal, + ICDTag: attacks.ICDTagNormalAttack, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeSlash, + Element: attributes.Physical, + Durability: 25, + HitlagFactor: 0.01, + HitlagHaltFrames: attackHitlagHaltFrame[c.NormalCounter] * 60, + CanBeDefenseHalted: true, + } + ap := combat.NewCircleHitOnTargetFanAngle( + c.Core.Combat.Player(), + geometry.Point{Y: attackOffsets[c.NormalCounter]}, + attackHitboxes[c.NormalCounter][0], + attackFanAngles[c.NormalCounter], + ) + if c.NormalCounter == 0 { + ap = combat.NewBoxHitOnTarget( + c.Core.Combat.Player(), + geometry.Point{Y: attackOffsets[c.NormalCounter]}, + attackHitboxes[c.NormalCounter][0], + attackHitboxes[c.NormalCounter][1], + ) + } + + c.Core.QueueAttack(ai, ap, attackHitmarks[c.NormalCounter], attackHitmarks[c.NormalCounter]) + + defer c.AdvanceNormalIndex() + + return action.Info{ + Frames: frames.NewAttackFunc(c.Character, attackFrames), + AnimationLength: attackFrames[c.NormalCounter][action.InvalidAction], + CanQueueAfter: attackHitmarks[c.NormalCounter], + State: action.NormalAttackState, + }, nil +} diff --git a/internal/characters/iansan/burst.go b/internal/characters/iansan/burst.go new file mode 100644 index 0000000000..0cba87fc97 --- /dev/null +++ b/internal/characters/iansan/burst.go @@ -0,0 +1,142 @@ +package iansan + +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/player/character" + "github.com/genshinsim/gcsim/pkg/modifier" +) + +var burstFrames []int + +func init() { + burstFrames = frames.InitAbilSlice(72) + burstFrames[action.ActionAttack] = 71 + burstFrames[action.ActionSkill] = 71 + burstFrames[action.ActionJump] = 70 + burstFrames[action.ActionSwap] = 69 +} + +const ( + burstHitmark = 34 + + burstStatus = "kinetic-energy" + burstBuffStatus = "iansan-burst-buff" +) + +func (c *char) Burst(p map[string]int) (action.Info, error) { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "The Three Principles of Power", + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + AttackTag: attacks.AttackTagElementalBurst, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.Electro, + Durability: 25, + Mult: burst[c.TalentLvlBurst()], + } + c.Core.QueueAttack( + ai, + combat.NewCircleHit(c.Core.Combat.Player(), c.Core.Combat.PrimaryTarget(), nil, 6), + burstHitmark, + burstHitmark, + ) + + c.Core.Tasks.Add(func() { + if !c.nightsoulState.HasBlessing() { + c.enterNightsoul(15) + } else { + c.nightsoulState.GeneratePoints(15) + } + }, burstHitmark) + + c.burstSrc = c.Core.F + c.burstRestoreNS = 0 + c.updateATKBuff() + c.applyBuffTask(c.burstSrc) + c.Core.Events.Subscribe(event.OnActionExec, c.burstMovementRestore, burstBuffStatus) + + if c.Base.Cons >= 2 { + c.a1ATK() + } + c.c4Stacks = 0 + + duration := 12 * 60 + if c.Base.Cons >= 6 { + duration += 3.0 + } + c.AddStatus(burstStatus, duration, false) // TODO: hitlag affected? + c.SetCD(action.ActionBurst, 18*60) + c.ConsumeEnergy(3) + + return action.Info{ + Frames: frames.NewAbilFunc(burstFrames), + AnimationLength: burstFrames[action.InvalidAction], + CanQueueAfter: burstFrames[action.ActionSwap], + State: action.BurstState, + }, nil +} + +func (c *char) applyBuffTask(src int) { + c.Core.Tasks.Add(func() { + if c.burstSrc != src { + return + } + if !c.StatusIsActive(burstStatus) { + c.c4Stacks = 0 + return + } + + points := float64(c.burstRestoreNS) + c.a1Points() + c.c4Points() + c.burstRestoreNS = 0 + c.pointsOverflow = max(c.nightsoulState.Points()+points-c.nightsoulState.MaxPoints, 0.0) + if c.pointsOverflow > 0 { + c.c6() + } + if points > 0.0 { + c.nightsoulState.GeneratePoints(points) + } + + active := c.Core.Player.ActiveChar() + active.AddStatMod(character.StatMod{ + Base: modifier.NewBaseWithHitlag(burstBuffStatus, 1*60), + Amount: func() ([]float64, bool) { + c.c2ATKBuff(active) + return c.burstBuff, true + }, + }) + c.applyBuffTask(src) + }, 0.5*60) // TODO: refresh rate? +} + +func (c *char) updateATKBuff() { + if !c.StatusIsActive(burstStatus) { + c.burstBuff[attributes.ATK] = 0 + return + } + + rate := highATK[c.TalentLvlBurst()] + if c.nightsoulState.Points() < 42 { + rate = lowATK[c.TalentLvlBurst()] * c.nightsoulState.Points() + } + c.burstBuff[attributes.ATK] = min(c.TotalAtk()*rate, maxATK[c.TalentLvlBurst()]) +} + +func (c *char) burstMovementRestore(args ...interface{}) bool { + if !c.StatusIsActive(burstStatus) { + return true + } + + param := args[2].(map[string]int) + movement, ok := param["movement"] + if ok { + c.burstRestoreNS += movement + } + return false +} diff --git a/internal/characters/iansan/charge.go b/internal/characters/iansan/charge.go new file mode 100644 index 0000000000..04fb7477ad --- /dev/null +++ b/internal/characters/iansan/charge.go @@ -0,0 +1,105 @@ +package iansan + +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" +) + +var ( + chargeFrames []int + swiftFrames []int +) + +const ( + chargeHitmark = 24 + swiftHitmark = 14 +) + +func init() { + chargeFrames = frames.InitAbilSlice(55) + chargeFrames[action.ActionAttack] = 49 + chargeFrames[action.ActionSkill] = 49 + chargeFrames[action.ActionBurst] = 50 + chargeFrames[action.ActionDash] = chargeHitmark + chargeFrames[action.ActionJump] = chargeHitmark + chargeFrames[action.ActionSwap] = 49 + + swiftFrames = frames.InitAbilSlice(52) + swiftFrames[action.ActionAttack] = 51 + swiftFrames[action.ActionSkill] = 51 + swiftFrames[action.ActionJump] = 50 + swiftFrames[action.ActionSwap] = 49 +} + +func (c *char) ChargeAttack(p map[string]int) (action.Info, error) { + if c.nightsoulState.HasBlessing() || c.StatusIsActive(fastSkill) { + c.DeleteStatus(fastSkill) + return c.chargedSwift(), nil + } + + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Charged Attack", + AttackTag: attacks.AttackTagExtra, + ICDTag: attacks.ICDTagExtraAttack, + ICDGroup: attacks.ICDGroupPoleExtraAttack, + StrikeType: attacks.StrikeTypeSpear, + Element: attributes.Physical, + Durability: 25, + HitlagFactor: 0.01, + CanBeDefenseHalted: true, + IsDeployable: true, + Mult: charged[c.TalentLvlAttack()], + } + + c.Core.QueueAttack( + ai, + combat.NewCircleHit( + c.Core.Combat.Player(), + c.Core.Combat.PrimaryTarget(), + nil, + 0.8, + ), + chargeHitmark, + chargeHitmark, + ) + + return action.Info{ + Frames: frames.NewAbilFunc(chargeFrames), + AnimationLength: chargeFrames[action.InvalidAction], + CanQueueAfter: chargeHitmark, + State: action.ChargeAttackState, + }, nil +} + +func (c *char) chargedSwift() action.Info { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Swift Stormflight", + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + AttackTag: attacks.AttackTagExtra, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeDefault, + Element: attributes.Electro, + Durability: 25, + Mult: swift[c.TalentLvlSkill()], + } + c.Core.QueueAttack( + ai, + combat.NewCircleHit(c.Core.Combat.Player(), c.Core.Combat.PrimaryTarget(), nil, 6), + swiftHitmark, + swiftHitmark, + c.makeA1CB(), + ) + + return action.Info{ + Frames: frames.NewAbilFunc(swiftFrames), + AnimationLength: swiftFrames[action.InvalidAction], + CanQueueAfter: swiftFrames[action.ActionSwap], + State: action.ChargeAttackState, + } +} diff --git a/internal/characters/iansan/config.yml b/internal/characters/iansan/config.yml new file mode 100644 index 0000000000..8fcb68fa5f --- /dev/null +++ b/internal/characters/iansan/config.yml @@ -0,0 +1,27 @@ +package_name: iansan +genshin_id: 10000110 +key: iansan +skill_data_mapping: + attack: + attack_1: + - 0 # 1-Hit DMG|{param1:F1P} + attack_2: + - 1 # 2-Hit DMG|{param2:F1P} + attack_3: + - 2 # 3-Hit DMG|{param3:F1P} + charged: + - 3 # Charged Attack DMG|{param4:F1P} + swift: + - 4 # Swift Stormflight DMG|{param5:F1P} + skill: + skill: + - 0 # Skill DMG|{param1:F1P} + burst: + burst: + - 0 # Skill DMG|{param1:F1P} + highATK: + - 1 # High Nightsoul Points ATK Conversion Rate|{param2:P} ATK + lowATK: + - 2 # Low Nightsoul Points ATK Conversion Rate|{param3:F1P} ATK / Nightsoul Point + maxATK: + - 3 # Max ATK Bonus|{param4:F1} diff --git a/internal/characters/iansan/cons.go b/internal/characters/iansan/cons.go new file mode 100644 index 0000000000..1889b1dea9 --- /dev/null +++ b/internal/characters/iansan/cons.go @@ -0,0 +1,90 @@ +package iansan + +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 ( + c1ICD = "iansan-c1" + c6Status = "iansan-c6" +) + +func (c *char) c1(points float64) { + if c.Base.Cons < 1 { + return + } + if c.StatusIsActive(c1ICD) { + return + } + c.c1Points += points + if c.c1Points < 6 { + return + } + c.AddEnergy("iansan-c1", 15) + c.AddStatus(c1ICD, 18*60, true) +} + +func (c *char) c2ATKBuff(char *character.CharWrapper) { + c.burstBuff[attributes.ATKP] = 0 + if c.Base.Cons < 2 { + return + } + if c.Base.Ascension < 1 { + return + } + if c.Index == c.Core.Player.Active() { + return + } + if char.Index != c.Core.Player.Active() { + return + } + c.burstBuff[attributes.ATKP] = 0.3 +} + +func (c *char) c4() { + if c.Base.Cons < 4 { + return + } + + c.Core.Events.Subscribe(event.OnBurst, func(args ...interface{}) bool { + if !c.StatusIsActive(burstStatus) { + return false + } + if c.Index == c.Core.Player.Active() { + return false + } + c.c4Stacks = 2 + return false + }, "iansan-c4") +} + +func (c *char) c4Points() float64 { + if c.Base.Cons < 4 { + return 0.0 + } + points := c.pointsOverflow * 0.5 + if c.c4Stacks > 0 { + points += 4.0 + } + return points +} + +func (c *char) c6() { + if c.Base.Cons < 6 { + return + } + m := make([]float64, attributes.EndStatType) + m[attributes.DmgP] = 0.25 + + active := c.Core.Player.ActiveChar() + active.AddAttackMod(character.AttackMod{ + Base: modifier.NewBaseWithHitlag(c6Status, 3*60), + Amount: func(atk *combat.AttackEvent, t combat.Target) ([]float64, bool) { + return m, true + }, + }) +} diff --git a/internal/characters/iansan/data_gen.textproto b/internal/characters/iansan/data_gen.textproto new file mode 100644 index 0000000000..e5e7ec0471 --- /dev/null +++ b/internal/characters/iansan/data_gen.textproto @@ -0,0 +1,151 @@ +id: 10000110 +key: "iansan" +rarity: QUALITY_PURPLE +body: BODY_LOLI +region: ASSOC_TYPE_NATLAN +element: Electric +weapon_class: WEAPON_POLE +icon_name: "UI_AvatarIcon_Iansan" +stats: { + base_hp: 893.5552 + base_atk: 21.54768 + base_def: 53.505375 + 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_ATTACK_PERCENT + } + } + promo_data: { + max_level: 40 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 667.5315 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 39.97125 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 16.097597 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + } + } + promo_data: { + max_level: 50 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 1141.8302 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 68.37187 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 27.535364 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + value: 0.06 + } + } + promo_data: { + max_level: 60 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 1774.2284 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 106.23937 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 42.78572 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + value: 0.12 + } + } + promo_data: { + max_level: 70 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2248.527 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 134.64 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 54.223488 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + value: 0.12 + } + } + promo_data: { + max_level: 80 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 2722.8257 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 163.04062 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 65.661255 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + value: 0.18 + } + } + promo_data: { + max_level: 90 + add_props: { + prop_type: FIGHT_PROP_BASE_HP + value: 3197.1245 + } + add_props: { + prop_type: FIGHT_PROP_BASE_DEFENSE + value: 191.44125 + } + add_props: { + prop_type: FIGHT_PROP_BASE_ATTACK + value: 77.09902 + } + add_props: { + prop_type: FIGHT_PROP_ATTACK_PERCENT + value: 0.24 + } + } +} +skill_details: { + skill: 11102 + burst: 11105 + attack: 11101 + burst_energy_cost: 70 +} +name_text_hash_map: 3120492514 diff --git a/internal/characters/iansan/iansan.go b/internal/characters/iansan/iansan.go new file mode 100644 index 0000000000..e801843ca6 --- /dev/null +++ b/internal/characters/iansan/iansan.go @@ -0,0 +1,76 @@ +package iansan + +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/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.Iansan, NewChar) +} + +type char struct { + *tmpl.Character + nightsoulState *nightsoul.State + + nightsoulSrc int + particleGenerated bool + burstSrc int + burstBuff []float64 + burstRestoreNS int + pointsOverflow float64 + + a1Increase bool + a4Src int + + c1Points float64 + c4Stacks int +} + +func NewChar(s *core.Core, w *character.CharWrapper, _ info.CharacterProfile) error { + c := char{} + c.Character = tmpl.NewWithWrapper(s, w) + + c.EnergyMax = 70 + c.SkillCon = 3 + c.BurstCon = 5 + c.NormalHitNum = normalHitNum + + c.nightsoulState = nightsoul.New(s, w) + c.nightsoulState.MaxPoints = 54 + + w.Character = &c + + return nil +} + +func (c *char) Init() error { + c.burstBuff = make([]float64, attributes.EndStatType) + + c.a1() + c.a4() + + return nil +} + +func (c *char) ActionStam(a action.Action, p map[string]int) float64 { + if a == action.ActionCharge && c.StatusIsActive(fastSkill) { + return 0 + } + return c.Character.ActionStam(a, p) +} + +func (c *char) Condition(fields []string) (any, error) { + switch fields[0] { + case "nightsoul": + return c.nightsoulState.Condition(fields) + default: + return c.Character.Condition(fields) + } +} diff --git a/internal/characters/iansan/iansan_gen.go b/internal/characters/iansan/iansan_gen.go new file mode 100644 index 0000000000..a99aad7a88 --- /dev/null +++ b/internal/characters/iansan/iansan_gen.go @@ -0,0 +1,216 @@ +// Code generated by "pipeline"; DO NOT EDIT. +package iansan + +import ( + _ "embed" + + "github.com/genshinsim/gcsim/pkg/model" + "google.golang.org/protobuf/encoding/prototext" +) + +//go:embed data_gen.textproto +var pbData []byte +var base *model.AvatarData + +func init() { + base = &model.AvatarData{} + err := prototext.Unmarshal(pbData, base) + if err != nil { + panic(err) + } +} + +func (x *char) Data() *model.AvatarData { + return base +} + +var ( + attack = [][]float64{ + attack_1, + attack_2, + attack_3, + } +) + +var ( + // attack: attack_1 = [0] + attack_1 = []float64{ + 0.469758, + 0.507994, + 0.54623, + 0.600853, + 0.639089, + 0.682787, + 0.742873, + 0.802958, + 0.863043, + 0.928591, + 0.994139, + 1.059686, + 1.125234, + 1.190781, + 1.256329, + } + // attack: attack_2 = [1] + attack_2 = []float64{ + 0.427644, + 0.462452, + 0.49726, + 0.546986, + 0.581794, + 0.621575, + 0.676274, + 0.730972, + 0.785671, + 0.845342, + 0.905013, + 0.964684, + 1.024356, + 1.084027, + 1.143698, + } + // attack: attack_3 = [2] + attack_3 = []float64{ + 0.643882, + 0.696291, + 0.7487, + 0.82357, + 0.875979, + 0.935875, + 1.018232, + 1.100589, + 1.182946, + 1.27279, + 1.362634, + 1.452478, + 1.542322, + 1.632166, + 1.72201, + } + // attack: charged = [3] + charged = []float64{ + 1.00276, + 1.08438, + 1.166, + 1.2826, + 1.36422, + 1.4575, + 1.58576, + 1.71402, + 1.84228, + 1.9822, + 2.12212, + 2.26204, + 2.40196, + 2.54188, + 2.6818, + } + // attack: swift = [4] + swift = []float64{ + 0.84194, + 0.91047, + 0.979, + 1.0769, + 1.14543, + 1.22375, + 1.33144, + 1.43913, + 1.54682, + 1.6643, + 1.78178, + 1.89926, + 2.01674, + 2.13422, + 2.2517, + } + // skill: skill = [0] + skill = []float64{ + 2.864, + 3.0788, + 3.2936, + 3.58, + 3.7948, + 4.0096, + 4.296, + 4.5824, + 4.8688, + 5.1552, + 5.4416, + 5.728, + 6.086, + 6.444, + 6.802, + } + // burst: burst = [0] + burst = []float64{ + 4.304, + 4.6268, + 4.9496, + 5.38, + 5.7028, + 6.0256, + 6.456, + 6.8864, + 7.3168, + 7.7472, + 8.1776, + 8.608, + 9.146, + 9.684, + 10.222, + } + // burst: highATK = [1] + highATK = []float64{ + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + 0.27, + } + // burst: lowATK = [2] + lowATK = []float64{ + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + 0.005, + } + // burst: maxATK = [3] + maxATK = []float64{ + 330, + 370, + 410, + 450, + 490, + 530, + 570, + 610, + 650, + 690, + 730, + 770, + 810, + 850, + 890, + } +) diff --git a/internal/characters/iansan/skill.go b/internal/characters/iansan/skill.go new file mode 100644 index 0000000000..906e169090 --- /dev/null +++ b/internal/characters/iansan/skill.go @@ -0,0 +1,124 @@ +package iansan + +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/targets" +) + +var ( + skillHitmark = 21 + skillFrames []int + + fastSkill = "fast-skill" +) + +func init() { + skillFrames = frames.InitAbilSlice(43) + skillFrames[action.ActionDash] = 31 + skillFrames[action.ActionJump] = 32 + skillFrames[action.ActionSwap] = 42 +} + +func (c *char) Skill(p map[string]int) (action.Info, error) { + ai := combat.AttackInfo{ + ActorIndex: c.Index, + Abil: "Thunderbolt Rush", + AdditionalTags: []attacks.AdditionalTag{attacks.AdditionalTagNightsoul}, + AttackTag: attacks.AttackTagElementalArt, + ICDTag: attacks.ICDTagNone, + ICDGroup: attacks.ICDGroupDefault, + StrikeType: attacks.StrikeTypeSlash, + Element: attributes.Electro, + Durability: 25, + Mult: skill[c.TalentLvlSkill()], + } + + c.Core.QueueAttack( + ai, + combat.NewCircleHit( + c.Core.Combat.Player(), + c.Core.Combat.PrimaryTarget(), + nil, + 1, + ), + skillHitmark, + skillHitmark, + c.particleCB, + ) + + c.AddStatus(fastSkill, 5*60, true) + c.enterNightsoul(c.nightsoulState.MaxPoints) + c.particleGenerated = false + c.SetCD(action.ActionSkill, 16*60) + + return action.Info{ + Frames: frames.NewAbilFunc(skillFrames), + AnimationLength: skillFrames[action.InvalidAction], + CanQueueAfter: skillFrames[action.ActionDash], // earliest cancel + State: action.SkillState, + }, nil +} + +func (c *char) particleCB(a combat.AttackCB) { + if a.Target.Type() != targets.TargettableEnemy { + return + } + if c.particleGenerated { + return + } + c.particleGenerated = true + + count := 4.0 + c.Core.QueueParticle(c.Base.Key.String(), count, attributes.Electro, c.ParticleDelay) +} + +func (c *char) enterNightsoul(points float64) { + c.nightsoulSrc = c.Core.F + c.nightsoulState.EnterBlessing(points) + c.nightsoulPointReduceTask(c.nightsoulSrc) + c.setNightsoulExitTimer(16 * 60) +} + +func (c *char) exitNightsoul() { + c.nightsoulSrc = -1 + c.nightsoulState.ExitBlessing() + c.DeleteStatus(burstStatus) + c.DeleteStatus(a1Status) +} + +func (c *char) nightsoulPointReduceTask(src int) { + // reduce 0.6 point every 6f, which is 6 per second + const tickInterval = .1 + + c.QueueCharTask(func() { + if c.nightsoulSrc != src { + return + } + + points := 0.6 + c.nightsoulState.ConsumePoints(points) + c.c1(points) + c.updateATKBuff() + if c.nightsoulState.Points() < 0.001 { + c.exitNightsoul() + return + } + + c.nightsoulPointReduceTask(src) + }, 60*tickInterval) +} + +func (c *char) setNightsoulExitTimer(duration int) { + src := c.nightsoulSrc + c.QueueCharTask(func() { + if c.nightsoulSrc != src { + return + } + c.nightsoulState.ClearPoints() + c.exitNightsoul() + }, duration) +} diff --git a/internal/services/assets/avatars_gen.go b/internal/services/assets/avatars_gen.go index f451a2e918..42bdd193ee 100644 --- a/internal/services/assets/avatars_gen.go +++ b/internal/services/assets/avatars_gen.go @@ -37,6 +37,7 @@ var avatarMap = map[string]string{ "gorou": "UI_AvatarIcon_Gorou", "heizou": "UI_AvatarIcon_Heizo", "hutao": "UI_AvatarIcon_Hutao", + "iansan": "UI_AvatarIcon_Iansan", "itto": "UI_AvatarIcon_Itto", "jean": "UI_AvatarIcon_Qin", "kaeya": "UI_AvatarIcon_Kaeya", diff --git a/pkg/core/keys/keys_char_gen.go b/pkg/core/keys/keys_char_gen.go index 42950ecb0a..1b7a98bcdd 100644 --- a/pkg/core/keys/keys_char_gen.go +++ b/pkg/core/keys/keys_char_gen.go @@ -42,6 +42,7 @@ const ( Gorou Heizou Hutao + Iansan Itto Jean Kaeya @@ -242,6 +243,10 @@ func init() { charPrettyName[Hutao] = "Hutao" CharKeyToEle[Hutao] = attributes.Pyro + charNames[Iansan] = "iansan" + charPrettyName[Iansan] = "Iansan" + CharKeyToEle[Iansan] = attributes.Electro + charNames[Itto] = "itto" charPrettyName[Itto] = "Itto" CharKeyToEle[Itto] = attributes.Geo diff --git a/pkg/gcs/validation/validate.go b/pkg/gcs/validation/validate.go index 5927197434..3af05c5c92 100644 --- a/pkg/gcs/validation/validate.go +++ b/pkg/gcs/validation/validate.go @@ -1,15 +1,31 @@ package validation import ( + "slices" + "github.com/genshinsim/gcsim/pkg/core/action" "github.com/genshinsim/gcsim/pkg/core/keys" ) +// generic params that can be used for any character +var ignoreParams = []string{ + // iansan burst + "movement", +} + func ValidateCharParamKeys(c keys.Char, a action.Action, keys []string) error { f, ok := charValidParamKeys[c] if !ok { // all is ok if no validation function registered return nil } - return f(a, keys) + + filtered := make([]string, 0, len(keys)) + for _, v := range keys { + if !slices.Contains(ignoreParams, v) { + filtered = append(filtered, v) + } + } + + return f(a, filtered) } diff --git a/pkg/shortcut/characters.go b/pkg/shortcut/characters.go index 58c1c4e83f..1431e2de02 100644 --- a/pkg/shortcut/characters.go +++ b/pkg/shortcut/characters.go @@ -180,4 +180,5 @@ var CharNameToKey = map[string]keys.Char{ "olorun": keys.Ororon, "chasca": keys.Chasca, "lanyan": keys.Lanyan, + "iansan": keys.Iansan, } diff --git a/pkg/simulation/imports_char_gen.go b/pkg/simulation/imports_char_gen.go index e4cc45dfba..54a97dd93a 100644 --- a/pkg/simulation/imports_char_gen.go +++ b/pkg/simulation/imports_char_gen.go @@ -37,6 +37,7 @@ import ( _ "github.com/genshinsim/gcsim/internal/characters/gorou" _ "github.com/genshinsim/gcsim/internal/characters/heizou" _ "github.com/genshinsim/gcsim/internal/characters/hutao" + _ "github.com/genshinsim/gcsim/internal/characters/iansan" _ "github.com/genshinsim/gcsim/internal/characters/itto" _ "github.com/genshinsim/gcsim/internal/characters/jean" _ "github.com/genshinsim/gcsim/internal/characters/kaeya" diff --git a/ui/packages/db/src/Data/char_data.generated.json b/ui/packages/db/src/Data/char_data.generated.json index c393b1f739..406c833929 100644 --- a/ui/packages/db/src/Data/char_data.generated.json +++ b/ui/packages/db/src/Data/char_data.generated.json @@ -685,6 +685,23 @@ }, "name_text_hash_map ": "1940919994" }, + "iansan": { + "id": 10000110, + "key": "iansan", + "rarity": "QUALITY_PURPLE", + "body": "BODY_LOLI", + "region": "ASSOC_TYPE_NATLAN", + "element": "Electric", + "weapon_class": "WEAPON_POLE", + "icon_name": "UI_AvatarIcon_Iansan", + "skill_details": { + "skill": 11102, + "burst": 11105, + "attack": 11101, + "burst_energy_cost": 70 + }, + "name_text_hash_map ": "3120492514" + }, "itto": { "id": 10000057, "key": "itto", diff --git a/ui/packages/docs/docs/reference/characters/iansan.md b/ui/packages/docs/docs/reference/characters/iansan.md new file mode 100644 index 0000000000..4e6fe2527c --- /dev/null +++ b/ui/packages/docs/docs/reference/characters/iansan.md @@ -0,0 +1,44 @@ +--- +title: Iansan +--- + +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/localization/src/locales/names.generated.json b/ui/packages/localization/src/locales/names.generated.json index b9ffa309cd..ee0eea3c24 100644 --- a/ui/packages/localization/src/locales/names.generated.json +++ b/ui/packages/localization/src/locales/names.generated.json @@ -41,6 +41,7 @@ "gorou": "五郎", "heizou": "鹿野院平藏", "hutao": "胡桃", + "iansan": "伊安珊", "itto": "荒泷一斗", "jean": "琴", "kaeya": "凯亚", @@ -165,6 +166,7 @@ "filletblade": "吃虎鱼刀", "finaleofthedeep": "海渊终曲", "fleuvecendreferryman": "灰河渡手", + "flowerwreathedfeathers": "缀花之翎", "flowingpurity": "纯水流华", "fluteofezpitzal": "息燧之笛", "footprintoftherainbow": "虹的行迹", @@ -272,6 +274,7 @@ "swordofdescension": "降临之剑", "swordofnarzissenkreuz": "水仙十字之剑", "talkingstick": "聊聊棒", + "tamayurateinoohanashi": "且住亭御咄", "thealleyflash": "暗巷闪光", "thebell": "钟剑", "theblacksword": "黑剑", @@ -742,6 +745,7 @@ "gorou": "Gorou", "heizou": "Shikanoin Heizou", "hutao": "Hu Tao", + "iansan": "Iansan", "itto": "Arataki Itto", "jean": "Jean", "kaeya": "Kaeya", @@ -866,6 +870,7 @@ "filletblade": "Fillet Blade", "finaleofthedeep": "Finale of the Deep", "fleuvecendreferryman": "Fleuve Cendre Ferryman", + "flowerwreathedfeathers": "Flower-Wreathed Feathers", "flowingpurity": "Flowing Purity", "fluteofezpitzal": "Flute of Ezpitzal", "footprintoftherainbow": "Footprint of the Rainbow", @@ -973,6 +978,7 @@ "swordofdescension": "Sword of Descension", "swordofnarzissenkreuz": "Sword of Narzissenkreuz", "talkingstick": "Talking Stick", + "tamayurateinoohanashi": "Tamayuratei no Ohanashi", "thealleyflash": "The Alley Flash", "thebell": "The Bell", "theblacksword": "The Black Sword", @@ -1443,6 +1449,7 @@ "gorou": "Gorou", "heizou": "Shikanoin Heizou", "hutao": "Hu Tao", + "iansan": "Iansan", "itto": "Arataki Itto", "jean": "Jean", "kaeya": "Kaeya", @@ -1567,6 +1574,7 @@ "filletblade": "Filetiermesser", "finaleofthedeep": "Finale in der Tiefe", "fleuvecendreferryman": "Schiffer des Fleuve Cendré", + "flowerwreathedfeathers": "Mit Blumen gebundene Federn", "flowingpurity": "Fließende Reinheit", "fluteofezpitzal": "Ezpitzal-Flöte", "footprintoftherainbow": "Spuren des Regenbogens", @@ -1674,6 +1682,7 @@ "swordofdescension": "Schwert der Niederkunft", "swordofnarzissenkreuz": "Schwert des Narzissenkreuzes", "talkingstick": "Lass uns reden", + "tamayurateinoohanashi": "Tamayuratei no Ohanashi", "thealleyflash": "Gassenleuchte", "thebell": "Glocke", "theblacksword": "Schwarzes Schwert", @@ -2144,6 +2153,7 @@ "gorou": "ゴロー", "heizou": "鹿野院平蔵", "hutao": "胡桃", + "iansan": "イアンサ", "itto": "荒瀧一斗", "jean": "ジン", "kaeya": "ガイア", @@ -2268,6 +2278,7 @@ "filletblade": "チ虎魚の刀", "finaleofthedeep": "海淵のフィナーレ", "fleuvecendreferryman": "サーンドルの渡し守", + "flowerwreathedfeathers": "花飾りの羽", "flowingpurity": "純水流華", "fluteofezpitzal": "エズピツァルの笛", "footprintoftherainbow": "虹の行方", @@ -2375,6 +2386,7 @@ "swordofdescension": "降臨の剣", "swordofnarzissenkreuz": "水仙十字の剣", "talkingstick": "話死合い棒", + "tamayurateinoohanashi": "玉響停の御噺", "thealleyflash": "ダークアレイの閃光", "thebell": "鐘の剣", "theblacksword": "黒剣", @@ -2844,6 +2856,7 @@ "gorou": "고로", "heizou": "시카노인 헤이조", "hutao": "호두", + "iansan": "얀사", "itto": "아라타키 이토", "jean": "진", "kaeya": "케이아", @@ -2968,6 +2981,7 @@ "filletblade": "흘호 생선회칼", "finaleofthedeep": "해연의 피날레", "fleuvecendreferryman": "잿빛의 강 뱃사공", + "flowerwreathedfeathers": "꽃장식 깃", "flowingpurity": "순수한 달빛 물결", "fluteofezpitzal": "에스피찰의 피리", "footprintoftherainbow": "무지개의 행적", @@ -3075,6 +3089,7 @@ "swordofdescension": "강림의 검", "swordofnarzissenkreuz": "수선화 십자검", "talkingstick": "대화봉", + "tamayurateinoohanashi": "쉼터의 이야기꾼", "thealleyflash": "뒷골목의 섬광", "thebell": "시간의 검", "theblacksword": "칠흑검", @@ -3545,6 +3560,7 @@ "gorou": "Горо", "heizou": "Хэйдзо", "hutao": "Ху Тао", + "iansan": "Иансан", "itto": "Итто", "jean": "Джинн", "kaeya": "Кэйа", @@ -3669,6 +3685,7 @@ "filletblade": "Филейный нож", "finaleofthedeep": "Грандиозный финал глубин", "fleuvecendreferryman": "Перевозчик Флёв Сандр", + "flowerwreathedfeathers": "Украшенные цветами перья", "flowingpurity": "Сверкание чистых вод", "fluteofezpitzal": "Флейта Эспицаль", "footprintoftherainbow": "След радуги", @@ -3776,6 +3793,7 @@ "swordofdescension": "Меч нисхождения", "swordofnarzissenkreuz": "Меч Нарциссенкрейца", "talkingstick": "Говорящая палица", + "tamayurateinoohanashi": "Тамаюратэй но оханаси", "thealleyflash": "Вспышка во тьме", "thebell": "Меч-колокол", "theblacksword": "Чёрный меч", @@ -4246,6 +4264,7 @@ "gorou": "Gorou", "heizou": "Shikanoin Heizou", "hutao": "Hu Tao", + "iansan": "Iansán", "itto": "Arataki Itto", "jean": "Jean", "kaeya": "Kaeya", @@ -4370,6 +4389,7 @@ "filletblade": "Hoja de Filetear", "finaleofthedeep": "Réquiem Abisal", "fleuvecendreferryman": "Vado del Río Ceniciento", + "flowerwreathedfeathers": "Penacho Engalanado", "flowingpurity": "Fluencia Impoluta", "fluteofezpitzal": "Flauta de Ezpitzal", "footprintoftherainbow": "Estela Iridiscente", @@ -4477,6 +4497,7 @@ "swordofdescension": "Espada del Descenso", "swordofnarzissenkreuz": "Espada Cruz de los Narcisos", "talkingstick": "Garrote del Diálogo", + "tamayurateinoohanashi": "Charla en el Pabellón", "thealleyflash": "Destello en la Oscuridad", "thebell": "Espada del Tiempo", "theblacksword": "Espada Negra", diff --git a/ui/packages/ui/src/Data/char_data.generated.json b/ui/packages/ui/src/Data/char_data.generated.json index c393b1f739..406c833929 100644 --- a/ui/packages/ui/src/Data/char_data.generated.json +++ b/ui/packages/ui/src/Data/char_data.generated.json @@ -685,6 +685,23 @@ }, "name_text_hash_map ": "1940919994" }, + "iansan": { + "id": 10000110, + "key": "iansan", + "rarity": "QUALITY_PURPLE", + "body": "BODY_LOLI", + "region": "ASSOC_TYPE_NATLAN", + "element": "Electric", + "weapon_class": "WEAPON_POLE", + "icon_name": "UI_AvatarIcon_Iansan", + "skill_details": { + "skill": 11102, + "burst": 11105, + "attack": 11101, + "burst_energy_cost": 70 + }, + "name_text_hash_map ": "3120492514" + }, "itto": { "id": 10000057, "key": "itto",