Repository: genshinsim/gcsim
Pull request #979 (2022-10-03 18:48:57)

fix targetting to use key only


Patch:
diff --git a/internal/characters/beidou/burst.go b/internal/characters/beidou/burst.go
index f049f5c7d..ac3475626 100644
--- a/internal/characters/beidou/burst.go
+++ b/internal/characters/beidou/burst.go
@@ -126,7 +126,7 @@ func (c *char) burstProc() {
 		//trigger a chain of attacks starting at the first target
 		atk := *c.burstAtk
 		atk.SourceFrame = c.Core.F
-		atk.Pattern = combat.NewDefSingleTarget(t.Index(), combat.TargettableEnemy)
+		atk.Pattern = combat.NewDefSingleTarget(t.Key(), combat.TargettableEnemy)
 		cb := c.chain(c.Core.F, 1)
 		if cb != nil {
 			atk.Callbacks = append(atk.Callbacks, cb)
@@ -162,7 +162,7 @@ func (c *char) chain(src int, count int) combat.AttackCBFunc {
 		//queue an attack vs next target
 		atk := *c.burstAtk
 		atk.SourceFrame = src
-		atk.Pattern = combat.NewDefSingleTarget(trgs[next], combat.TargettableEnemy)
+		atk.Pattern = combat.NewDefSingleTarget(c.Core.Combat.Enemy(next).Key(), combat.TargettableEnemy)
 		cb := c.chain(src, count+1)
 		if cb != nil {
 			atk.Callbacks = append(atk.Callbacks, cb)
diff --git a/internal/characters/beidou/cons.go b/internal/characters/beidou/cons.go
index d767976d1..976b378fa 100644
--- a/internal/characters/beidou/cons.go
+++ b/internal/characters/beidou/cons.go
@@ -46,7 +46,7 @@ func (c *char) c4() {
 			Durability: 25,
 			Mult:       0.2,
 		}
-		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(t.Index(), t.Type()), 0, 1)
+		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(t.Key(), t.Type()), 0, 1)
 		return false
 	}, "beidou-c4")
 }
diff --git a/internal/characters/fischl/asc.go b/internal/characters/fischl/asc.go
index 03fd52945..2215dda0d 100644
--- a/internal/characters/fischl/asc.go
+++ b/internal/characters/fischl/asc.go
@@ -39,7 +39,7 @@ func (c *char) a4() {
 		// Technically should have a separate snapshot for each attack info?
 		// ai.ModsLog = c.ozSnapshot.Info.ModsLog
 		// A4 uses Oz Snapshot
-		c.Core.QueueAttackWithSnap(ai, c.ozSnapshot.Snapshot, combat.NewDefSingleTarget(t.Index(), combat.TargettableEnemy), 3)
+		c.Core.QueueAttackWithSnap(ai, c.ozSnapshot.Snapshot, combat.NewDefSingleTarget(t.Key(), combat.TargettableEnemy), 3)
 
 		return false
 	}
diff --git a/internal/characters/hutao/skill.go b/internal/characters/hutao/skill.go
index dd5cd8fbd..b47329dca 100644
--- a/internal/characters/hutao/skill.go
+++ b/internal/characters/hutao/skill.go
@@ -134,7 +134,7 @@ func (c *char) bbtickfunc(src int, trg *enemy.Enemy) func() {
 		if c.Base.Cons >= 2 {
 			ai.FlatDmg += c.MaxHP() * 0.1
 		}
-		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(trg.Index(), combat.TargettableEnemy), 0, 0)
+		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(trg.Key(), combat.TargettableEnemy), 0, 0)
 
 		if c.Core.Flags.LogDebug {
 			c.Core.Log.NewEvent("Blood Blossom ticked", glog.LogCharacterEvent, c.Index).
diff --git a/internal/characters/lisa/burst.go b/internal/characters/lisa/burst.go
index c927f5a6e..abeae7c70 100644
--- a/internal/characters/lisa/burst.go
+++ b/internal/characters/lisa/burst.go
@@ -83,7 +83,7 @@ func (c *char) Burst(p map[string]int) action.ActionInfo {
 				c.Core.QueueAttackWithSnap(
 					ai,
 					snap,
-					combat.NewDefSingleTarget(ind, combat.TargettableEnemy),
+					combat.NewDefSingleTarget(c.Core.Combat.Enemy(ind).Key(), combat.TargettableEnemy),
 					0,
 					c.a4,
 				)
diff --git a/internal/characters/mona/burst.go b/internal/characters/mona/burst.go
index cf23c6608..a09a8c201 100644
--- a/internal/characters/mona/burst.go
+++ b/internal/characters/mona/burst.go
@@ -99,8 +99,8 @@ func (c *char) burstDamageBonus() {
 	}
 }
 
-//bubble bursts when hit by an attack either while not frozen, or when the attack breaks freeze
-//i.e. impulse > 0
+// bubble bursts when hit by an attack either while not frozen, or when the attack breaks freeze
+// i.e. impulse > 0
 func (c *char) burstHook() {
 	//hook on to OnDamage; leave this always active
 	//since freeze will trigger an attack, this should be ok
@@ -149,5 +149,5 @@ func (c *char) triggerBubbleBurst(t *enemy.Enemy) {
 		Durability: 50,
 		Mult:       explosion[c.TalentLvlBurst()],
 	}
-	c.Core.QueueAttack(ai, combat.NewDefSingleTarget(t.Index(), t.Type()), 1, 1)
+	c.Core.QueueAttack(ai, combat.NewDefSingleTarget(t.Key(), t.Type()), 1, 1)
 }
diff --git a/internal/characters/yaemiko/kitsune.go b/internal/characters/yaemiko/kitsune.go
index 79bbf3bbb..1dd952650 100644
--- a/internal/characters/yaemiko/kitsune.go
+++ b/internal/characters/yaemiko/kitsune.go
@@ -142,7 +142,7 @@ func (c *char) kitsuneTick(totem *kitsune) func() {
 			c.Core.QueueParticle("yaemiko", 1, attributes.Electro, c.ParticleDelay)
 		}
 
-		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(c.Core.Combat.RandomEnemyTarget(), combat.TargettableEnemy), 1, 1, cb)
+		c.Core.QueueAttack(ai, combat.NewDefSingleTarget(c.Core.Combat.Enemy(c.Core.Combat.RandomEnemyTarget()).Key(), combat.TargettableEnemy), 1, 1, cb)
 		// tick per ~2.9s seconds
 		c.Core.Tasks.Add(c.kitsuneTick(totem), 176)
 	}
diff --git a/internal/characters/yelan/skill.go b/internal/characters/yelan/skill.go
index 91f64cfea..0680a43ed 100644
--- a/internal/characters/yelan/skill.go
+++ b/internal/characters/yelan/skill.go
@@ -118,7 +118,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo {
 			marked--
 			//queueing attack one frame later
 			//TODO: does hold have different attack size? don't think so?
-			c.Core.QueueAttack(ai, combat.NewDefSingleTarget(e.Index(), combat.TargettableEnemy), 1, 1, cb)
+			c.Core.QueueAttack(ai, combat.NewDefSingleTarget(e.Key(), combat.TargettableEnemy), 1, 1, cb)
 		}
 
 		//activate c4 if relevant
diff --git a/internal/weapons/bow/messenger/messenger.go b/internal/weapons/bow/messenger/messenger.go
index 35da121bc..541f55239 100644
--- a/internal/weapons/bow/messenger/messenger.go
+++ b/internal/weapons/bow/messenger/messenger.go
@@ -66,7 +66,7 @@ func NewWeapon(c *core.Core, char *character.CharWrapper, p weapon.WeaponProfile
 			Mult:         dmg,
 			HitWeakPoint: true, // ensure crit by marking it as hitting weakspot
 		}
-		c.QueueAttack(ai, combat.NewDefSingleTarget(trg.Index(), combat.TargettableEnemy), 0, 1)
+		c.QueueAttack(ai, combat.NewDefSingleTarget(trg.Key(), combat.TargettableEnemy), 0, 1)
 
 		return false
 	}, fmt.Sprintf("messenger-%v", char.Base.Key.String()))
diff --git a/internal/weapons/catalyst/perception/perception.go b/internal/weapons/catalyst/perception/perception.go
index a8b7b3d05..232ecf13d 100644
--- a/internal/weapons/catalyst/perception/perception.go
+++ b/internal/weapons/catalyst/perception/perception.go
@@ -40,7 +40,7 @@ func (w *Weapon) chain(count int, c *core.Core, char *character.CharWrapper) fun
 		}
 		t.SetTag(bounceKey, c.F+36)
 		x, y := a.Target.Shape().Pos()
-		trgs := c.Combat.EnemyByDistance(x, y, a.Target.Index())
+		trgs := c.Combat.EnemyByDistance(x, y, a.Target.Key())
 		next := -1
 		for _, v := range trgs {
 			trg, ok := c.Combat.Enemy(v).(*enemy.Enemy)
@@ -58,7 +58,7 @@ func (w *Weapon) chain(count int, c *core.Core, char *character.CharWrapper) fun
 		}
 
 		cb := w.chain(count+1, c, char)
-		c.QueueAttackWithSnap(w.ai, w.snap, combat.NewDefSingleTarget(next, combat.TargettableEnemy), 10, cb)
+		c.QueueAttackWithSnap(w.ai, w.snap, combat.NewDefSingleTarget(c.Combat.Enemy(next).Key(), combat.TargettableEnemy), 10, cb)
 	}
 }
 
diff --git a/internal/weapons/spear/halberd/halberd.go b/internal/weapons/spear/halberd/halberd.go
index 5d16c31ba..a4e1cb281 100644
--- a/internal/weapons/spear/halberd/halberd.go
+++ b/internal/weapons/spear/halberd/halberd.go
@@ -61,7 +61,7 @@ func NewWeapon(c *core.Core, char *character.CharWrapper, p weapon.WeaponProfile
 			Durability: 100,
 			Mult:       dmg,
 		}
-		c.QueueAttack(ai, combat.NewDefSingleTarget(trg.Index(), combat.TargettableEnemy), 0, 1)
+		c.QueueAttack(ai, combat.NewDefSingleTarget(trg.Key(), combat.TargettableEnemy), 0, 1)
 
 		return false
 	}, fmt.Sprintf("halberd-%v", char.Base.Key.String()))
diff --git a/pkg/core/combat/absorb.go b/pkg/core/combat/absorb.go
index be8d47895..f5801cec0 100644
--- a/pkg/core/combat/absorb.go
+++ b/pkg/core/combat/absorb.go
@@ -5,7 +5,7 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core/glog"
 )
 
-func WillCollide(p AttackPattern, t Target, index int) bool {
+func WillCollide(p AttackPattern, t Target, key TargetKey) bool {
 	//shape shouldn't be nil; panic here
 	if p.Shape == nil {
 		panic("unexpected nil shape")
@@ -23,7 +23,7 @@ func WillCollide(p AttackPattern, t Target, index int) bool {
 		return t.Shape().IntersectRectangle(*v)
 	case *SingleTarget:
 		//only true if
-		return v.Target == index
+		return v.Target == key
 	default:
 		return false
 	}
diff --git a/pkg/core/combat/attackevent.go b/pkg/core/combat/attackevent.go
index 8e9853ab7..1732a72c6 100644
--- a/pkg/core/combat/attackevent.go
+++ b/pkg/core/combat/attackevent.go
@@ -26,9 +26,9 @@ type AttackCB struct {
 type AttackCBFunc func(AttackCB)
 
 type AttackInfo struct {
-	ActorIndex       int    //character this attack belongs to
-	DamageSrc        int    //source of this attack; should be a unique key identifying the target
-	Abil             string //name of ability triggering the damage
+	ActorIndex       int       //character this attack belongs to
+	DamageSrc        TargetKey //source of this attack; should be a unique key identifying the target
+	Abil             string    //name of ability triggering the damage
 	AttackTag        AttackTag
 	ICDTag           ICDTag
 	ICDGroup         ICDGroup
diff --git a/pkg/core/combat/combat.go b/pkg/core/combat/combat.go
index 802c0fc8c..ad726fed3 100644
--- a/pkg/core/combat/combat.go
+++ b/pkg/core/combat/combat.go
@@ -29,7 +29,7 @@ type Handler struct {
 	player      Target
 	TotalDamage float64
 	gccount     int
-	keycount    int
+	keycount    TargetKey
 }
 
 type Opt struct {
@@ -42,7 +42,7 @@ type Opt struct {
 	DamageMode    bool
 	DefHalt       bool
 	EnableHitlag  bool
-	DefaultTarget int //index for default target
+	DefaultTarget TargetKey //index for default target
 }
 
 func New(opt Opt) *Handler {
@@ -56,7 +56,7 @@ func New(opt Opt) *Handler {
 	return h
 }
 
-func (h *Handler) nextkey() int {
+func (h *Handler) nextkey() TargetKey {
 	h.keycount++
 	return h.keycount - 1
 }
diff --git a/pkg/core/combat/enemy.go b/pkg/core/combat/enemy.go
index 67ca9d628..6203ce6cb 100644
--- a/pkg/core/combat/enemy.go
+++ b/pkg/core/combat/enemy.go
@@ -31,6 +31,19 @@ func (h *Handler) KillEnemy(i int) {
 	h.enemies[i].Kill()
 	h.Events.Emit(event.OnTargetDied, h.enemies[i], &AttackEvent{}) // TODO: it's fine?
 	h.Log.NewEvent("enemy dead", glog.LogSimEvent, -1).Write("index", i)
+	//try setting default target to a diff one if same as dead enemy
+	if h.enemies[i].Key() == h.DefaultTarget {
+		for j, v := range h.enemies {
+			if j == i {
+				continue
+			}
+			if v.IsAlive() {
+				h.DefaultTarget = v.Key()
+				h.Log.NewEvent("default target changed on enemy death", glog.LogWarnings, -1)
+				break
+			}
+		}
+	}
 }
 
 func (h *Handler) AddEnemy(t Target) {
@@ -48,11 +61,19 @@ func (h *Handler) EnemyCount() int {
 }
 
 func (h *Handler) PrimaryTarget() Target {
-	return h.enemies[h.DefaultTarget]
+	for _, v := range h.enemies {
+		if v.Key() == h.DefaultTarget {
+			if !v.IsAlive() {
+				h.Log.NewEvent("default target is dead", glog.LogWarnings, -1)
+			}
+			return v
+		}
+	}
+	panic("default target does not exist?!")
 }
 
 // EnemyByDistance returns an array of indices of the enemies sorted by distance
-func (c *Handler) EnemyByDistance(x, y float64, excl int) []int {
+func (c *Handler) EnemyByDistance(x, y float64, excl TargetKey) []int {
 	//we dont actually need to know the exact distance. just find the lowest
 	//of x^2 + y^2 to avoid sqrt
 
@@ -62,7 +83,7 @@ func (c *Handler) EnemyByDistance(x, y float64, excl int) []int {
 	}
 
 	for i, v := range c.enemies {
-		if i == excl {
+		if v.Key() == excl {
 			continue
 		}
 		vx, vy := v.Shape().Pos()
diff --git a/pkg/core/combat/gadget.go b/pkg/core/combat/gadget.go
index 2189dd47c..dc33e94fc 100644
--- a/pkg/core/combat/gadget.go
+++ b/pkg/core/combat/gadget.go
@@ -26,7 +26,7 @@ type Gadget interface {
 	GadgetTyp() GadgetTyp
 }
 
-func (h *Handler) RemoveGadget(key int) {
+func (h *Handler) RemoveGadget(key TargetKey) {
 	h.ReplaceGadget(key, nil)
 }
 
@@ -56,7 +56,7 @@ func (h *Handler) AddGadget(t Gadget) {
 	t.SetKey(h.nextkey())
 }
 
-func (h *Handler) ReplaceGadget(key int, t Gadget) {
+func (h *Handler) ReplaceGadget(key TargetKey, t Gadget) {
 	//do nothing if not found
 	for i, v := range h.gadgets {
 		if v != nil && v.Key() == key {
diff --git a/pkg/core/combat/hitbox.go b/pkg/core/combat/hitbox.go
index 52655ea80..87c6e72e3 100644
--- a/pkg/core/combat/hitbox.go
+++ b/pkg/core/combat/hitbox.go
@@ -6,7 +6,7 @@ type AttackPattern struct {
 	SelfHarm bool
 }
 
-func NewDefSingleTarget(ind int, typ TargettableType) AttackPattern {
+func NewDefSingleTarget(ind TargetKey, typ TargettableType) AttackPattern {
 	var arr [TargettableTypeCount]bool
 	arr[typ] = true
 	return AttackPattern{
diff --git a/pkg/core/combat/shape.go b/pkg/core/combat/shape.go
index cf3767983..2178a2113 100644
--- a/pkg/core/combat/shape.go
+++ b/pkg/core/combat/shape.go
@@ -25,7 +25,7 @@ func NewCircle(x, y, r float64) *Circle {
 }
 
 type SingleTarget struct {
-	Target int
+	Target TargetKey
 }
 
 func (s *SingleTarget) IntersectCircle(in Circle) bool       { return false }
@@ -33,7 +33,7 @@ func (s *SingleTarget) IntersectRectangle(in Rectangle) bool { return false }
 func (s *SingleTarget) Pos() (float64, float64)              { return 0, 0 }
 func (s *SingleTarget) String() string                       { return fmt.Sprintf("single target: %v", s.Target) }
 
-//this is for attack that only hits self
+// this is for attack that only hits self
 type SelfDamage struct{}
 
 func (c *SelfDamage) IntersectCircle(in Circle) bool       { return false }
diff --git a/pkg/core/combat/target.go b/pkg/core/combat/target.go
index 8f5a2b73e..f9b650e88 100644
--- a/pkg/core/combat/target.go
+++ b/pkg/core/combat/target.go
@@ -5,17 +5,21 @@ import (
 	"github.com/genshinsim/gcsim/pkg/core/glog"
 )
 
+type TargetKey int
+
+const InvalidTargetKey TargetKey = -1
+
 type Target interface {
 	Index() int              //should correspond to index
 	SetIndex(index int)      //update the current index
-	Key() int                //unique key for the target
-	SetKey(k int)            //update key
+	Key() TargetKey          //unique key for the target
+	SetKey(k TargetKey)      //update key
 	Type() TargettableType   //type of target
 	Shape() Shape            // shape of target
 	Pos() (float64, float64) // center of target
 	SetPos(x, y float64)     // move target
 	IsAlive() bool
-	AttackWillLand(a AttackPattern, src int) (bool, string)
+	AttackWillLand(a AttackPattern, src TargetKey) (bool, string)
 	Attack(*AttackEvent, glog.Event) (float64, bool)
 	ApplyDamage(*AttackEvent, float64)
 	Tick() //called every tick
diff --git a/pkg/gcs/sysfunc.go b/pkg/gcs/sysfunc.go
index 7f242948b..1d1e39c55 100644
--- a/pkg/gcs/sysfunc.go
+++ b/pkg/gcs/sysfunc.go
@@ -200,7 +200,7 @@ func (e *Eval) setDefaultTarget(c *ast.CallExpr, env *Env) (Obj, error) {
 		return nil, fmt.Errorf("index for set_default_target is invalid, should be between %v and %v, got %v", 1, e.Core.Combat.EnemyCount(), idx)
 	}
 
-	e.Core.Combat.DefaultTarget = idx - 1
+	e.Core.Combat.DefaultTarget = e.Core.Combat.Enemy(idx - 1).Key()
 
 	return &number{}, nil
 
diff --git a/pkg/reactable/bloom.go b/pkg/reactable/bloom.go
index 463a5b562..d9c290a4b 100644
--- a/pkg/reactable/bloom.go
+++ b/pkg/reactable/bloom.go
@@ -132,7 +132,7 @@ func (s *DendroCore) Attack(atk *combat.AttackEvent, evt glog.Event) (float64, b
 		ai := NewHyperbloomAttack(char, s)
 		// queue dmg nearest enemy
 		x, y := s.Gadget.Pos()
-		enemies := s.Core.Combat.EnemyByDistance(x, y, s.Gadget.Key())
+		enemies := s.Core.Combat.EnemyByDistance(x, y, combat.InvalidTargetKey)
 		if len(enemies) > 0 {
 			s.Core.QueueAttack(ai, combat.NewCircleHit(s.Core.Combat.Enemy(enemies[0]), 1, false, combat.TargettableEnemy), -1, 5)
 
diff --git a/pkg/reactable/burning.go b/pkg/reactable/burning.go
index c9f0d9da1..0479a2c79 100644
--- a/pkg/reactable/burning.go
+++ b/pkg/reactable/burning.go
@@ -71,7 +71,7 @@ func (r *Reactable) attachBurningFuel(dur combat.Durability, mult combat.Durabil
 func (r *Reactable) calcBurningDmg(a *combat.AttackEvent) {
 	atk := combat.AttackInfo{
 		ActorIndex:       a.Info.ActorIndex,
-		DamageSrc:        r.self.Index(),
+		DamageSrc:        r.self.Key(),
 		Abil:             string(combat.Burning),
 		AttackTag:        combat.AttackTagBurningDamage,
 		ICDTag:           combat.ICDTagBurningDamage,
diff --git a/pkg/reactable/electrocharged.go b/pkg/reactable/electrocharged.go
index e6067d9c7..9fb034037 100644
--- a/pkg/reactable/electrocharged.go
+++ b/pkg/reactable/electrocharged.go
@@ -63,7 +63,7 @@ func (r *Reactable) tryAddEC(a *combat.AttackEvent) {
 
 		r.core.QueueAttack(
 			r.ecSnapshot,
-			combat.NewDefSingleTarget(r.self.Index(), r.self.Type()),
+			combat.NewDefSingleTarget(r.self.Key(), r.self.Type()),
 			-1,
 			10,
 		)
@@ -152,7 +152,7 @@ func (r *Reactable) nextTick(src int) func() {
 		//so ec is active, which means both aura must still have value > 0; so we can do dmg
 		r.core.QueueAttack(
 			r.ecSnapshot,
-			combat.NewDefSingleTarget(r.self.Index(), r.self.Type()),
+			combat.NewDefSingleTarget(r.self.Key(), r.self.Type()),
 			-1,
 			0,
 		)
diff --git a/pkg/reactable/freeze.go b/pkg/reactable/freeze.go
index d576f6e2c..4f52004b2 100644
--- a/pkg/reactable/freeze.go
+++ b/pkg/reactable/freeze.go
@@ -78,7 +78,7 @@ func (r *Reactable) ShatterCheck(a *combat.AttackEvent) {
 	//shatter is a self attack
 	r.core.QueueAttack(
 		ai,
-		combat.NewDefSingleTarget(r.self.Index(), r.self.Type()),
+		combat.NewDefSingleTarget(r.self.Key(), r.self.Type()),
 		-1,
 		1,
 	)
@@ -110,6 +110,6 @@ func (r *Reactable) checkFreeze() {
 			DoNotLog:    true,
 		}
 		//TODO: delay attack by 1 frame ok?
-		r.core.QueueAttack(ai, combat.NewDefSingleTarget(r.self.Index(), r.self.Type()), -1, 1)
+		r.core.QueueAttack(ai, combat.NewDefSingleTarget(r.self.Key(), r.self.Type()), -1, 1)
 	}
 }
diff --git a/pkg/reactable/swirl.go b/pkg/reactable/swirl.go
index a945a4d75..7c0f1b9e9 100644
--- a/pkg/reactable/swirl.go
+++ b/pkg/reactable/swirl.go
@@ -38,7 +38,7 @@ func (r *Reactable) queueSwirl(rt combat.ReactionType, ele attributes.Element, t
 	r.core.QueueAttackWithSnap(
 		ai,
 		snap,
-		combat.NewDefSingleTarget(r.self.Index(), r.self.Type()),
+		combat.NewDefSingleTarget(r.self.Key(), r.self.Type()),
 		1,
 	)
 	//next is aoe - hydro swirls never do AoE damage, as they only spread the element
diff --git a/pkg/simulation/setup.go b/pkg/simulation/setup.go
index 004798954..1c7d04aa5 100644
--- a/pkg/simulation/setup.go
+++ b/pkg/simulation/setup.go
@@ -39,6 +39,10 @@ func SetupTargetsInCore(core *core.Core, p core.Coord, targets []enemy.EnemyProf
 		//s.stats.ElementUptime[i+1] = make(map[core.EleType]int)
 	}
 
+	//default target is closest to player?
+	trgs := core.Combat.EnemyByDistance(p.X, p.Y, combat.InvalidTargetKey)
+	core.Combat.DefaultTarget = core.Combat.Enemy(trgs[0]).Key()
+
 	return nil
 }
 
diff --git a/pkg/target/target.go b/pkg/target/target.go
index 4c0c8a6b7..fbc267cca 100644
--- a/pkg/target/target.go
+++ b/pkg/target/target.go
@@ -8,7 +8,7 @@ import (
 type Target struct {
 	Core            *core.Core
 	TargetIndex     int
-	key             int
+	key             combat.TargetKey
 	Hitbox          combat.Circle
 	Tags            map[string]int
 	CollidableTypes [combat.TargettableTypeCount]bool
@@ -36,15 +36,15 @@ func (t *Target) CollidedWith(x combat.Target) {
 	}
 }
 
-func (t *Target) Key() int                { return t.key }
-func (t *Target) SetKey(x int)            { t.key = x }
-func (t *Target) Index() int              { return t.TargetIndex }
-func (t *Target) SetIndex(ind int)        { t.TargetIndex = ind }
-func (t *Target) Shape() combat.Shape     { return &t.Hitbox }
-func (t *Target) SetPos(x, y float64)     { t.Hitbox.SetPos(x, y) }
-func (t *Target) Pos() (float64, float64) { return t.Hitbox.Pos() }
-func (t *Target) Kill()                   { t.Alive = false }
-func (t *Target) IsAlive() bool           { return t.Alive }
+func (t *Target) Key() combat.TargetKey     { return t.key }
+func (t *Target) SetKey(x combat.TargetKey) { t.key = x }
+func (t *Target) Index() int                { return t.TargetIndex }
+func (t *Target) SetIndex(ind int)          { t.TargetIndex = ind }
+func (t *Target) Shape() combat.Shape       { return &t.Hitbox }
+func (t *Target) SetPos(x, y float64)       { t.Hitbox.SetPos(x, y) }
+func (t *Target) Pos() (float64, float64)   { return t.Hitbox.Pos() }
+func (t *Target) Kill()                     { t.Alive = false }
+func (t *Target) IsAlive() bool             { return t.Alive }
 func (t *Target) SetTag(key string, val int) {
 	t.Tags[key] = val
 }
@@ -71,7 +71,7 @@ func (t *Target) WillCollide(s combat.Shape) bool {
 	}
 }
 
-func (t *Target) AttackWillLand(a combat.AttackPattern, src int) (bool, string) {
+func (t *Target) AttackWillLand(a combat.AttackPattern, src combat.TargetKey) (bool, string) {
 	//shape shouldn't be nil; panic here
 	if a.Shape == nil {
 		panic("unexpected nil shape")
@@ -96,7 +96,7 @@ func (t *Target) AttackWillLand(a combat.AttackPattern, src int) (bool, string)
 		return t.Shape().IntersectRectangle(*v), "intersect rectangle"
 	case *combat.SingleTarget:
 		//only true if
-		return v.Target == t.TargetIndex, "target"
+		return v.Target == t.key, "target"
 	default:
 		return false, "unknown shape"
 	}


Test patch:
diff --git a/internal/tests/reactable/burning_test.go b/internal/tests/reactable/burning_test.go
index 35e008926..e23b5e457 100644
--- a/internal/tests/reactable/burning_test.go
+++ b/internal/tests/reactable/burning_test.go
@@ -34,7 +34,7 @@ func TestBurningTicks(t *testing.T) {
 			Element:    attributes.Pyro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 70)
 	//tighnari skill at 200
 	c.QueueAttackEvent(&combat.AttackEvent{
@@ -42,7 +42,7 @@ func TestBurningTicks(t *testing.T) {
 			Element:    attributes.Dendro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 200)
 	//lisa 250
 	c.QueueAttackEvent(&combat.AttackEvent{
@@ -50,7 +50,7 @@ func TestBurningTicks(t *testing.T) {
 			Element:    attributes.Electro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 250)
 
 	//burning starts ticking at 200 and ticks every 15 frames
@@ -106,7 +106,7 @@ func TestBurningQuickenFuel(t *testing.T) {
 			Element:    attributes.Dendro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 290)
 	//beidou e should apply hitlag here
 	c.QueueAttackEvent(&combat.AttackEvent{
@@ -114,7 +114,7 @@ func TestBurningQuickenFuel(t *testing.T) {
 			Element:    attributes.Electro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 327)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
@@ -122,7 +122,7 @@ func TestBurningQuickenFuel(t *testing.T) {
 			Durability: 25,
 			ActorIndex: 0,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 396)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
@@ -130,14 +130,14 @@ func TestBurningQuickenFuel(t *testing.T) {
 			Durability: 25,
 			ActorIndex: 1,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 462)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Electro,
 			Durability: 100,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 536)
 
 	f := make(map[event.Event]int)
@@ -205,49 +205,49 @@ func TestPyroDendroCoexist(t *testing.T) {
 			Element:    attributes.Dendro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 121)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Pyro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 133)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Dendro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 195)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Cryo,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 202)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Pyro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 249)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Cryo,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 327)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Cryo,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 344)
 	//pyro ended 546, dendro ended 689
 
@@ -282,28 +282,28 @@ func TestDendroDecayTry1(t *testing.T) {
 			Element:    attributes.Dendro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 155)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Pyro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 168)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Dendro,
 			Durability: 50,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 230)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Hydro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 263)
 
 	f := make(map[event.Event]int)
@@ -340,21 +340,21 @@ func TestDendroDecayTry2(t *testing.T) {
 			Element:    attributes.Pyro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 80)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Dendro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 440)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Hydro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 453)
 
 	f := make(map[event.Event]int)
@@ -391,28 +391,28 @@ func TestQuickenBurningDecay(t *testing.T) {
 			Element:    attributes.Electro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 61)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Dendro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 128)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Pyro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 188)
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Hydro,
 			Durability: 25,
 		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
+		Pattern: combat.NewDefSingleTarget(trg[0].Key(), combat.TargettableEnemy),
 	}, 206)
 
 	f := make(map[event.Event]int)
diff --git a/pkg/core/combat/combat_test.go b/pkg/core/combat/combat_test.go
index 8dacb45f5..22656a7ad 100644
--- a/pkg/core/combat/combat_test.go
+++ b/pkg/core/combat/combat_test.go
@@ -19,7 +19,7 @@ type (
 		hdlr        *Handler
 		src         int //source of gadget
 		idx         int
-		key         int
+		key         TargetKey
 		shp         Shape
 		alive       bool
 		collideWith [TargettableTypeCount]bool
@@ -39,8 +39,8 @@ func (t *testteam) ApplyHitlag(char int, factor, dur float64) {}
 // target
 func (t *testtarg) Index() int                                      { return t.idx }
 func (t *testtarg) SetIndex(i int)                                  { t.idx = i }
-func (t *testtarg) Key() int                                        { return t.key }
-func (t *testtarg) SetKey(i int)                                    { t.key = i }
+func (t *testtarg) Key() TargetKey                                  { return t.key }
+func (t *testtarg) SetKey(i TargetKey)                              { t.key = i }
 func (t *testtarg) Type() TargettableType                           { return t.typ }
 func (t *testtarg) Shape() Shape                                    { return t.shp }
 func (t *testtarg) Pos() (float64, float64)                         { return t.shp.Pos() }
@@ -71,7 +71,7 @@ func (t *testtarg) WillCollide(s Shape) bool {
 		return false
 	}
 }
-func (t *testtarg) AttackWillLand(a AttackPattern, src int) (bool, string) {
+func (t *testtarg) AttackWillLand(a AttackPattern, src TargetKey) (bool, string) {
 	//shape shouldn't be nil; panic here
 	if a.Shape == nil {
 		panic("unexpected nil shape")
@@ -96,7 +96,7 @@ func (t *testtarg) AttackWillLand(a AttackPattern, src int) (bool, string) {
 		return t.Shape().IntersectRectangle(*v), "intersect rectangle"
 	case *SingleTarget:
 		//only true if
-		return v.Target == t.idx, "target"
+		return v.Target == t.key, "target"
 	default:
 		return false, "unknown shape"
 	}
diff --git a/pkg/reactable/reactable_test.go b/pkg/reactable/reactable_test.go
index 726c546e2..62471ed5e 100644
--- a/pkg/reactable/reactable_test.go
+++ b/pkg/reactable/reactable_test.go
@@ -79,7 +79,7 @@ func makeAOEAttack(ele attributes.Element, dur combat.Durability) *combat.Attack
 	}
 }
 
-func makeSTAttack(ele attributes.Element, dur combat.Durability, trg int) *combat.AttackEvent {
+func makeSTAttack(ele attributes.Element, dur combat.Durability, trg combat.TargetKey) *combat.AttackEvent {
 	return &combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    ele,
@@ -98,8 +98,10 @@ type testTarget struct {
 	last combat.AttackEvent
 }
 
-func (t *testTarget) Type() combat.TargettableType                                  { return t.typ }
-func (t *testTarget) AttackWillLand(a combat.AttackPattern, src int) (bool, string) { return true, "" }
+func (t *testTarget) Type() combat.TargettableType { return t.typ }
+func (t *testTarget) AttackWillLand(a combat.AttackPattern, src combat.TargetKey) (bool, string) {
+	return true, ""
+}
 
 func (t *testTarget) Attack(atk *combat.AttackEvent, evt glog.Event) (float64, bool) {
 	t.last = *atk