Repository: genshinsim/gcsim
Pull request #1069 (2022-11-04 19:56:13)

DMC implementation continued
Continuation of #877 by @Charlie-Zheng with additional changes


Patch:
diff --git a/app/src/Components/Character/characters.tsx b/app/src/Components/Character/characters.tsx
index ede3ccd49..207faa1d0 100644
--- a/app/src/Components/Character/characters.tsx
+++ b/app/src/Components/Character/characters.tsx
@@ -126,6 +126,11 @@ export const characterKeyToICharacter: { [key: string]: ICharacter } = {
     element: 'geo',
     weapon_type: 'Sword',
   },
+  travelerdendro: {
+    key: 'travelerdendro',
+    element: 'dendro',
+    weapon_type: 'Sword',
+  },
   nahida: {
     key: 'nahida',
     element: 'dendro',
diff --git a/internal/characters/travelerdendro/asc.go b/internal/characters/travelerdendro/asc.go
new file mode 100644
index 000000000..99e4c1420
--- /dev/null
+++ b/internal/characters/travelerdendro/asc.go
@@ -0,0 +1,68 @@
+package travelerdendro
+
+import (
+	"github.com/genshinsim/gcsim/pkg/core/attributes"
+	"github.com/genshinsim/gcsim/pkg/core/combat"
+	"github.com/genshinsim/gcsim/pkg/core/event"
+	"github.com/genshinsim/gcsim/pkg/core/player/character"
+	"github.com/genshinsim/gcsim/pkg/modifier"
+)
+
+const a1Key = "dmc-a1"
+
+// Gets removed on swap - from Kolibri
+func (c *char) a1Init() {
+	c.Core.Events.Subscribe(event.OnCharacterSwap, func(args ...interface{}) bool {
+		prev := args[0].(int)
+		prevChar := c.Core.Player.ByIndex(prev)
+		prevChar.DeleteStatMod(a1Key)
+		return false
+	}, "dmc-a1-remove")
+}
+
+func (c *char) a1Buff(delay int) {
+	m := make([]float64, attributes.EndStatType)
+	// A1/C6 buff ticks every 0.3s and applies for 1s. probably counting from gadget spawn - from Kolibri
+	c.Core.Tasks.Add(func() {
+		if c.Core.Status.Duration(burstKey) > 0 { //burst isn't expired
+			active := c.Core.Player.ActiveChar()
+			m[attributes.EM] = float64(6 * c.burstOverflowingLotuslight)
+			active.AddStatMod(character.StatMod{
+				Base:         modifier.NewBase(a1Key, 60),
+				AffectedStat: attributes.EM,
+				Amount: func() ([]float64, bool) {
+					return m, true
+				},
+			})
+		}
+	}, delay)
+}
+
+func (c *char) a1Stack(delay int) {
+	c.Core.Tasks.Add(func() {
+		if c.Core.Status.Duration(burstKey) > 0 && c.burstOverflowingLotuslight < 10 { //burst isn't expired, and stacks aren't capped
+			c.burstOverflowingLotuslight += 1
+		}
+	}, delay)
+}
+
+// Every point of Elemental Mastery the Traveler possesses increases the DMG dealt
+// by Razorgrass Blade by 0.15% and the DMG dealt by Surgent Manifestation by 0.1%.
+func (c *char) a4Init() {
+	m := make([]float64, attributes.EndStatType)
+	c.AddAttackMod(character.AttackMod{
+		Base: modifier.NewBaseWithHitlag("dmc-a4", -1),
+		Amount: func(atk *combat.AttackEvent, _ combat.Target) ([]float64, bool) {
+			switch atk.Info.AttackTag {
+			case combat.AttackTagElementalArt:
+				m[attributes.DmgP] = c.Stat(attributes.EM) * 0.0015
+				return m, true
+			case combat.AttackTagElementalBurst:
+				m[attributes.DmgP] = c.Stat(attributes.EM) * 0.001
+				return m, true
+			default:
+				return nil, false
+			}
+		},
+	})
+}
diff --git a/internal/characters/travelerdendro/attack.go b/internal/characters/travelerdendro/attack.go
new file mode 100644
index 000000000..e4644f9f0
--- /dev/null
+++ b/internal/characters/travelerdendro/attack.go
@@ -0,0 +1,88 @@
+package travelerdendro
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/internal/frames"
+	"github.com/genshinsim/gcsim/pkg/core/action"
+	"github.com/genshinsim/gcsim/pkg/core/attributes"
+	"github.com/genshinsim/gcsim/pkg/core/combat"
+)
+
+var attackFrames [][][]int
+var attackHitmarks = [][]int{{13, 13, 16, 30, 25}, {16, 10, 19, 23, 14}}
+var attackHitlagHaltFrame = [][]float64{{0.03, 0.03, 0.06, 0.09, 0.12}, {0.03, 0.03, 0.06, 0.06, 0.10}}
+
+const normalHitNum = 5
+
+func init() {
+	attackFrames = make([][][]int, 2)
+
+	// Male
+	attackFrames[0] = make([][]int, normalHitNum)
+
+	attackFrames[0][0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 28) // N1 -> CA
+	attackFrames[0][0][action.ActionAttack] = 17                                // N1 -> N2
+
+	attackFrames[0][1] = frames.InitNormalCancelSlice(attackHitmarks[0][1], 28) // N2 -> CA
+	attackFrames[0][1][action.ActionAttack] = 26                                // N2 -> N3
+
+	attackFrames[0][2] = frames.InitNormalCancelSlice(attackHitmarks[0][2], 36) // N3 -> CA
+	attackFrames[0][2][action.ActionAttack] = 32                                // N3 -> N4
+
+	attackFrames[0][3] = frames.InitNormalCancelSlice(attackHitmarks[0][3], 45) // N4 -> CA
+	attackFrames[0][3][action.ActionAttack] = 39                                // N4 -> N5
+
+	attackFrames[0][4] = frames.InitNormalCancelSlice(attackHitmarks[0][4], 69) // N5 -> N1
+	attackFrames[0][4][action.ActionCharge] = 500                               // N5 -> CA, TODO: this action is illegal; need better way to handle it
+
+	// Female
+	attackFrames[1] = make([][]int, normalHitNum)
+
+	attackFrames[1][0] = frames.InitNormalCancelSlice(attackHitmarks[1][0], 32) // N1 -> CA
+	attackFrames[1][0][action.ActionAttack] = 24                                // N1 -> N2
+
+	attackFrames[1][1] = frames.InitNormalCancelSlice(attackHitmarks[1][1], 23) // N2 -> CA
+	attackFrames[1][1][action.ActionAttack] = 21                                // N2 -> N3
+
+	attackFrames[1][2] = frames.InitNormalCancelSlice(attackHitmarks[1][2], 39) // N3 -> CA
+	attackFrames[1][2][action.ActionAttack] = 27                                // N3 -> N4
+
+	attackFrames[1][3] = frames.InitNormalCancelSlice(attackHitmarks[1][3], 45) // N4 -> CA
+	attackFrames[1][3][action.ActionAttack] = 38                                // N4 -> N5
+
+	attackFrames[1][4] = frames.InitNormalCancelSlice(attackHitmarks[1][4], 64) // N5 -> N1
+	attackFrames[1][4][action.ActionCharge] = 500                               // N5 -> CA, TODO: this action is illegal; need better way to handle it
+}
+
+func (c *char) Attack(p map[string]int) action.ActionInfo {
+	ai := combat.AttackInfo{
+		ActorIndex:         c.Index,
+		Abil:               fmt.Sprintf("Normal %v", c.NormalCounter),
+		AttackTag:          combat.AttackTagNormal,
+		ICDTag:             combat.ICDTagNormalAttack,
+		ICDGroup:           combat.ICDGroupDefault,
+		StrikeType:         combat.StrikeTypeSlash,
+		Element:            attributes.Physical,
+		Durability:         25,
+		Mult:               attack[c.NormalCounter][c.TalentLvlAttack()],
+		HitlagFactor:       0.01,
+		HitlagHaltFrames:   attackHitlagHaltFrame[c.gender][c.NormalCounter] * 60,
+		CanBeDefenseHalted: true,
+	}
+	c.Core.QueueAttack(
+		ai,
+		combat.NewCircleHit(c.Core.Combat.Player(), 0.3),
+		attackHitmarks[c.gender][c.NormalCounter],
+		attackHitmarks[c.gender][c.NormalCounter],
+	)
+
+	defer c.AdvanceNormalIndex()
+
+	return action.ActionInfo{
+		Frames:          frames.NewAttackFunc(c.Character, attackFrames[c.gender]),
+		AnimationLength: attackFrames[c.gender][c.NormalCounter][action.InvalidAction],
+		CanQueueAfter:   attackHitmarks[c.gender][c.NormalCounter],
+		State:           action.NormalAttackState,
+	}
+}
diff --git a/internal/characters/travelerdendro/burst.go b/internal/characters/travelerdendro/burst.go
new file mode 100644
index 000000000..a348b2f74
--- /dev/null
+++ b/internal/characters/travelerdendro/burst.go
@@ -0,0 +1,61 @@
+package travelerdendro
+
+import (
+	"github.com/genshinsim/gcsim/internal/frames"
+	"github.com/genshinsim/gcsim/pkg/core/action"
+)
+
+var burstFrames [][]int
+
+const (
+	burstKey       = "travelerdendro-q"
+	burstHitmark   = 91
+	leaLotusAppear = 54
+)
+
+func init() {
+	burstFrames = make([][]int, 2)
+
+	// Male
+	burstFrames[0] = frames.InitAbilSlice(58)
+	burstFrames[0][action.ActionSwap] = 57 // Q -> Swap
+
+	// Female
+	burstFrames[1] = frames.InitAbilSlice(58)
+	burstFrames[1][action.ActionSwap] = 57 // Q -> Swap
+}
+
+func (c *char) Burst(p map[string]int) action.ActionInfo {
+
+	c.SetCD(action.ActionBurst, 1200)
+	c.ConsumeEnergy(2)
+
+	// Duration counts from first hitmark
+
+	c.Core.Tasks.Add(func() {
+		s := c.newLeaLotusLamp()
+		// A1 adds a stack per second
+		for delay := 0; delay <= s.Gadget.Duration; delay += 60 {
+			c.a1Stack(delay)
+		}
+
+		// A1/C6 buff ticks every 0.3s and applies for 1s. probably counting from gadget spawn - Kolibri
+		for delay := 0; delay <= s.Gadget.Duration; delay += 0.3 * 60 {
+			c.a1Buff(delay)
+		}
+
+		if c.Base.Cons >= 6 {
+			for delay := 0; delay <= s.Gadget.Duration; delay += 0.3 * 60 {
+				c.c6Buff(delay)
+			}
+		}
+		c.Core.Combat.AddGadget(s)
+	}, leaLotusAppear)
+
+	return action.ActionInfo{
+		Frames:          frames.NewAbilFunc(burstFrames[c.gender]),
+		AnimationLength: burstFrames[c.gender][action.InvalidAction],
+		CanQueueAfter:   burstFrames[c.gender][action.ActionSwap], // earliest cancel
+		State:           action.BurstState,
+	}
+}
diff --git a/internal/characters/travelerdendro/charge.go b/internal/characters/travelerdendro/charge.go
new file mode 100644
index 000000000..4749a43c7
--- /dev/null
+++ b/internal/characters/travelerdendro/charge.go
@@ -0,0 +1,61 @@
+﻿package travelerdendro
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/internal/frames"
+	"github.com/genshinsim/gcsim/pkg/core/action"
+	"github.com/genshinsim/gcsim/pkg/core/attributes"
+	"github.com/genshinsim/gcsim/pkg/core/combat"
+)
+
+var chargeFrames [][]int
+var chargeHitmarks = [][]int{{9, 20}, {14, 25}}
+
+func init() {
+	chargeFrames = make([][]int, 2)
+	// Male
+	chargeFrames[0] = frames.InitAbilSlice(55)                                       // CA -> N1
+	chargeFrames[0][action.ActionSkill] = 37                                         // CA -> E
+	chargeFrames[0][action.ActionBurst] = 36                                         // CA -> Q
+	chargeFrames[0][action.ActionDash] = chargeHitmarks[0][len(chargeHitmarks[0])-1] // CA -> D
+	chargeFrames[0][action.ActionJump] = chargeHitmarks[0][len(chargeHitmarks[0])-1] // CA -> J
+	chargeFrames[0][action.ActionSwap] = 44                                          // CA -> Swap
+
+	// Female
+	chargeFrames[1] = frames.InitAbilSlice(58)                                       // CA -> N1
+	chargeFrames[1][action.ActionSkill] = 34                                         // CA -> E
+	chargeFrames[1][action.ActionBurst] = 35                                         // CA -> Q
+	chargeFrames[1][action.ActionDash] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> D
+	chargeFrames[1][action.ActionJump] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> J
+	chargeFrames[1][action.ActionSwap] = chargeHitmarks[1][len(chargeHitmarks[1])-1] // CA -> Swap
+}
+
+func (c *char) ChargeAttack(p map[string]int) action.ActionInfo {
+	ai := combat.AttackInfo{
+		ActorIndex: c.Index,
+		AttackTag:  combat.AttackTagExtra,
+		ICDTag:     combat.ICDTagNormalAttack,
+		ICDGroup:   combat.ICDGroupDefault,
+		Element:    attributes.Physical,
+		Durability: 25,
+	}
+
+	for i, mult := range charge[c.gender] {
+		ai.Mult = mult[c.TalentLvlAttack()]
+		ai.Abil = fmt.Sprintf("Charge %v", i)
+		c.Core.QueueAttack(
+			ai,
+			combat.NewCircleHit(c.Core.Combat.Player(), 2),
+			chargeHitmarks[c.gender][i],
+			chargeHitmarks[c.gender][i],
+		)
+	}
+
+	return action.ActionInfo{
+		Frames:          frames.NewAbilFunc(chargeFrames[c.gender]),
+		AnimationLength: chargeFrames[c.gender][action.InvalidAction],
+		CanQueueAfter:   chargeHitmarks[c.gender][len(chargeHitmarks[c.gender])-1],
+		State:           action.ChargeAttackState,
+	}
+}
diff --git a/internal/characters/travelerdendro/cons.go b/internal/characters/travelerdendro/cons.go
new file mode 100644
index 000000000..5f47df68e
--- /dev/null
+++ b/internal/characters/travelerdendro/cons.go
@@ -0,0 +1,66 @@
+package travelerdendro
+
+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/glog"
+	"github.com/genshinsim/gcsim/pkg/core/player/character"
+	"github.com/genshinsim/gcsim/pkg/modifier"
+)
+
+func (c *char) c1cb() func(a combat.AttackCB) {
+	return func(a combat.AttackCB) {
+		if c.skillC1 {
+			c.AddEnergy("dmc-c1", 3.5)
+			c.skillC1 = false
+		}
+
+	}
+}
+
+func (c *char) c4() {
+	c.burstOverflowingLotuslight += 5
+	if c.burstOverflowingLotuslight > 10 {
+		c.burstOverflowingLotuslight = 10
+	}
+	c.Core.Log.NewEvent("dmc-c4-triggered", glog.LogCharacterEvent, c.Index)
+}
+
+// Gets removed on swap - from Kolibri
+func (c *char) c6Init() {
+	c.Core.Events.Subscribe(event.OnCharacterSwap, func(args ...interface{}) bool {
+		prev := args[0].(int)
+		prevChar := c.Core.Player.ByIndex(prev)
+		prevChar.DeleteStatMod("dmc-c6")
+		return false
+	}, "dmc-c6-remove")
+}
+
+func (c *char) c6Buff(delay int) {
+	m := make([]float64, attributes.EndStatType)
+	// A1/C6 buff ticks every 0.3s and applies for 1s. probably counting from gadget spawn - from Kolibri
+	c.Core.Tasks.Add(func() {
+		if c.Core.Status.Duration(burstKey) > 0 { //burst isn't expired
+			active := c.Core.Player.ActiveChar()
+			m[attributes.DendroP] = 0.12
+			if c.burstTransfig != attributes.NoElement {
+				switch c.burstTransfig {
+				case attributes.Hydro:
+					m[attributes.HydroP] = 0.12
+				case attributes.Electro:
+					m[attributes.ElectroP] = 0.12
+				case attributes.Pyro:
+					m[attributes.PyroP] = 0.12
+				}
+
+			}
+			active.AddStatMod(character.StatMod{
+				Base: modifier.NewBaseWithHitlag("dmc-c6", 60),
+				Amount: func() ([]float64, bool) {
+					return m, true
+				},
+			})
+		}
+	}, delay)
+}
diff --git a/internal/characters/travelerdendro/lealotus.go b/internal/characters/travelerdendro/lealotus.go
new file mode 100644
index 000000000..dabb409bd
--- /dev/null
+++ b/internal/characters/travelerdendro/lealotus.go
@@ -0,0 +1,252 @@
+package travelerdendro
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+	"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/glog"
+	"github.com/genshinsim/gcsim/pkg/gadget"
+	"github.com/genshinsim/gcsim/pkg/reactable"
+)
+
+type LeaLotus struct {
+	*gadget.Gadget
+	*reactable.Reactable
+	burstAtk        *combat.AttackEvent
+	char            *char
+	targetingRadius float64
+	hitboxRadius    float64
+}
+
+func (s *LeaLotus) AuraContains(e ...attributes.Element) bool {
+	for ele := range e {
+		if s.Reactable.Durability[ele] <= reactable.ZeroDur {
+			return false
+		}
+	}
+	return true
+}
+
+func (c *char) newLeaLotusLamp() *LeaLotus {
+	s := &LeaLotus{}
+	x, y := c.Core.Combat.Player().Pos()
+	// TODO The gadget spawns 1 unit away from the player (in the direction the player is facing?)
+	s.Gadget = gadget.New(c.Core, core.Coord{X: x, Y: y, R: 1}, combat.GadgetTypLeaLotus)
+	s.Reactable = &reactable.Reactable{}
+	s.Reactable.Init(s, c.Core)
+	s.Durability[reactable.ModifierDendro] = 10
+
+	s.Duration = 12 * 60
+	if c.Base.Cons >= 2 {
+		s.Duration += 3 * 60
+	}
+
+	//burst status last the duration of the gadget but is removed if pyro applied
+	c.Core.Status.Add(burstKey, s.Duration)
+
+	// First hitmark is 37f after spawn, all other pre-transfig hits will be 90f between.
+	c.Core.Tasks.Add(func() {
+		if !s.Alive {
+			return
+		}
+		s.QueueAttack(0)
+		//repeat attack every 90
+		s.Gadget.OnThinkInterval = func() {
+			s.QueueAttack(0)
+		}
+		s.Gadget.ThinkInterval = 90
+	}, burstHitmark-leaLotusAppear)
+
+	c.burstTransfig = attributes.NoElement
+	s.char = c
+
+	s.targetingRadius = 8
+	s.hitboxRadius = 2
+	c.burstOverflowingLotuslight = 0
+
+	procAI := combat.AttackInfo{
+		ActorIndex: c.Index,
+		Abil:       "Lea Lotus Lamp",
+		AttackTag:  combat.AttackTagElementalBurst,
+		ICDTag:     combat.ICDTagElementalBurst,
+		ICDGroup:   combat.ICDGroupDefault,
+		Element:    attributes.Dendro,
+		Durability: 25,
+		Mult:       burstTick[c.TalentLvlBurst()],
+	}
+	s.burstAtk = &combat.AttackEvent{
+		Info:     procAI,
+		Snapshot: c.Snapshot(&procAI),
+	}
+
+	return s
+}
+
+func (s *LeaLotus) HandleAttack(atk *combat.AttackEvent) float64 {
+	s.Core.Events.Emit(event.OnGadgetHit, s, atk)
+
+	s.Core.Log.NewEvent(fmt.Sprintf("dmc lamp hit by %s", atk.Info.Abil), glog.LogCharacterEvent, s.char.Index)
+
+	s.ShatterCheck(atk)
+
+	if atk.Info.Durability > 0 {
+		atk.Info.Durability *= combat.Durability(s.WillApplyEle(atk.Info.ICDTag, atk.Info.ICDGroup, atk.Info.ActorIndex))
+		if atk.Info.Durability > 0 && atk.Info.Element != attributes.Physical {
+			existing := s.Reactable.ActiveAuraString()
+			applied := atk.Info.Durability
+			s.React(atk)
+			if s.Core.Flags.LogDebug && atk.Reacted {
+				s.Core.Log.NewEvent(
+					"application",
+					glog.LogElementEvent,
+					atk.Info.ActorIndex,
+				).
+					Write("attack_tag", atk.Info.AttackTag).
+					Write("applied_ele", atk.Info.Element.String()).
+					Write("dur", applied).
+					Write("abil", atk.Info.Abil).
+					Write("target", s.Key()).
+					Write("existing", existing).
+					Write("after", s.Reactable.ActiveAuraString())
+
+			}
+		}
+	}
+
+	//apply damage delay is only there to make sure aura gets applied at the end of current frame
+	//however because we can only hold cryo, we'll only call this if atk is cryo and there
+	//is durability left
+	if atk.Info.Element != attributes.Cryo {
+		return 0
+	}
+	if atk.Info.Durability < reactable.ZeroDur {
+		return 0
+	}
+	if atk.Reacted {
+		return 0
+	}
+	s.Core.Combat.Tasks.Add(func() {
+		s.attachEle(atk)
+	}, 0)
+	return 0
+}
+
+func (s *LeaLotus) attachEle(atk *combat.AttackEvent) {
+	// check for ICD first
+	existing := s.Reactable.ActiveAuraString()
+	applied := atk.Info.Durability
+	s.AttachOrRefill(atk)
+	if s.Core.Flags.LogDebug {
+		s.Core.Log.NewEvent(
+			"application",
+			glog.LogElementEvent,
+			atk.Info.ActorIndex,
+		).
+			Write("attack_tag", atk.Info.AttackTag).
+			Write("applied_ele", atk.Info.Element.String()).
+			Write("dur", applied).
+			Write("abil", atk.Info.Abil).
+			Write("target", s.Key()).
+			Write("existing", existing).
+			Write("after", s.Reactable.ActiveAuraString())
+
+	}
+}
+
+func (s *LeaLotus) Tick() {
+	//this is needed since gadget tick
+	s.Reactable.Tick()
+	s.Gadget.Tick()
+}
+
+func (l *LeaLotus) QueueAttack(delay int) {
+	x, y := l.Gadget.Pos()
+	enemies := l.Core.Combat.EnemiesWithinRadius(x, y, l.targetingRadius)
+	if len(enemies) > 0 {
+		idx := l.Core.Rand.Intn(len(enemies))
+
+		l.Core.QueueAttackWithSnap(
+			l.burstAtk.Info,
+			l.burstAtk.Snapshot,
+			combat.NewCircleHit(l.Core.Combat.Enemy(enemies[idx]), l.hitboxRadius),
+			delay,
+		)
+	}
+}
+
+func (r *LeaLotus) React(a *combat.AttackEvent) {
+	//only check the ones possible
+	switch a.Info.Element {
+	case attributes.Electro:
+		r.TryAggravate(a)
+		r.TryFrozenSuperconduct(a)
+		r.TrySuperconduct(a)
+		r.TryQuicken(a)
+	case attributes.Pyro:
+		r.TryMelt(a)
+		r.TryBurning(a)
+	case attributes.Cryo:
+	case attributes.Hydro:
+		r.TryFreeze(a)
+		r.TryBloom(a)
+	case attributes.Anemo:
+		r.TrySwirlHydro(a)
+		r.TrySwirlCryo(a)
+		r.TrySwirlFrozen(a)
+	case attributes.Geo:
+		r.TryCrystallizeCryo(a)
+		r.TryCrystallizeFrozen(a)
+	case attributes.Dendro:
+		r.TrySpread(a)
+		r.TryBloom(a)
+	}
+}
+
+func (s *LeaLotus) TryQuicken(a *combat.AttackEvent) {
+	if !s.Reactable.TryQuicken(a) {
+		return
+	}
+	for t := 15; t <= s.Duration; t += 54 {
+		s.QueueAttack(t)
+	}
+	s.transfig(attributes.Electro)
+}
+
+func (s *LeaLotus) TryBloom(a *combat.AttackEvent) {
+	if !s.Reactable.TryBloom(a) {
+		return
+	}
+	s.targetingRadius = 12
+	s.hitboxRadius = 4
+	for t := 15; t <= s.Duration; t += 90 {
+		s.QueueAttack(t)
+	}
+	s.transfig(attributes.Hydro)
+}
+
+func (s *LeaLotus) TryBurning(a *combat.AttackEvent) {
+	if !s.Reactable.TryBurning(a) {
+		return
+	}
+	s.burstAtk.Info.Abil = "Lea Lotus Lamp Explosion"
+	s.burstAtk.Info.Durability = 50
+	s.burstAtk.Info.ICDTag = combat.ICDTagNone
+	s.burstAtk.Info.Mult = burstExplode[s.char.TalentLvlBurst()]
+	s.Core.Tasks.Add(func() {
+		s.Core.QueueAttackWithSnap(s.burstAtk.Info, s.burstAtk.Snapshot, combat.NewCircleHit(s, 6.5), 0)
+		s.Core.Status.Delete(burstKey)
+	}, 60)
+	s.transfig(attributes.Pyro)
+}
+
+func (s *LeaLotus) transfig(ele attributes.Element) {
+	s.Core.Log.NewEvent(fmt.Sprintf("dmc lamp %s transfig triggered", ele.String()), glog.LogCharacterEvent, s.char.Index)
+	s.char.burstTransfig = ele
+	if s.char.Base.Cons >= 4 {
+		s.char.c4()
+	}
+	s.Kill()
+}
diff --git a/internal/characters/travelerdendro/skill.go b/internal/characters/travelerdendro/skill.go
new file mode 100644
index 000000000..8e268eb97
--- /dev/null
+++ b/internal/characters/travelerdendro/skill.go
@@ -0,0 +1,65 @@
+package travelerdendro
+
+import (
+	"github.com/genshinsim/gcsim/internal/frames"
+	"github.com/genshinsim/gcsim/pkg/core/action"
+	"github.com/genshinsim/gcsim/pkg/core/attributes"
+	"github.com/genshinsim/gcsim/pkg/core/combat"
+)
+
+var skillFrames [][]int
+
+const skillHitmark = 28
+const cdStart = 25
+
+func init() {
+	skillFrames = make([][]int, 2)
+
+	// Male
+	skillFrames[0] = frames.InitAbilSlice(37) // E -> N1
+	skillFrames[0][action.ActionDash] = 29    // E -> D
+	skillFrames[0][action.ActionJump] = 29    // E -> J
+	skillFrames[0][action.ActionSwap] = 36    // E -> Swap
+
+	// Female
+	skillFrames[1] = frames.InitAbilSlice(37) // E -> N1/Q
+	skillFrames[1][action.ActionDash] = 28    // E -> D
+	skillFrames[1][action.ActionJump] = 28    // E -> J
+	skillFrames[1][action.ActionSwap] = 35    // E -> Swap
+}
+
+func (c *char) Skill(p map[string]int) action.ActionInfo {
+	ai := combat.AttackInfo{
+		ActorIndex: c.Index,
+		Abil:       "Razorgrass Blade",
+		AttackTag:  combat.AttackTagElementalArt,
+		ICDTag:     combat.ICDTagNone,
+		ICDGroup:   combat.ICDGroupDefault,
+		StrikeType: combat.StrikeTypeDefault,
+		Element:    attributes.Dendro,
+		Durability: 25,
+		Mult:       skill[c.TalentLvlSkill()],
+	}
+
+	var count float64 = 2
+	if c.Core.Rand.Float64() < 0.50 {
+		count = 3
+	}
+
+	var skillCB func(a combat.AttackCB)
+	if c.Base.Cons >= 1 {
+		c.skillC1 = true
+		skillCB = c.c1cb()
+	}
+	c.Core.QueueAttack(ai, combat.NewCircleHit(c.Core.Combat.Player(), 6.5), skillHitmark, skillHitmark, skillCB)
+
+	c.Core.QueueParticle(c.Base.Key.String(), count, attributes.Dendro, skillHitmark+c.ParticleDelay)
+	c.SetCDWithDelay(action.ActionSkill, 8*60, cdStart)
+
+	return action.ActionInfo{
+		Frames:          frames.NewAbilFunc(skillFrames[c.gender]),
+		AnimationLength: skillFrames[c.gender][action.InvalidAction],
+		CanQueueAfter:   skillFrames[c.gender][action.ActionDash], // earliest cancel
+		State:           action.SkillState,
+	}
+}
diff --git a/internal/characters/travelerdendro/stats.go b/internal/characters/travelerdendro/stats.go
new file mode 100644
index 000000000..90ced2218
--- /dev/null
+++ b/internal/characters/travelerdendro/stats.go
@@ -0,0 +1,218 @@
+package travelerdendro
+
+var (
+	attack = [][]float64{
+		{
+			0.4446,
+			0.4808,
+			0.517,
+			0.5687,
+			0.6049,
+			0.6463,
+			0.7031,
+			0.76,
+			0.8169,
+			0.8789,
+			0.9409,
+			1.003,
+			1.065,
+			1.1271,
+			1.1891,
+		},
+		{
+			0.4343,
+			0.4697,
+			0.505,
+			0.5555,
+			0.5909,
+			0.6313,
+			0.6868,
+			0.7423,
+			0.7979,
+			0.8585,
+			0.9191,
+			0.9797,
+			1.0403,
+			1.1009,
+			1.1615,
+		},
+		{
+			0.5298,
+			0.5729,
+			0.616,
+			0.6776,
+			0.7207,
+			0.77,
+			0.8378,
+			0.9055,
+			0.9733,
+			1.0472,
+			1.1211,
+			1.195,
+			1.269,
+			1.3429,
+			1.4168,
+		},
+		{
+			0.5831,
+			0.6305,
+			0.678,
+			0.7458,
+			0.7933,
+			0.8475,
+			0.9221,
+			0.9967,
+			1.0712,
+			1.1526,
+			1.234,
+			1.3153,
+			1.3967,
+			1.478,
+			1.5594,
+		},
+		{
+			0.7078,
+			0.7654,
+			0.823,
+			0.9053,
+			0.9629,
+			1.0288,
+			1.1193,
+			1.2098,
+			1.3003,
+			1.3991,
+			1.4979,
+			1.5966,
+			1.6954,
+			1.7941,
+			1.8929,
+		},
+	}
+	charge = [][][]float64{
+		// Male
+		{
+			{
+				0.559,
+				0.6045,
+				0.65,
+				0.715,
+				0.7605,
+				0.8125,
+				0.884,
+				0.9555,
+				1.027,
+				1.105,
+				1.183,
+				1.261,
+				1.339,
+				1.417,
+				1.495,
+			},
+			{
+				0.6072,
+				0.6566,
+				0.706,
+				0.7766,
+				0.826,
+				0.8825,
+				0.9602,
+				1.0378,
+				1.1155,
+				1.2002,
+				1.2849,
+				1.3696,
+				1.4544,
+				1.5391,
+				1.6238,
+			},
+		},
+		// Female
+		{
+			{
+				0.559,
+				0.6045,
+				0.65,
+				0.715,
+				0.7605,
+				0.8125,
+				0.884,
+				0.9555,
+				1.027,
+				1.105,
+				1.183,
+				1.261,
+				1.339,
+				1.417,
+				1.495,
+			},
+			{
+				0.7224,
+				0.7812,
+				0.84,
+				0.924,
+				0.9828,
+				1.05,
+				1.1424,
+				1.2348,
+				1.3272,
+				1.428,
+				1.5288,
+				1.6296,
+				1.7304,
+				1.8312,
+				1.932,
+			},
+		},
+	}
+	skill = []float64{
+		2.304,
+		2.4768,
+		2.6496,
+		2.8800,
+		3.0528,
+		3.2256,
+		3.456,
+		3.6864,
+		3.9168,
+		4.1472,
+		4.3776,
+		4.608,
+		4.896,
+		5.184,
+		5.472,
+	}
+	burstTick = []float64{
+		0.8016,
+		0.8617,
+		0.9218,
+		1.002,
+		1.0621,
+		1.1222,
+		1.2024,
+		1.2826,
+		1.3627,
+		1.4429,
+		1.523,
+		1.6032,
+		1.7034,
+		1.80036,
+		1.9038,
+	}
+	burstExplode = []float64{
+		4.008,
+		4.3086,
+		4.6092,
+		5.01,
+		5.3106,
+		5.6112,
+		6.012,
+		6.4128,
+		6.8136,
+		7.2144,
+		7.6152,
+		8.016,
+		8.517,
+		9.018,
+		9.519,
+	}
+)
diff --git a/internal/characters/travelerdendro/travelerdendro.go b/internal/characters/travelerdendro/travelerdendro.go
new file mode 100644
index 000000000..a590f6692
--- /dev/null
+++ b/internal/characters/travelerdendro/travelerdendro.go
@@ -0,0 +1,54 @@
+package travelerdendro
+
+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/keys"
+	"github.com/genshinsim/gcsim/pkg/core/player/character"
+	"github.com/genshinsim/gcsim/pkg/core/player/character/profile"
+)
+
+func init() {
+	core.RegisterCharFunc(keys.AetherDendro, NewChar(0))
+	core.RegisterCharFunc(keys.LumineDendro, NewChar(1))
+}
+
+type char struct {
+	*tmpl.Character
+	burstOverflowingLotuslight int
+	skillC1                    bool // this variable also ensures that C1 only restores energy once per cast
+	burstTransfig              attributes.Element
+	gender                     int
+}
+
+func NewChar(gender int) core.NewCharacterFunc {
+	return func(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error {
+		c := char{
+			gender: gender,
+		}
+		c.Character = tmpl.NewWithWrapper(s, w)
+
+		c.Base.Element = attributes.Dendro
+		c.EnergyMax = 80
+		c.BurstCon = 5
+		c.SkillCon = 3
+		c.NormalHitNum = normalHitNum
+
+		w.Character = &c
+
+		return nil
+	}
+}
+
+func (c *char) Init() error {
+	c.a1Init()
+	c.a4Init()
+
+	if c.Base.Cons >= 6 {
+		c.c6Init()
+	}
+
+	c.skillC1 = false
+	return nil
+}
diff --git a/pkg/core/combat/gadget.go b/pkg/core/combat/gadget.go
index d3924dbd5..32925c5e1 100644
--- a/pkg/core/combat/gadget.go
+++ b/pkg/core/combat/gadget.go
@@ -8,6 +8,7 @@ const (
 	GadgetTypUnknown GadgetTyp = iota
 	GadgetTypDendroCore
 	GadgetTypGuoba
+	GadgetTypLeaLotus
 	GadgetTypTest
 	EndGadgetTyp
 )
@@ -18,6 +19,7 @@ func init() {
 	gadgetLimits = make([]int, EndGadgetTyp)
 	gadgetLimits[GadgetTypDendroCore] = 5
 	gadgetLimits[GadgetTypTest] = 2
+	gadgetLimits[GadgetTypLeaLotus] = 1
 }
 
 type Gadget interface {
diff --git a/pkg/gadget/gadget.go b/pkg/gadget/gadget.go
index 568741a0e..2cd1825b7 100644
--- a/pkg/gadget/gadget.go
+++ b/pkg/gadget/gadget.go
@@ -50,11 +50,10 @@ func (g *Gadget) Tick() {
 		if g.sinceLastThink < g.ThinkInterval {
 			g.sinceLastThink++
 		}
-		if g.sinceLastThink != g.ThinkInterval {
-			return
+		if g.sinceLastThink == g.ThinkInterval {
+			g.OnThinkInterval()
+			g.sinceLastThink = 0
 		}
-		g.OnThinkInterval()
-		g.sinceLastThink = 0
 	}
 	if g.Duration != -1 && g.Duration > 0 {
 		g.Duration--
diff --git a/pkg/simulation/imports.go b/pkg/simulation/imports.go
index dab8d21c6..be2d35daf 100644
--- a/pkg/simulation/imports.go
+++ b/pkg/simulation/imports.go
@@ -96,6 +96,7 @@ import (
 	_ "github.com/genshinsim/gcsim/internal/characters/thoma"
 	_ "github.com/genshinsim/gcsim/internal/characters/tighnari"
 	_ "github.com/genshinsim/gcsim/internal/characters/traveleranemo"
+	_ "github.com/genshinsim/gcsim/internal/characters/travelerdendro"
 	_ "github.com/genshinsim/gcsim/internal/characters/travelerelectro"
 	_ "github.com/genshinsim/gcsim/internal/characters/travelergeo"
 	_ "github.com/genshinsim/gcsim/internal/characters/venti"
diff --git a/scripts/docmdx/main.go b/scripts/docmdx/main.go
index 67eebcd92..6e2e8a563 100644
--- a/scripts/docmdx/main.go
+++ b/scripts/docmdx/main.go
@@ -18,8 +18,7 @@ func skip(key keys.Char) bool {
 	case keys.AetherAnemo: //skip aether by default
 	case keys.AetherElectro:
 	case keys.AetherGeo:
-	case keys.AetherDendro: //skip not implemented
-	case keys.LumineDendro:
+	case keys.AetherDendro:
 	case keys.AetherHydro:
 	case keys.LumineHydro:
 	case keys.AetherPyro:


Test patch:
diff --git a/internal/tests/characters/travelerdendro_test.go b/internal/tests/characters/travelerdendro_test.go
new file mode 100644
index 000000000..ebbe2a4ba
--- /dev/null
+++ b/internal/tests/characters/travelerdendro_test.go
@@ -0,0 +1,289 @@
+package characters
+
+import (
+	"log"
+	"testing"
+
+	"github.com/genshinsim/gcsim/internal/characters/travelerdendro"
+	"github.com/genshinsim/gcsim/pkg/core/action"
+	"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/keys"
+	"github.com/genshinsim/gcsim/pkg/reactable"
+)
+
+func TestTravelerDendroBurstAttach(t *testing.T) {
+
+	c, trg := makeCore(2)
+	prof := defProfile(keys.AetherDendro)
+	prof.Base.Cons = 6
+	idx, err := c.AddChar(prof)
+	if err != nil {
+		t.Errorf("error adding char: %v", err)
+		t.FailNow()
+	}
+	c.Player.SetActive(idx)
+	err = c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+	c.Combat.DefaultTarget = trg[0].Key()
+	c.Events.Subscribe(event.OnGadgetHit, func(args ...interface{}) bool {
+		atk := args[1].(*combat.AttackEvent)
+		log.Printf("hit by %v attack, dur %v", atk.Info.Element, atk.Info.Durability)
+		return false
+	}, "hit-check")
+	advanceCoreFrame(c)
+
+	// use burst to create a ball
+	p := make(map[string]int)
+	c.Player.Exec(action.ActionBurst, keys.AetherDendro, p)
+	for !c.Player.CanQueueNextAction() {
+		advanceCoreFrame(c)
+	}
+	//wait until dendro gadget is created
+	for c.Combat.GadgetCount() < 1 {
+		advanceCoreFrame(c)
+	}
+	//skip an additional frame to be safe
+	advanceCoreFrame(c)
+
+	//check that gadget has dendro on it
+	g := c.Combat.Gadget(0)
+	gr, ok := g.(*travelerdendro.LeaLotus)
+	if !ok {
+		t.Errorf("expecting gadget to be lea lotus. failed")
+		t.FailNow()
+	}
+	log.Println("initial aura string: ", gr.ActiveAuraString())
+	if gr.Durability[reactable.ModifierDendro] != 10 {
+		t.Errorf("expecting initial 10 dendro on traveler lea lotus, got %v", gr.Durability[reactable.ModifierDendro])
+	}
+
+	//pattern only hit gadet
+	pattern := combat.NewCircleHit(combat.NewCircle(0, 0, 1), 100)
+	pattern.SkipTargets[combat.TargettableEnemy] = true
+
+	// check the cryo attaches
+	c.QueueAttackEvent(&combat.AttackEvent{
+		Info: combat.AttackInfo{
+			Element:    attributes.Cryo,
+			Durability: 100,
+		},
+		Pattern: pattern,
+	}, 0)
+	advanceCoreFrame(c)
+
+	log.Println("after applying 100 cyro: ", gr.ActiveAuraString())
+	if gr.Durability[reactable.ModifierCryo] != 80 {
+		t.Errorf("expecting 80 dendro on traveler lea lotus, got %v", gr.Durability[reactable.ModifierCryo])
+	}
+	if gr.Durability[reactable.ModifierDendro] != 10 {
+		t.Errorf("expecting 10 dendro on traveler lea lotus, got %v", gr.Durability[reactable.ModifierDendro])
+	}
+
+}
+
+func TestTravelerDendroBurstPyro(t *testing.T) {
+
+	c, trg := makeCore(1)
+	prof := defProfile(keys.AetherDendro)
+	prof.Base.Cons = 6
+	idx, err := c.AddChar(prof)
+	if err != nil {
+		t.Errorf("error adding char: %v", err)
+		t.FailNow()
+	}
+	c.Player.SetActive(idx)
+	err = c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+	c.Combat.DefaultTarget = trg[0].Key()
+	c.Events.Subscribe(event.OnGadgetHit, func(args ...interface{}) bool {
+		atk := args[1].(*combat.AttackEvent)
+		log.Printf("gadget hit by %v attack, dur %v", atk.Info.Element, atk.Info.Durability)
+		return false
+	}, "hit-check")
+	dmgCount := 0
+	c.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool {
+		atk := args[1].(*combat.AttackEvent)
+		if atk.Info.Abil == "Lea Lotus Lamp Explosion" {
+			dmgCount++
+			log.Println("big boom at: ", c.F)
+		}
+		return false
+	}, "hit-check")
+	advanceCoreFrame(c)
+
+	// use burst to create a ball
+	p := make(map[string]int)
+	c.Player.Exec(action.ActionBurst, keys.AetherDendro, p)
+	for !c.Player.CanQueueNextAction() {
+		advanceCoreFrame(c)
+	}
+	//wait until dendro gadget is created
+	for c.Combat.GadgetCount() < 1 {
+		advanceCoreFrame(c)
+	}
+	//skip an additional frame to be safe
+	advanceCoreFrame(c)
+
+	//check that gadget has dendro on it
+	g := c.Combat.Gadget(0)
+	gr, ok := g.(*travelerdendro.LeaLotus)
+	if !ok {
+		t.Errorf("expecting gadget to be lea lotus. failed")
+		t.FailNow()
+	}
+	log.Println("initial aura string: ", gr.ActiveAuraString())
+	if gr.Durability[reactable.ModifierDendro] != 10 {
+		t.Errorf("expecting initial 10 dendro on traveler lea lotus, got %v", gr.Durability[reactable.ModifierDendro])
+	}
+
+	//pattern only hit gadet
+	pattern := combat.NewCircleHit(combat.NewCircle(0, 0, 1), 100)
+	pattern.SkipTargets[combat.TargettableEnemy] = true
+
+	// check the cryo attaches
+	c.QueueAttackEvent(&combat.AttackEvent{
+		Info: combat.AttackInfo{
+			Element:    attributes.Pyro,
+			Durability: 100,
+		},
+		Pattern: pattern,
+	}, 0)
+	advanceCoreFrame(c)
+
+	log.Printf("at f %v after applying 100 pyro: %v\n", c.F, gr.ActiveAuraString())
+	if gr.Durability[reactable.ModifierPyro] != 0 {
+		t.Errorf("expecting 0 dendro on traveler lea lotus, got %v", gr.Durability[reactable.ModifierPyro])
+	}
+
+	//should get an explosion 60 frfames later
+	for i := 0; i < 100; i++ {
+		advanceCoreFrame(c)
+	}
+
+	if dmgCount != 1 {
+		t.Errorf("expected 1 dmg count, got %v", dmgCount)
+	}
+
+}
+
+// lotus is expected to tick at frame 37 after appearing, which is 54+37 after cast
+// and then tick every 90 frames after that for the duration
+// duration is either 12s at c0 or 15s at c2
+func TestTravelerDendroBurstTicks(t *testing.T) {
+	c, trg := makeCore(1)
+	prof := defProfile(keys.AetherDendro)
+	prof.Base.Cons = 6
+	idx, err := c.AddChar(prof)
+	if err != nil {
+		t.Errorf("error adding char: %v", err)
+		t.FailNow()
+	}
+	c.Player.SetActive(idx)
+	err = c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+	c.Combat.DefaultTarget = trg[0].Key()
+	dmgCount := 0
+	c.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool {
+		atk := args[1].(*combat.AttackEvent)
+		if atk.Info.Abil == "Lea Lotus Lamp" {
+			dmgCount++
+			log.Println("boom at (adjusted): ", c.F-54-1)
+		}
+		return false
+	}, "hit-check")
+	advanceCoreFrame(c)
+
+	// use burst to create a ball
+	p := make(map[string]int)
+	log.Println("casting burst: ", c.F)
+	c.Player.Exec(action.ActionBurst, keys.AetherDendro, p)
+
+	//expecting to take a total of 54 frames to appear + 15s duration
+	totalDuration := 15 * 60
+	expectedCount := 1 + (totalDuration-37)/90
+
+	//add 100 for good measures in case bugs from extra ticks
+	for i := 0; i < 54+totalDuration+100; i++ {
+		advanceCoreFrame(c)
+	}
+
+	if dmgCount != expectedCount {
+		t.Errorf("expecting %v ticks, got %v", expectedCount, dmgCount)
+	}
+
+}
+
+func TestTravelerDendroBurstElectroTicks(t *testing.T) {
+	c, trg := makeCore(1)
+	prof := defProfile(keys.AetherDendro)
+	prof.Base.Cons = 6
+	idx, err := c.AddChar(prof)
+	if err != nil {
+		t.Errorf("error adding char: %v", err)
+		t.FailNow()
+	}
+	c.Player.SetActive(idx)
+	err = c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+	c.Combat.DefaultTarget = trg[0].Key()
+	dmgCount := 0
+	c.Events.Subscribe(event.OnEnemyDamage, func(args ...interface{}) bool {
+		atk := args[1].(*combat.AttackEvent)
+		if atk.Info.Abil == "Lea Lotus Lamp" {
+			dmgCount++
+			log.Println("boom at (adjusted): ", c.F-54-1)
+		}
+		return false
+	}, "hit-check")
+	advanceCoreFrame(c)
+
+	// use burst to create a ball
+	p := make(map[string]int)
+	log.Println("casting burst: ", c.F)
+	c.Player.Exec(action.ActionBurst, keys.AetherDendro, p)
+	//wait until dendro gadget is created
+	for c.Combat.GadgetCount() < 1 {
+		advanceCoreFrame(c)
+	}
+
+	//pattern only hit gadet
+	pattern := combat.NewCircleHit(combat.NewCircle(0, 0, 1), 100)
+	pattern.SkipTargets[combat.TargettableEnemy] = true
+
+	// check the cryo attaches
+	c.QueueAttackEvent(&combat.AttackEvent{
+		Info: combat.AttackInfo{
+			Element:    attributes.Electro,
+			Durability: 100,
+		},
+		Pattern: pattern,
+	}, 0)
+
+	//first tick at 15, then tick every 54 after that
+	totalDuration := 15 * 60
+	expectedCount := 1 + (totalDuration-15)/54
+
+	//add 100 for good measures in case bugs from extra ticks
+	for i := 0; i < totalDuration+100; i++ {
+		advanceCoreFrame(c)
+	}
+
+	if dmgCount != expectedCount {
+		t.Errorf("expecting %v ticks, got %v", expectedCount, dmgCount)
+	}
+
+}