diff --git a/internal/tests/reactable/mutable_test.go b/internal/tests/reactable/mutable_test.go new file mode 100644 index 0000000000..4756fbe6b0 --- /dev/null +++ b/internal/tests/reactable/mutable_test.go @@ -0,0 +1,75 @@ +package reactable_test + +import ( + "math" + "testing" + + "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/combat" + "github.com/genshinsim/gcsim/pkg/core/event" + "github.com/genshinsim/gcsim/pkg/core/geometry" + "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/enemy" + "github.com/genshinsim/gcsim/pkg/reactable" +) + +func TestNonMutableVape(t *testing.T) { + c, _ := makeCore(0) + + // create enemy with hydro aura + trg := enemy.New(c, info.EnemyProfile{ + Level: 100, + Resist: make(map[attributes.Element]float64), + Pos: info.Coord{ + X: 0, + Y: 0, + R: 1, + }, + Element: attributes.Hydro, + ElementDurability: 25, + }) + c.Combat.AddEnemy(trg) + + err := c.Init() + if err != nil { + t.Errorf("error initializing core: %v", err) + t.FailNow() + } + + count := 0 + c.Events.Subscribe(event.OnVaporize, func(args ...interface{}) bool { + count++ + return false + }, "vaporize") + + c.QueueAttackEvent(&combat.AttackEvent{ + Info: combat.AttackInfo{ + Element: attributes.Pyro, + Durability: 25, + }, + Pattern: combat.NewCircleHitOnTarget(geometry.Point{}, nil, 100), + }, 0) + advanceCoreFrame(c) + + if float64(trg.Durability[reactable.Pyro]) > 0.000001 { + t.Errorf( + "expected pyro=%v, got pyro=%v", + 0, + trg.Durability[reactable.Pyro], + ) + } + if math.Abs(float64(trg.Durability[reactable.Hydro])-25) > 0.000001 { + t.Errorf( + "expected hydro=%v, got hydro=%v", + 25, + trg.Durability[reactable.Hydro], + ) + } + if count != 1 { + t.Errorf( + "expected %v vaporizes, got %v", + 1, + count, + ) + } +} diff --git a/internal/tests/reactable/reactable_test.go b/internal/tests/reactable/reactable_test.go index 6e5f6ce325..16e9cc51c9 100644 --- a/internal/tests/reactable/reactable_test.go +++ b/internal/tests/reactable/reactable_test.go @@ -37,6 +37,7 @@ func makeCore(trgCount int) (*core.Core, []*enemy.Enemy) { Y: 0, R: 1, }, + Element: attributes.NoElement, }) trgs = append(trgs, e) c.Combat.AddEnemy(e) diff --git a/pkg/core/info/enemy.go b/pkg/core/info/enemy.go index a32d8c4a7b..145f143128 100644 --- a/pkg/core/info/enemy.go +++ b/pkg/core/info/enemy.go @@ -2,6 +2,7 @@ package info import ( "github.com/genshinsim/gcsim/pkg/core/attributes" + "github.com/genshinsim/gcsim/pkg/core/reactions" "github.com/genshinsim/gcsim/pkg/model" ) @@ -19,6 +20,8 @@ type EnemyProfile struct { HpGrowCurve model.MonsterCurveType `json:"-"` Id int `json:"-"` MonsterName string `json:"monster_name"` + Element attributes.Element `json:"element"` + ElementDurability reactions.Durability `json:"element_durability"` Modified bool `json:"modified"` } diff --git a/pkg/enemy/enemy.go b/pkg/enemy/enemy.go index a14e0f3557..46b98d4241 100644 --- a/pkg/enemy/enemy.go +++ b/pkg/enemy/enemy.go @@ -7,6 +7,7 @@ import ( "github.com/genshinsim/gcsim/pkg/core/geometry" "github.com/genshinsim/gcsim/pkg/core/glog" "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/core/reactions" "github.com/genshinsim/gcsim/pkg/core/targets" "github.com/genshinsim/gcsim/pkg/modifier" "github.com/genshinsim/gcsim/pkg/queue" @@ -53,6 +54,24 @@ func New(core *core.Core, p info.EnemyProfile) *Enemy { e.hp = p.HP e.maxhp = p.HP } + if p.Element != attributes.NoElement && p.ElementDurability > 0 { + e.ApplySelfInfusion(p.Element, p.ElementDurability, -1) + + var mod reactable.Modifier + switch p.Element { + case attributes.Electro: + mod = reactable.Electro + case attributes.Hydro: + mod = reactable.Hydro + case attributes.Pyro: + mod = reactable.Pyro + case attributes.Cryo: + mod = reactable.Cryo + case attributes.Dendro: + mod = reactable.Dendro + } + e.Reactable.Mutable[mod] = false + } return e } @@ -81,3 +100,42 @@ func (e *Enemy) SetDirectionToClosestEnemy() {} func (e *Enemy) CalcTempDirection(trg geometry.Point) geometry.Point { return geometry.DefaultDirection() } + +func (e *Enemy) ApplySelfInfusion(ele attributes.Element, dur reactions.Durability, f int) { + e.Core.Log.NewEventBuildMsg(glog.LogEnemyEvent, -1, "self infusion applied to enemy: "+ele.String()). + Write("index", e.Key()). + Write("durability", dur). + Write("duration", f) + // we're assuming self infusion isn't subject to 0.8x multiplier + // also no real sanity check + if ele == attributes.Frozen { + return + } + var mod reactable.Modifier + switch ele { + case attributes.Electro: + mod = reactable.Electro + case attributes.Hydro: + mod = reactable.Hydro + case attributes.Pyro: + mod = reactable.Pyro + case attributes.Cryo: + mod = reactable.Cryo + case attributes.Dendro: + mod = reactable.Dendro + } + + // we're assuming refill maintains the same decay rate? + if e.Durability[mod] > reactable.ZeroDur { + // make sure we're not adding more than incoming + if e.Durability[mod] < dur { + e.Durability[mod] = dur + } + return + } + // otherwise calculate decay based on specified f (in frames) + e.Durability[mod] = dur + if f > 0 { + e.DecayRate[mod] = dur / reactions.Durability(f) + } +} diff --git a/pkg/enemy/types.go b/pkg/enemy/types.go index 42007c1b21..0cc169338a 100644 --- a/pkg/enemy/types.go +++ b/pkg/enemy/types.go @@ -49,6 +49,8 @@ func ConfigureTarget(profile *info.EnemyProfile, name string, params TargetParam enemyInfo.ParticleDropCount = profile.ParticleDropCount enemyInfo.ParticleElement = profile.ParticleElement enemyInfo.ParticleDrops = []*model.MonsterHPDrop{} + } else { + enemyInfo.ParticleElement = attributes.NoElement } *profile = enemyInfo return nil @@ -92,5 +94,6 @@ func getMonsterInfo(name string) (info.EnemyProfile, error) { HpGrowCurve: result.BaseStats.HpCurve, Id: int(result.Id), MonsterName: result.Key, + Element: attributes.NoElement, }, nil } diff --git a/pkg/gcs/ast/item.go b/pkg/gcs/ast/item.go index dac99d53cd..747c69666a 100644 --- a/pkg/gcs/ast/item.go +++ b/pkg/gcs/ast/item.go @@ -109,6 +109,8 @@ const ( keywordParticleDropCount // particle_drop_count keywordParticleElement // particle_element keywordHurt // hurt + keywordElement // element + keywordElementDurability // element_durability // Keywords specific to gcsim appears after this itemKeys diff --git a/pkg/gcs/ast/keys.go b/pkg/gcs/ast/keys.go index b3d5e8fd37..28c336285c 100644 --- a/pkg/gcs/ast/keys.go +++ b/pkg/gcs/ast/keys.go @@ -43,6 +43,8 @@ var key = map[string]TokenType{ "resist": keywordResist, "energy": keywordEnergy, "hurt": keywordHurt, + "element": keywordElement, + "element_durability": keywordElementDurability, // commands // team keywords // flags diff --git a/pkg/gcs/ast/parseTarget.go b/pkg/gcs/ast/parseTarget.go index 2560255608..2586b6f08f 100644 --- a/pkg/gcs/ast/parseTarget.go +++ b/pkg/gcs/ast/parseTarget.go @@ -6,6 +6,7 @@ import ( "github.com/genshinsim/gcsim/pkg/core/attributes" "github.com/genshinsim/gcsim/pkg/core/info" + "github.com/genshinsim/gcsim/pkg/core/reactions" "github.com/genshinsim/gcsim/pkg/enemy" ) @@ -14,6 +15,7 @@ func parseTarget(p *Parser) (parseFn, error) { var r info.EnemyProfile r.Resist = make(map[attributes.Element]float64) r.ParticleElement = attributes.NoElement + r.Element = attributes.NoElement for n := p.next(); n.Typ != itemEOF; n = p.next() { switch n.Typ { case itemIdentifier: @@ -122,7 +124,6 @@ func parseTarget(p *Parser) (parseFn, error) { } r.ParticleDropThreshold = amt r.ParticleDrops = nil // separate particle system - r.ParticleElement = attributes.NoElement r.Modified = true case keywordParticleDropCount: item, err := p.acceptSeqReturnLast(itemAssign, itemNumber) @@ -157,6 +158,26 @@ func parseTarget(p *Parser) (parseFn, error) { r.Resist[eleKeys[s]] += amt r.Modified = true + case keywordElement: + item, err := p.acceptSeqReturnLast(itemAssign, itemElementKey) + if err != nil { + return nil, err + } + if ele, ok := eleKeys[item.Val]; ok { + r.Element = ele + } + r.Modified = true + case keywordElementDurability: + item, err := p.acceptSeqReturnLast(itemAssign, itemNumber) + if err != nil { + return nil, err + } + dur, err := itemNumberToFloat64(item) + if err != nil { + return nil, err + } + r.ElementDurability = reactions.Durability(dur) + r.Modified = true case itemTerminateLine: p.res.Targets = append(p.res.Targets, r) return parseRows, nil diff --git a/pkg/reactable/electrocharged.go b/pkg/reactable/electrocharged.go index 6cc34a9d29..bdd90e39a2 100644 --- a/pkg/reactable/electrocharged.go +++ b/pkg/reactable/electrocharged.go @@ -118,10 +118,14 @@ func (r *Reactable) TryAddEC(a *combat.AttackEvent) bool { } func (r *Reactable) waneEC() { - r.Durability[Electro] -= 10 - r.Durability[Electro] = max(0, r.Durability[Electro]) - r.Durability[Hydro] -= 10 - r.Durability[Hydro] = max(0, r.Durability[Hydro]) + if r.Mutable[Electro] { + r.Durability[Electro] -= 10 + r.Durability[Electro] = max(0, r.Durability[Electro]) + } + if r.Mutable[Hydro] { + r.Durability[Hydro] -= 10 + r.Durability[Hydro] = max(0, r.Durability[Hydro]) + } r.core.Log.NewEvent("ec wane", glog.LogElementEvent, -1, diff --git a/pkg/reactable/reactable.go b/pkg/reactable/reactable.go index 5811471e05..96bbc722b3 100644 --- a/pkg/reactable/reactable.go +++ b/pkg/reactable/reactable.go @@ -100,6 +100,7 @@ func (r *Modifier) UnmarshalJSON(b []byte) error { type Reactable struct { Durability [EndModifier]reactions.Durability DecayRate [EndModifier]reactions.Durability + Mutable [EndModifier]bool // Source []int //source frame of the aura self combat.Target core *core.Core @@ -134,6 +135,10 @@ const frzDecayCap reactions.Durability = 10.0 / 60.0 const ZeroDur reactions.Durability = 0.00000000001 func (r *Reactable) Init(self combat.Target, c *core.Core) *Reactable { + for i := Invalid; i < EndModifier; i++ { + r.Mutable[i] = true + } + r.self = self r.core = c r.DecayRate[Frozen] = frzDecayCap @@ -250,6 +255,9 @@ func (r *Reactable) attachOrRefillNormalEle(mod Modifier, dur reactions.Durabili } func (r *Reactable) attachOverlap(mod Modifier, amt, length reactions.Durability) { + if !r.Mutable[mod] { + return + } if r.Durability[mod] > ZeroDur { add := max(amt-r.Durability[mod], 0) if add > 0 { @@ -264,6 +272,9 @@ func (r *Reactable) attachOverlap(mod Modifier, amt, length reactions.Durability } func (r *Reactable) attachOverlapRefreshDuration(mod Modifier, amt, length reactions.Durability) { + if !r.Mutable[mod] { + return + } if amt < r.Durability[mod] { return } @@ -277,6 +288,9 @@ func (r *Reactable) attachBurning() { } func (r *Reactable) addDurability(mod Modifier, amt reactions.Durability) { + if !r.Mutable[mod] { + return + } r.Durability[mod] += amt r.core.Events.Emit(event.OnAuraDurabilityAdded, r.self, mod, amt) } @@ -329,7 +343,9 @@ func (r *Reactable) reduce(e attributes.Element, dur, factor reactions.Durabilit // reset decay rate to 0 } - r.Durability[i] -= red + if r.Mutable[i] { + r.Durability[i] -= red + } if red > reduced { reduced = red @@ -362,7 +378,7 @@ func (r *Reactable) Tick() { if r.DecayRate[i] == 0 { continue } - if r.Durability[i] > ZeroDur { + if r.Durability[i] > ZeroDur && r.Mutable[i] { r.Durability[i] -= r.DecayRate[i] r.deplete(i) } @@ -387,6 +403,9 @@ func (r *Reactable) Tick() { if r.Durability[i] < ZeroDur { continue } + if !r.Mutable[i] { + continue + } rate := r.DecayRate[i] if r.Durability[BurningFuel] > ZeroDur { rate = r.DecayRate[BurningFuel]