diff --git a/internal/character/luocha/attack.go b/internal/character/luocha/attack.go new file mode 100644 index 00000000..ad18f84d --- /dev/null +++ b/internal/character/luocha/attack.go @@ -0,0 +1,32 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +const Normal key.Attack = "luocha-normal" + +var hitSplit = []float64{0.3, 0.3, 0.4} + +func (c *char) Attack(target key.TargetID, state info.ActionState) { + for i, hitRatio := range hitSplit { + c.engine.Attack(info.Attack{ + Key: Normal, + HitIndex: i, + Targets: []key.TargetID{target}, + Source: c.id, + AttackType: model.AttackType_NORMAL, + DamageType: model.DamageType_IMAGINARY, + BaseDamage: info.DamageMap{ + model.DamageFormula_BY_ATK: basic[c.info.AttackLevelIndex()], + }, + EnergyGain: 20, + StanceDamage: 30, + HitRatio: hitRatio, + }) + } + + c.engine.EndAttack() +} diff --git a/internal/character/luocha/data.go b/internal/character/luocha/data.go new file mode 100644 index 00000000..49b0e4cd --- /dev/null +++ b/internal/character/luocha/data.go @@ -0,0 +1,164 @@ +// Code generated by "charstat"; DO NOT EDIT. + +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/pkg/engine/target/character" +) + +var promotions = []character.PromotionData{ + { + MaxLevel: 20, + ATKBase: 102.96, + ATKAdd: 5.148, + DEFBase: 49.5, + DEFAdd: 2.475, + HPBase: 174.24, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 30, + ATKBase: 144.144, + ATKAdd: 5.148, + DEFBase: 69.3, + DEFAdd: 2.475, + HPBase: 243.936, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 40, + ATKBase: 185.328, + ATKAdd: 5.148, + DEFBase: 89.1, + DEFAdd: 2.475, + HPBase: 313.632, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 50, + ATKBase: 226.512, + ATKAdd: 5.148, + DEFBase: 108.9, + DEFAdd: 2.475, + HPBase: 383.328, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 60, + ATKBase: 267.696, + ATKAdd: 5.148, + DEFBase: 128.7, + DEFAdd: 2.475, + HPBase: 453.024, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 70, + ATKBase: 308.88, + ATKAdd: 5.148, + DEFBase: 148.5, + DEFAdd: 2.475, + HPBase: 522.72, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, + { + MaxLevel: 80, + ATKBase: 350.064, + ATKAdd: 5.148, + DEFBase: 168.3, + DEFAdd: 2.475, + HPBase: 592.416, + HPAdd: 8.712, + SPD: 101, + CritChance: 0.05, + CritDMG: 0.5, + Aggro: 100, + }, +} + +var traces = character.TraceMap{ + "101": { + Ascension: 2, + }, + "102": { + Ascension: 4, + }, + "103": { + Ascension: 6, + }, + "201": { + Stat: prop.ATKPercent, + Amount: 0.04, + Level: 1, + }, + "202": { + Stat: prop.HPPercent, + Amount: 0.04, + Ascension: 2, + }, + "203": { + Stat: prop.ATKPercent, + Amount: 0.04, + Ascension: 3, + }, + "204": { + Stat: prop.DEFPercent, + Amount: 0.05, + Ascension: 3, + }, + "205": { + Stat: prop.ATKPercent, + Amount: 0.06, + Ascension: 4, + }, + "206": { + Stat: prop.HPPercent, + Amount: 0.06, + Ascension: 5, + }, + "207": { + Stat: prop.ATKPercent, + Amount: 0.06, + Ascension: 5, + }, + "208": { + Stat: prop.DEFPercent, + Amount: 0.075, + Ascension: 6, + }, + "209": { + Stat: prop.HPPercent, + Amount: 0.08, + Level: 75, + }, + "210": { + Stat: prop.ATKPercent, + Amount: 0.08, + Level: 80, + }, +} \ No newline at end of file diff --git a/internal/character/luocha/eidolon.go b/internal/character/luocha/eidolon.go new file mode 100644 index 00000000..3c602688 --- /dev/null +++ b/internal/character/luocha/eidolon.go @@ -0,0 +1,69 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/event" + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/modifier" + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/pkg/model" +) + +const ( + E1 = "luocha-e1" + E2HealBoost = "luocha-e2-healboost" + E2Shield = "luocha-e2-shield" + E4 = "luocha-e4" + E6 = "luocha-e6" +) + +func init() { + modifier.Register(E1, modifier.Config{ + Stacking: modifier.ReplaceBySource, + StatusType: model.StatusType_STATUS_BUFF, + }) + + modifier.Register(E2HealBoost, modifier.Config{ + Stacking: modifier.ReplaceBySource, + Listeners: modifier.Listeners{ + OnBeforeDealHeal: applyE2HealBoost, + }, + }) + + modifier.Register(E2Shield, modifier.Config{ + Stacking: modifier.Replace, + StatusType: model.StatusType_STATUS_BUFF, + CanDispel: true, + Duration: 2, + Listeners: modifier.Listeners{ + OnAdd: func(mod *modifier.Instance) { + mod.Engine().AddShield(E2Shield, info.Shield{ + Source: mod.Source(), + Target: mod.Owner(), + BaseShield: info.ShieldMap{model.ShieldFormula_SHIELD_BY_SHIELDER_ATK: 0.18}, + ShieldValue: 240, + }) + }, + OnRemove: func(mod *modifier.Instance) { + mod.Engine().RemoveShield(E2Shield, mod.Owner()) + }, + }, + }) + + modifier.Register(E4, modifier.Config{ + Stacking: modifier.ReplaceBySource, + StatusType: model.StatusType_STATUS_DEBUFF, + BehaviorFlags: []model.BehaviorFlag{model.BehaviorFlag_STAT_FATIGUE}, + }) + + modifier.Register(E6, modifier.Config{ + Stacking: modifier.ReplaceBySource, + StatusType: model.StatusType_STATUS_DEBUFF, + CanDispel: true, + Duration: 2, + }) +} + +func applyE2HealBoost(mod *modifier.Instance, e *event.HealStart) { + e.Healer.AddProperty(E2HealBoost, prop.HealBoost, 0.3) + mod.RemoveSelf() +} diff --git a/internal/character/luocha/luocha.go b/internal/character/luocha/luocha.go new file mode 100644 index 00000000..f281f068 --- /dev/null +++ b/internal/character/luocha/luocha.go @@ -0,0 +1,57 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine" + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/target/character" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +func init() { + character.Register(key.Luocha, character.Config{ + Create: NewInstance, + Rarity: 5, + Element: model.DamageType_IMAGINARY, + Path: model.Path_ABUNDANCE, + MaxEnergy: 100, + Promotions: promotions, + Traces: traces, + SkillInfo: character.SkillInfo{ + Attack: character.Attack{ + SPAdd: 1, + TargetType: model.TargetType_ENEMIES, + }, + Skill: character.Skill{ + SPNeed: 1, + TargetType: model.TargetType_ALLIES, + }, + Ult: character.Ult{ + TargetType: model.TargetType_ENEMIES, + }, + Technique: character.Technique{ + TargetType: model.TargetType_SELF, + IsAttack: false, + }, + }, + }) +} + +type char struct { + engine engine.Engine + id key.TargetID + info info.Character +} + +func NewInstance(engine engine.Engine, id key.TargetID, charInfo info.Character) info.CharInstance { + c := &char{ + engine: engine, + id: id, + info: charInfo, + } + + c.initInsertSkill() + c.initTraces() + + return c +} diff --git a/internal/character/luocha/skill.go b/internal/character/luocha/skill.go new file mode 100644 index 00000000..245ea86e --- /dev/null +++ b/internal/character/luocha/skill.go @@ -0,0 +1,275 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine" + "github.com/simimpact/srsim/pkg/engine/event" + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/modifier" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +const ( + Skill = "luocha-skill" + InsertSkill = "luocha-insert-skill" + InsertSkillCD = "luocha-insert-skill-cooldown" + InsertSkillRetarget = "luocha-insert-skill-retarget" + InsertSkillMark = "luocha-insert-skill-mark" +) + +// this flag might be a global value, not sure what else it does +// for now, this is commented out everywhere as there is no deeper functionality +// var IsInsert bool + +func init() { + modifier.Register(InsertSkill, modifier.Config{}) + + modifier.Register(InsertSkillCD, modifier.Config{ + TickMoment: modifier.ModifierPhase1End, + }) + + modifier.Register(InsertSkillRetarget, modifier.Config{ + Listeners: modifier.Listeners{ + OnAdd: applyInsertSkillMark, + }, + }) + + modifier.Register(InsertSkillMark, modifier.Config{ + Listeners: modifier.Listeners{ + OnAdd: doInsertSkill, + OnBeforeDying: func(mod *modifier.Instance) { + mod.Engine().RemoveModifier(mod.Source(), InsertSkillRetarget) + }, + }, + }) +} + +func (c *char) initInsertSkill() { + c.engine.Events().HPChange.Subscribe(c.InsertSkillListener) + c.engine.Events().InsertEnd.Subscribe(c.onInsertFinish) +} + +func (c *char) Skill(target key.TargetID, state info.ActionState) { + // Uses helper function + ExecuteSkill(c.engine, c.id, target) +} + +func (c *char) InsertSkillListener(e event.HPChange) { + // Bypass if hp change is positive + if e.NewHP > e.OldHP { + return + } + + // Bypass if on cooldown + if c.engine.HasModifier(c.id, InsertSkillCD) { + return + } + + // Bypass if CC'd or unable to act + if c.engine.HasBehaviorFlag(c.id, model.BehaviorFlag_STAT_CTRL) { + return + } + if c.engine.HasBehaviorFlag(c.id, model.BehaviorFlag_DISABLE_ACTION) { + return + } + + trg := c.engine.Retarget(info.Retarget{ + Targets: c.engine.Characters(), + Filter: func(target key.TargetID) bool { + // Filter conditions as bypasses + // Bypass if HP at or below 0 + if c.engine.HPRatio(target) <= 0 { + return false + } + // Bypass if BattleEventEntity (using workaround) + if !c.engine.IsCharacter(target) { + return false + } + // Bypass if not lowest HP ratio + if !isMinHPRatio(c.engine, target, c.engine.Characters()) { + return false + } + return true + }, + Max: 1, + }) + + if c.engine.HPRatio(trg[0]) <= 0.5 { + // Apply another mod that applies another mod... + c.engine.AddModifier(c.id, info.Modifier{ + Name: InsertSkillRetarget, + Source: c.id, + }) + } +} + +func applyInsertSkillMark(mod *modifier.Instance) { + trg := doRetarget(mod) + + mod.Engine().AddModifier(trg[0], info.Modifier{ + Name: InsertSkillMark, + Source: mod.Source(), + }) +} + +func doInsertSkill(mod *modifier.Instance) { + mod.Engine().InsertAbility(info.Insert{ + Key: InsertSkill, + Execute: func() { + // IsInsert = true + trg := doRetarget(mod) + if mod.Engine().HPRatio(trg[0]) <= 0.5 { + // Apply cooldown mod + mod.Engine().AddModifier(mod.Source(), info.Modifier{ + Name: InsertSkillCD, + Source: mod.Source(), + Duration: 2, + }) + // Remove mark mod on all allies and retarget mod on source + for _, marktrg := range mod.Engine().Characters() { + mod.Engine().RemoveModifier(marktrg, InsertSkillMark) + } + mod.Engine().RemoveModifier(mod.Source(), InsertSkillRetarget) + // Do skill as insert + ExecuteSkill(mod.Engine(), mod.Source(), trg[0]) + } else { + // Remove mark mod on all allies and retarget mod on source + for _, marktrg := range mod.Engine().Characters() { + mod.Engine().RemoveModifier(marktrg, InsertSkillMark) + } + mod.Engine().RemoveModifier(mod.Source(), InsertSkillRetarget) + // IsInsert = false + } + }, + Source: mod.Source(), + AbortFlags: []model.BehaviorFlag{model.BehaviorFlag_STAT_CTRL, model.BehaviorFlag_DISABLE_ACTION}, + Priority: info.CharHealOthers, + }) +} + +func (c *char) onInsertFinish(e event.InsertEnd) { + // Check if CC'd or unable to act + cond1 := c.engine.HasBehaviorFlag(c.id, model.BehaviorFlag_STAT_CTRL) + cond2 := c.engine.HasBehaviorFlag(c.id, model.BehaviorFlag_DISABLE_ACTION) + if cond1 || cond2 { + // Remove mark mod and retarget mod from all allies + for _, trg := range c.engine.Characters() { + c.engine.RemoveModifier(trg, InsertSkillMark) + c.engine.RemoveModifier(trg, InsertSkillRetarget) + } + // IsInsert = false + if c.engine.HasModifier(c.id, TalentInsertMark) { + c.engine.RemoveModifier(c.id, TalentInsertMark) + c.engine.AddModifier(c.id, info.Modifier{ + Name: DisableTalentInsertMark, + Source: c.id, + }) + } + } +} + +// Helper function for executing Skill through c.Skill and through doInsertSkill +func ExecuteSkill(engine engine.Engine, source, target key.TargetID) { + charInfo, _ := engine.CharacterInfo(source) + + // Do A2: Dispel debuff if applicable + if charInfo.Traces["102"] { + engine.DispelStatus(target, info.Dispel{ + Status: model.StatusType_STATUS_DEBUFF, + Order: model.DispelOrder_LAST_ADDED, + Count: 1, + }) + } + + // Do E2: Apply healing or shield based on target's HP ratio + if charInfo.Eidolon >= 2 { + if engine.HPRatio(target) < 0.5 { + engine.AddModifier(source, info.Modifier{ + Name: E2HealBoost, + Source: source, + }) + } else { + engine.AddModifier(target, info.Modifier{ + Name: E2Shield, + Source: source, + }) + } + } + + // Heal target + skillLevelIndex := charInfo.SkillLevelIndex() + engine.Heal(info.Heal{ + Key: Skill, + Targets: []key.TargetID{target}, + Source: source, + BaseHeal: info.HealMap{ + model.HealFormula_BY_HEALER_ATK: skillPer[skillLevelIndex], + }, + HealValue: skillFlat[skillLevelIndex], + }) + + // Modify energy + engine.ModifyEnergy(info.ModifyAttribute{ + Key: Skill, + Target: source, + Source: source, + Amount: 30, + }) + + // Add stack of Abyss Flower if no Field active + if !engine.HasModifier(source, Field) { + engine.AddModifier(source, info.Modifier{ + Name: AbyssFlower, + Source: source, + }) + } + + // Reset insert flag + // if IsInsert { + // IsInsert = false + // } +} + +// Helper function for evaluating whether target is lowest HP ratio out of a list comparetargets +func isMinHPRatio(engine engine.Engine, target key.TargetID, comparetargets []key.TargetID) bool { + // Bypass if empty list + if len(comparetargets) == 0 { + return false + } + + // Start by assuming the first target has the minimum HP ratio + minHPRatio := engine.HPRatio(comparetargets[0]) + + // Loop through all the targets to find the one with the smallest HP ratio + for _, t := range comparetargets[1:] { + if engine.HPRatio(t) < minHPRatio { + minHPRatio = engine.HPRatio(t) + } + } + + // Compare target's HPRatio with the minimum found + return engine.HPRatio(target) == minHPRatio +} + +// Helper function for small retarget (without BattleEventEntity check) +func doRetarget(mod *modifier.Instance) []key.TargetID { + trg := mod.Engine().Retarget(info.Retarget{ + Targets: mod.Engine().Characters(), + Filter: func(target key.TargetID) bool { + // Filter conditions as bypasses + // Bypass if HP at or below 0 + if mod.Engine().HPRatio(target) <= 0 { + return false + } + // No check for BattleEventEntity + // Bypass if not lowest HP ratio + if !isMinHPRatio(mod.Engine(), target, mod.Engine().Characters()) { + return false + } + return true + }, + Max: 1, + }) + + return trg +} diff --git a/internal/character/luocha/stats.go b/internal/character/luocha/stats.go new file mode 100644 index 00000000..a797ff87 --- /dev/null +++ b/internal/character/luocha/stats.go @@ -0,0 +1,105 @@ +package luocha + +var ( + basic = []float64{ + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1, + 1.1, + 1.2, + 1.3, + } + + skillPer = []float64{ + 0.4, + 0.425, + 0.45, + 0.475, + 0.5, + 0.52, + 0.54, + 0.56, + 0.58, + 0.6, + 0.62, + 0.64, + 0.66, + 0.68, + 0.7, + } + + skillFlat = []float64{ + 200, + 320, + 410, + 500, + 560, + 620, + 665, + 710, + 755, + 800, + 845, + 890, + 935, + 980, + 1025, + } + + talentPer = []float64{ + 0.12, + 0.1275, + 0.135, + 0.1425, + 0.15, + 0.156, + 0.162, + 0.168, + 0.174, + 0.18, + 0.186, + 0.192, + 0.198, + 0.204, + 0.21, + } + + talentFlat = []float64{ + 60, + 96, + 123, + 150, + 168, + 186, + 199.5, + 213, + 226.5, + 240, + 253.5, + 267, + 280.5, + 294, + 307.5, + } + + ult = []float64{ + 1.2, + 1.28, + 1.36, + 1.44, + 1.52, + 1.6, + 1.7, + 1.8, + 1.9, + 2, + 2.08, + 2.16, + 2.24, + 2.32, + 2.4, + } +) diff --git a/internal/character/luocha/talent.go b/internal/character/luocha/talent.go new file mode 100644 index 00000000..f4f1789e --- /dev/null +++ b/internal/character/luocha/talent.go @@ -0,0 +1,202 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/event" + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/modifier" + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +const ( + AbyssFlower = "luocha-Abyss-flower" + Field = "luocha-field" + FieldHeal = "luocha-field-heal" + A4 = "luocha-a4" + TalentInsert = "luocha-talent-insert" + TalentInsertMark = "luocha-talent-insert-mark" + DisableTalentInsertMark = "luocha-disable-talent-insert-mark" +) + +type state struct { + talentPer float64 + talentFlat float64 +} + +func (c *char) init() { + modifier.Register(AbyssFlower, modifier.Config{ + Stacking: modifier.ReplaceBySource, + MaxCount: 2, + CountAddWhenStack: 1, + Listeners: modifier.Listeners{ + OnAdd: checkStacks, + }, + }) + + modifier.Register(TalentInsertMark, modifier.Config{ + Listeners: modifier.Listeners{ + OnAdd: doTalentInsert, + }, + }) + + modifier.Register(DisableTalentInsertMark, modifier.Config{ + Listeners: modifier.Listeners{ + OnAdd: modRemoveSubscribe, + }, + }) + + modifier.Register(Field, modifier.Config{ + Stacking: modifier.ReplaceBySource, + Listeners: modifier.Listeners{ + OnAdd: addSubMods, + OnRemove: removeSubMods, + }, + }) + + modifier.Register(FieldHeal, modifier.Config{ + Listeners: modifier.Listeners{ + OnAfterAttack: doFieldHeal, + }, + }) +} + +func checkStacks(mod *modifier.Instance) { + if mod.Count() >= 2 { + mod.Engine().AddModifier(mod.Source(), info.Modifier{ + Name: TalentInsertMark, + Source: mod.Owner(), + }) + } +} + +func doTalentInsert(mod *modifier.Instance) { + mod.Engine().RemoveModifier(mod.Source(), DisableTalentInsertMark) + + luo := mod.Owner() + ci, _ := mod.Engine().CharacterInfo(luo) + mod.Engine().InsertAbility(info.Insert{ + Key: TalentInsert, + Priority: info.CharBuffSelf, + Execute: func() { + mod.Engine().AddModifier(mod.Source(), info.Modifier{ + Name: Field, + Source: luo, + State: state{ + talentPer: talentPer[ci.TalentLevelIndex()], + talentFlat: talentFlat[ci.TalentLevelIndex()], + }, + }) + }, + Source: luo, + AbortFlags: []model.BehaviorFlag{model.BehaviorFlag_STAT_CTRL, model.BehaviorFlag_DISABLE_ACTION}, + }) + + mod.Engine().RemoveModifier(mod.Source(), AbyssFlower) + mod.Engine().RemoveModifier(mod.Source(), TalentInsertMark) +} + +func addSubMods(mod *modifier.Instance) { + // Apply sub modifiers as normal modifiers + st := mod.State().(state) + ci, _ := mod.Engine().CharacterInfo(mod.Owner()) + + for _, trg := range mod.Engine().Characters() { + // Talent and A4 heal + mod.Engine().AddModifier(trg, info.Modifier{ + Name: FieldHeal, + Source: mod.Owner(), + State: state{ + talentPer: st.talentPer, + talentFlat: st.talentFlat, + }, + }) + + // Do E1 + mod.Engine().AddModifier(trg, info.Modifier{ + Name: E1, + Source: mod.Owner(), + Stats: info.PropMap{prop.ATKPercent: 0.2}, + }) + } + + // Do E4 + if ci.Eidolon >= 4 { + for _, trg := range mod.Engine().Enemies() { + mod.Engine().AddModifier(trg, info.Modifier{ + Name: E4, + Source: mod.Owner(), + Stats: info.PropMap{prop.Fatigue: 0.12}, + }) + } + } +} + +func removeSubMods(mod *modifier.Instance) { + // Remove sub modifiers with a workaround + ci, _ := mod.Engine().CharacterInfo(mod.Owner()) + + for _, trg := range mod.Engine().Characters() { + mod.Engine().RemoveModifierFromSource(trg, mod.Source(), FieldHeal) + if ci.Eidolon >= 1 { + mod.Engine().RemoveModifierFromSource(trg, mod.Source(), E1) + } + } + + if ci.Eidolon >= 4 { + for _, trg := range mod.Engine().Enemies() { + mod.Engine().RemoveModifierFromSource(trg, mod.Source(), E4) + } + } +} + +func doFieldHeal(mod *modifier.Instance, e event.AttackEnd) { + st := mod.State().(state) + // Heal self + mod.Engine().Heal(info.Heal{ + Key: FieldHeal, + Targets: []key.TargetID{mod.Owner()}, + Source: mod.Source(), + BaseHeal: info.HealMap{ + model.HealFormula_BY_HEALER_ATK: st.talentPer, + }, + HealValue: st.talentFlat, + }) + + // Do A4: heal other allies + ci, _ := mod.Engine().CharacterInfo(mod.Source()) + if ci.Traces["102"] { + mod.Engine().Heal(info.Heal{ + Key: FieldHeal, + Targets: mod.Engine().Retarget(info.Retarget{ + Targets: mod.Engine().Characters(), + Filter: func(target key.TargetID) bool { + return target != mod.Owner() + }, + }), + Source: mod.Source(), + BaseHeal: info.HealMap{ + model.HealFormula_BY_HEALER_ATK: 0.07, + }, + HealValue: 93, + }) + } +} + +// Function to mimic OnListenModifierRemove +func modRemoveSubscribe(mod *modifier.Instance) { + mod.Engine().Events().ModifierRemoved.Subscribe(func(event event.ModifierRemoved) { + if event.Target == mod.Source() { + cond1 := mod.Engine().HasBehaviorFlag(mod.Source(), model.BehaviorFlag_STAT_CTRL) + cond2 := mod.Engine().HasBehaviorFlag(mod.Source(), model.BehaviorFlag_DISABLE_ACTION) + // Bypass if CC'd or unable to act + if cond1 || cond2 { + return + } + mod.Engine().AddModifier(mod.Source(), info.Modifier{ + Name: TalentInsertMark, + Source: mod.Source(), + }) + } + }) +} diff --git a/internal/character/luocha/technique.go b/internal/character/luocha/technique.go new file mode 100644 index 00000000..ae3ec1dc --- /dev/null +++ b/internal/character/luocha/technique.go @@ -0,0 +1,16 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/key" +) + +const Technique = "luocha-technique" + +func (c *char) Technique(target key.TargetID, state info.ActionState) { + c.engine.AddModifier(c.id, info.Modifier{ + Name: AbyssFlower, + Source: c.id, + Count: 2, + }) +} diff --git a/internal/character/luocha/trace.go b/internal/character/luocha/trace.go new file mode 100644 index 00000000..60be03e7 --- /dev/null +++ b/internal/character/luocha/trace.go @@ -0,0 +1,23 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/modifier" + "github.com/simimpact/srsim/pkg/model" +) + +const A6 = "luocha-a6" + +func init() { + modifier.Register(A6, modifier.Config{}) +} + +func (c *char) initTraces() { + if c.info.Traces["103"] { + c.engine.AddModifier(c.id, info.Modifier{ + Name: A6, + Source: c.id, + DebuffRES: info.DebuffRESMap{model.BehaviorFlag_STAT_CTRL: 0.7}, + }) + } +} diff --git a/internal/character/luocha/ult.go b/internal/character/luocha/ult.go new file mode 100644 index 00000000..01708b03 --- /dev/null +++ b/internal/character/luocha/ult.go @@ -0,0 +1,56 @@ +package luocha + +import ( + "github.com/simimpact/srsim/pkg/engine/info" + "github.com/simimpact/srsim/pkg/engine/prop" + "github.com/simimpact/srsim/pkg/key" + "github.com/simimpact/srsim/pkg/model" +) + +const ( + Ult = "luocha-ult" +) + +func (c *char) Ult(target key.TargetID, state info.ActionState) { + // Do E6 + if c.info.Eidolon >= 6 { + for _, trg := range c.engine.Enemies() { + c.engine.AddModifier(trg, info.Modifier{ + Name: E6, + Source: c.id, + Stats: info.PropMap{prop.AllDamageRES: 0.2}, + }) + } + } + + // Dispel 1 buff on all enemies + for _, trg := range c.engine.Enemies() { + c.engine.DispelStatus(trg, info.Dispel{ + Status: model.StatusType_STATUS_BUFF, + Order: model.DispelOrder_LAST_ADDED, + Count: 1, + }) + } + + // Add 1 stack of Abyss Flower if no Field active + if !c.engine.HasModifier(c.id, Field) { + c.engine.AddModifier(c.id, info.Modifier{ + Name: AbyssFlower, + Source: c.id, + }) + } + + // Do damage + c.engine.Attack(info.Attack{ + Key: Ult, + AttackType: model.AttackType_ULT, + DamageType: model.DamageType_IMAGINARY, + BaseDamage: info.DamageMap{ + model.DamageFormula_BY_ATK: ult[c.info.UltLevelIndex()], + }, + Targets: c.engine.Enemies(), + Source: c.id, + EnergyGain: 5, + StanceDamage: 60, + }) +} diff --git a/pkg/key/character.go b/pkg/key/character.go index e8c74ecb..d856acd2 100644 --- a/pkg/key/character.go +++ b/pkg/key/character.go @@ -31,6 +31,7 @@ const ( Seele Character = "seele" Huohuo Character = "huohuo" Xueyi Character = "Xueyi" + Luocha Character = "luocha" ) func (c Character) String() string { diff --git a/pkg/simulation/imports.go b/pkg/simulation/imports.go index 78e8c59e..7560a29a 100644 --- a/pkg/simulation/imports.go +++ b/pkg/simulation/imports.go @@ -19,6 +19,7 @@ import ( _ "github.com/simimpact/srsim/internal/character/jingliu" _ "github.com/simimpact/srsim/internal/character/kafka" _ "github.com/simimpact/srsim/internal/character/luka" + _ "github.com/simimpact/srsim/internal/character/luocha" _ "github.com/simimpact/srsim/internal/character/march7th" _ "github.com/simimpact/srsim/internal/character/natasha" _ "github.com/simimpact/srsim/internal/character/pela"