Repository: genshinsim/gcsim
Pull request #970 (2022-10-02 23:25:31)

update bloom limit - max 5
continuation of the PR here: https://github.com/genshinsim/gcsim/pull/963

changed it so that the limit is enforced by core and force gadgets to have types. should make it easier for future gadgets that also have limits


Patch:
diff --git a/.gitignore b/.gitignore
index b0ae43375..b7f98979d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,3 @@ internal/characters/replace.txt
 cmd/gcsim/gcsim
 !release-notes.txt
 \.vscode/
-**/node_modules
\ No newline at end of file
diff --git a/app/src/Components/Character/CharacterCard.tsx b/app/src/Components/Character/CharacterCard.tsx
index ef4621b5c..6bc9ed9f8 100644
--- a/app/src/Components/Character/CharacterCard.tsx
+++ b/app/src/Components/Character/CharacterCard.tsx
@@ -113,7 +113,7 @@ export function CharacterCard({
         <Tooltip2 content={key}>
           <img
             key="key"
-            src={`/api/assets/artifacts/${key}_flower.png`}
+            src={`/images/artifacts/${key}_flower.png`}
             className="w-full h-8"
           />
         </Tooltip2>
@@ -221,7 +221,7 @@ export function CharacterCard({
           <div className="w-1/2">
             <img
               src={
-                '/api/assets/avatar/' +
+                '/images/avatar/' +
                 TransformTravelerKeyToName(char.name) +
                 '.png'
               }
diff --git a/app/src/Components/Character/CharacterTooltip.tsx b/app/src/Components/Character/CharacterTooltip.tsx
index ca7b96bb0..c364e25d5 100644
--- a/app/src/Components/Character/CharacterTooltip.tsx
+++ b/app/src/Components/Character/CharacterTooltip.tsx
@@ -17,7 +17,7 @@ export function CharacterTooltip({ char }: CharacterTooltipProps) {
       <div className="w-full border-b border-gray-500 mt-2 mb-2"></div>
       <div className="capitalize flex flex-row">
         <img
-          src={'/api/assets/weapons/' + char.weapon.name + '.png'}
+          src={'/images/weapons/' + char.weapon.name + '.png'}
           alt={char.weapon.name}
           className="wide:h-8 h-auto "
         />
diff --git a/app/src/Components/Nav/Nav.tsx b/app/src/Components/Nav/Nav.tsx
index c641df318..88ea05a1b 100644
--- a/app/src/Components/Nav/Nav.tsx
+++ b/app/src/Components/Nav/Nav.tsx
@@ -29,7 +29,7 @@ export default function Nav() {
     <Navbar>
       <NavbarGroup align={Alignment.LEFT} className="w-full">
         <img
-          src="/api/assets/logo.png"
+          src="/images/logo.png"
           className=" object-contain max-h-[75%] mt-auto mb-auto mr-1"
         />
         <Link href="/">
diff --git a/app/src/Components/Team/TeamCard.tsx b/app/src/Components/Team/TeamCard.tsx
index 62233edba..8b9edd86f 100644
--- a/app/src/Components/Team/TeamCard.tsx
+++ b/app/src/Components/Team/TeamCard.tsx
@@ -27,7 +27,7 @@ export function TeamCard({
             onClick={() => onCharacterClick(char.name)}
           >
             <img
-              src={'/api/assets/avatar/' + char.name + '.png'}
+              src={'/images/avatar/' + char.name + '.png'}
               alt={char.name}
               className="w-16"
               key={char.name}
diff --git a/app/src/Components/Viewer/Team/Character.tsx b/app/src/Components/Viewer/Team/Character.tsx
index 849ec0e5f..22a6443bc 100644
--- a/app/src/Components/Viewer/Team/Character.tsx
+++ b/app/src/Components/Viewer/Team/Character.tsx
@@ -67,7 +67,7 @@ export default function Character({ char }: Props) {
         >
           <img
             key="key"
-            src={`/api/assets/artifacts/${key}_flower.png`}
+            src={`/images/artifacts/${key}_flower.png`}
             alt={key}
             className="w-full h-auto "
           />
@@ -105,7 +105,7 @@ export default function Character({ char }: Props) {
         </div>
         <div className="w-1/2">
           <img
-            src={"/api/assets/avatar/" + char.name + ".png"}
+            src={"/images/avatar/" + char.name + ".png"}
             alt={char.name}
             className="w-full h-auto "
           />
@@ -116,7 +116,7 @@ export default function Character({ char }: Props) {
         <div className="flex flex-row">
           <div className="rounded-md">
             <img
-              src={`/api/assets/weapons/${char.weapon.name}.png`}
+              src={`/images/weapons/${char.weapon.name}.png`}
               alt={char.weapon.name}
               className="object-contain h-16"
             />
diff --git a/app/src/Components/Weapon/WeaponCard.tsx b/app/src/Components/Weapon/WeaponCard.tsx
index 5f231f30d..e84733de1 100644
--- a/app/src/Components/Weapon/WeaponCard.tsx
+++ b/app/src/Components/Weapon/WeaponCard.tsx
@@ -10,7 +10,7 @@ export function WeaponCard({ weapon }: { weapon: Weapon }) {
       <div className="flex flex-row">
         <div className="w-12 ">
           <img
-            src={`/api/assets/weapons/${weapon.name}.png`}
+            src={`/images/weapons/${weapon.name}.png`}
             alt={weapon.name}
             className="object-contain h-auto w-full"
           />
diff --git a/app/src/PageDatabase/CharacterCard.tsx b/app/src/PageDatabase/CharacterCard.tsx
index 477f61299..8593f5414 100644
--- a/app/src/PageDatabase/CharacterCard.tsx
+++ b/app/src/PageDatabase/CharacterCard.tsx
@@ -10,7 +10,7 @@ export function CharacterCard({ char, style = '' }: CharacterCardProps) {
       }
     >
       <img
-        src={'/api/assets/avatar/' + char + '.png'}
+        src={'/images/avatar/' + char + '.png'}
         alt={char}
         className="ml-auto h-32 wide:h-auto "
       />
diff --git a/app/src/Pages/DB/DB.tsx b/app/src/Pages/DB/DB.tsx
index 222b1a5d5..9117522aa 100644
--- a/app/src/Pages/DB/DB.tsx
+++ b/app/src/Pages/DB/DB.tsx
@@ -45,7 +45,7 @@ function CharTooltip({ char }: { char: DBCharInfo }) {
       <div className="w-full border-b border-gray-500 mt-2 mb-2"></div>
       <div className="capitalize flex flex-row">
         <img
-          src={'/api/assets/weapons/' + char.weapon + '.png'}
+          src={'/images/weapons/' + char.weapon + '.png'}
           alt={char.name}
           className="wide:h-8 h-auto "
         />
@@ -66,7 +66,7 @@ function TeamCard({ row, setCfg }: { row: DBItem; setCfg: () => void }) {
         <Tooltip2 content={<CharTooltip char={char} />}>
           <div className="hover:bg-gray-600 border border-gray-700 hover:border-gray-400 rounded-md relative">
             <img
-              src={'/api/assets/avatar/' + char.name + '.png'}
+              src={'/images/avatar/' + char.name + '.png'}
               alt={char.name}
               className="w-16"
               key={char.name}
diff --git a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditArtifactSets.tsx b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditArtifactSets.tsx
index b49265c3d..1c64af520 100644
--- a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditArtifactSets.tsx
+++ b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditArtifactSets.tsx
@@ -97,7 +97,7 @@ export function CharacterEditArtifactSets() {
       >
         <img
           key="key"
-          src={`/api/assets/artifacts/${key}_flower.png`}
+          src={`/images/artifacts/${key}_flower.png`}
           className="w-12"
         />
         <span className="font-bold">
diff --git a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditDetails.tsx b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditDetails.tsx
index bd147d2ad..ceac25047 100644
--- a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditDetails.tsx
+++ b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditDetails.tsx
@@ -74,7 +74,7 @@ export function CharacterEditDetails() {
     <div className="flex flex-row gap-2 flex-wrap">
       <div className="flex flex-col place-items-center gap-1 basis-full hd:basis-36">
         <img
-          src={'/api/assets/avatar/' + char.name + '.png'}
+          src={'/images/avatar/' + char.name + '.png'}
           alt={char.name}
           className="w-28"
         />
diff --git a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditWeaponAndArtifacts.tsx b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditWeaponAndArtifacts.tsx
index 4ebcb2811..a1931a708 100644
--- a/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditWeaponAndArtifacts.tsx
+++ b/app/src/Pages/Sim/Simulator/CharacterEdit/CharacterEditWeaponAndArtifacts.tsx
@@ -50,7 +50,7 @@ export function CharacterEditWeaponAndArtifacts() {
     <div className="flex flex-row gap-2 flex-wrap">
       <div className="flex flex-col place-items-center gap-1 basis-full hd:basis-36">
         <img
-          src={`/api/assets/weapons/${char.weapon.name}.png`}
+          src={`/images/weapons/${char.weapon.name}.png`}
           alt={char.weapon.name}
           className="w-28 "
         />
diff --git a/db/src/Components/Cards/CharacterCard.tsx b/db/src/Components/Cards/CharacterCard.tsx
index 5d7960d8d..c78aa5217 100644
--- a/db/src/Components/Cards/CharacterCard.tsx
+++ b/db/src/Components/Cards/CharacterCard.tsx
@@ -20,7 +20,7 @@ export function CharacterCard({
   return (
     <div className={cls} onClick={onClick ? onClick : () => {}}>
       <img
-        src={"/api/assets/avatar/" + char + ".png"}
+        src={"/assets/avatar/" + char + ".png"}
         alt={char}
         className="margin-auto"
       />
diff --git a/db/src/setupProxy.js b/db/src/setupProxy.js
index b36b702d0..2c3cab726 100644
--- a/db/src/setupProxy.js
+++ b/db/src/setupProxy.js
@@ -8,4 +8,11 @@ module.exports = function (app) {
       changeOrigin: true,
     })
   );
+  app.use(
+    "/assets",
+    createProxyMiddleware({
+      target: "http://localhost:3030",
+      changeOrigin: true,
+    })
+  );
 };
diff --git a/internal/characters/ayato/attack.go b/internal/characters/ayato/attack.go
index 00c3d1716..0b9275412 100644
--- a/internal/characters/ayato/attack.go
+++ b/internal/characters/ayato/attack.go
@@ -22,19 +22,14 @@ func init() {
 	// NA cancels
 	attackFrames = make([][]int, normalHitNum)
 
-	attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 21) // N1 -> CA
-	attackFrames[0][action.ActionAttack] = 15                                // N1 -> N2
-
-	attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1][0], 27) // N2 -> N3/CA
-
-	attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2][0], 34) // N3 -> CA
-	attackFrames[2][action.ActionAttack] = 30                                // N3 -> N4
-
-	attackFrames[3] = frames.InitNormalCancelSlice(attackHitmarks[3][1], 29) // N4 -> CA
-	attackFrames[3][action.ActionAttack] = 27                                // N4 -> N5
-
-	attackFrames[4] = frames.InitNormalCancelSlice(attackHitmarks[4][0], 63) // N5 -> N1
-	attackFrames[4][action.ActionCharge] = 500                               // N5 -> CA, TODO: this action is illegal; need better way to handle it
+	attackFrames[0] = frames.InitNormalCancelSlice(attackHitmarks[0][0], 24)
+	attackFrames[0][action.ActionAttack] = 15
+
+	// TODO: charge cancels are missing?
+	attackFrames[1] = frames.InitNormalCancelSlice(attackHitmarks[1][0], 27)
+	attackFrames[2] = frames.InitNormalCancelSlice(attackHitmarks[2][0], 30)
+	attackFrames[3] = frames.InitNormalCancelSlice(attackHitmarks[3][1], 27)
+	attackFrames[4] = frames.InitNormalCancelSlice(attackHitmarks[4][0], 63)
 
 	// NA (in skill) -> x
 	shunsuikenFrames = frames.InitNormalCancelSlice(shunsuikenHitmark, 23)
diff --git a/internal/characters/diluc/diluc.go b/internal/characters/diluc/diluc.go
index 7446e93a4..6b7297434 100644
--- a/internal/characters/diluc/diluc.go
+++ b/internal/characters/diluc/diluc.go
@@ -18,14 +18,13 @@ func init() {
 type char struct {
 	*tmpl.Character
 	eCounter int
+	eWindow  int
 	a4buff   []float64
 	c2buff   []float64
 	c2stack  int
 	c4buff   []float64
 }
 
-const eWindowKey = "diluc-e-window"
-
 func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile) error {
 	c := char{}
 	c.Character = tmpl.NewWithWrapper(s, w)
@@ -34,6 +33,7 @@ func NewChar(s *core.Core, w *character.CharWrapper, _ profile.CharacterProfile)
 	c.NormalHitNum = normalHitNum
 
 	c.eCounter = 0
+	c.eWindow = -1
 
 	w.Character = &c
 
@@ -60,7 +60,7 @@ func (c *char) Init() error {
 
 func (c *char) ActionReady(a action.Action, p map[string]int) bool {
 	// check if it is possible to use next skill
-	if a == action.ActionSkill && c.StatusIsActive(eWindowKey) {
+	if a == action.ActionSkill && c.Core.F < c.eWindow {
 		return true
 	}
 	return c.Character.ActionReady(a, p)
diff --git a/internal/characters/diluc/skill.go b/internal/characters/diluc/skill.go
index d14ea0dd6..670c011a0 100644
--- a/internal/characters/diluc/skill.go
+++ b/internal/characters/diluc/skill.go
@@ -43,7 +43,7 @@ func init() {
 
 func (c *char) Skill(p map[string]int) action.ActionInfo {
 	// reset counter
-	if !c.StatusIsActive(eWindowKey) {
+	if c.Core.F >= c.eWindow {
 		c.eCounter = 0
 	}
 
@@ -84,7 +84,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo {
 	}
 
 	// allow skill to be used again if 4s hasn't passed since last use
-	c.AddStatus(eWindowKey, 4*60, true)
+	c.eWindow = c.Core.F + 60*4
 
 	// store skill counter so we can determine which frames to return
 	idx := c.eCounter
@@ -96,7 +96,7 @@ func (c *char) Skill(p map[string]int) action.ActionInfo {
 		c.SetCD(action.ActionSkill, 10*60)
 	case 3:
 		// reset window since we're at 3rd use
-		c.DeleteStatus(eWindowKey)
+		c.eWindow = -1
 		c.eCounter = 0
 	}
 
diff --git a/internal/characters/heizou/skill.go b/internal/characters/heizou/skill.go
index 5a738612f..41a386cf3 100644
--- a/internal/characters/heizou/skill.go
+++ b/internal/characters/heizou/skill.go
@@ -62,7 +62,7 @@ func (c *char) skillRelease(p map[string]int, delay int) action.ActionInfo {
 			StrikeType:         combat.StrikeTypeDefault,
 			Element:            attributes.Anemo,
 			Durability:         50,
-			Mult:               skill[c.TalentLvlSkill()] + float64(c.decStack)*decBonus[c.TalentLvlSkill()],
+			Mult:               skill[c.TalentLvlAttack()] + float64(c.decStack)*decBonus[c.TalentLvlAttack()],
 			HitlagFactor:       0.01,
 			HitlagHaltFrames:   skillHitlagHaltFrame * 60,
 			CanBeDefenseHalted: false,
@@ -70,7 +70,7 @@ func (c *char) skillRelease(p map[string]int, delay int) action.ActionInfo {
 		AoE := 0.3
 		if c.decStack == 4 {
 			ai.Abil = "Heartstopper Strike (Max Stacks)"
-			ai.Mult += convicBonus[c.TalentLvlSkill()]
+			ai.Mult += convicBonus[c.TalentLvlAttack()]
 			ai.HitlagHaltFrames = skillHitlagMaxStackHaltFrame * 60
 			AoE = 1
 		}
diff --git a/internal/characters/xiangling/guoba.go b/internal/characters/xiangling/guoba.go
index 9c1c1d4d6..417981c5c 100644
--- a/internal/characters/xiangling/guoba.go
+++ b/internal/characters/xiangling/guoba.go
@@ -27,7 +27,7 @@ func (c *char) newGuoba(ai combat.AttackInfo) *panda {
 	}
 	x, y := c.Core.Combat.Player().Pos()
 	//TODO: guoba placement??
-	p.Gadget = gadget.New(c.Core, core.Coord{X: x, Y: y, R: 0.2})
+	p.Gadget = gadget.New(c.Core, core.Coord{X: x, Y: y, R: 0.2}, combat.GadgetTypGuoba)
 	p.Gadget.Duration = 438
 	p.Reactable = &reactable.Reactable{}
 	p.Reactable.Init(p, c.Core)
diff --git a/internal/weapons/common/goldenmajesty.go b/internal/weapons/common/goldenmajesty.go
index 395c62541..f9da775eb 100644
--- a/internal/weapons/common/goldenmajesty.go
+++ b/internal/weapons/common/goldenmajesty.go
@@ -23,7 +23,6 @@ func NewGoldenMajesty(c *core.Core, char *character.CharWrapper, p weapon.Weapon
 	w := &GoldenMajesty{}
 	r := p.Refine
 
-	const buffKey = "golden-majesty"
 	const icdKey = "golden-majesty-icd"
 
 	shd := .15 + float64(r)*.05
@@ -32,6 +31,7 @@ func NewGoldenMajesty(c *core.Core, char *character.CharWrapper, p weapon.Weapon
 	c.Player.Shields.AddShieldBonusMod(key, -1, func() (float64, bool) { return shd, false })
 
 	stacks := 0
+	expiry := 0
 	m := make([]float64, attributes.EndStatType)
 
 	c.Events.Subscribe(event.OnDamage, func(args ...interface{}) bool {
@@ -48,7 +48,7 @@ func NewGoldenMajesty(c *core.Core, char *character.CharWrapper, p weapon.Weapon
 		}
 		char.AddStatus(icdKey, 18, true)
 
-		if !char.StatModIsActive(buffKey) {
+		if c.F > expiry {
 			stacks = 0
 		}
 
@@ -58,7 +58,7 @@ func NewGoldenMajesty(c *core.Core, char *character.CharWrapper, p weapon.Weapon
 		}
 
 		char.AddStatMod(character.StatMod{
-			Base:         modifier.NewBaseWithHitlag(buffKey, 60*8),
+			Base:         modifier.NewBaseWithHitlag("golden-majesty", 60*8),
 			AffectedStat: attributes.NoStat,
 			Amount: func() ([]float64, bool) {
 				m[attributes.ATKP] = atkbuff * float64(stacks)
diff --git a/pkg/conditional/conditional.go b/pkg/conditional/conditional.go
index 9347cc871..b7c91fcaf 100644
--- a/pkg/conditional/conditional.go
+++ b/pkg/conditional/conditional.go
@@ -29,6 +29,8 @@ func Eval(c *core.Core, fields []string) (any, error) {
 		return c.Player.Stam, nil
 	case "construct":
 		return evalConstruct(c, fields)
+	case "gadgets":
+		return evalGadgets(c, fields)
 	case "keys":
 		return evalKeys(c, fields)
 	default:
diff --git a/pkg/conditional/gadgets.go b/pkg/conditional/gadgets.go
new file mode 100644
index 000000000..641a6f2ea
--- /dev/null
+++ b/pkg/conditional/gadgets.go
@@ -0,0 +1,36 @@
+package conditional
+
+import (
+	"fmt"
+
+	"github.com/genshinsim/gcsim/pkg/core"
+	"github.com/genshinsim/gcsim/pkg/reactable"
+)
+
+func evalGadgets(c *core.Core, fields []string) (int, error) {
+	if err := fieldsCheck(fields, 3, "gadgets"); err != nil {
+		return 0, err
+	}
+	switch fields[1] {
+	case "dendrocore":
+		return evalDendroCore(c, fields[2])
+	default:
+		return 0, fmt.Errorf("bad gadgets condition: invalid criteria %v", fields[1])
+	}
+}
+
+func evalDendroCore(c *core.Core, key string) (int, error) {
+	switch key {
+	case "count":
+		count := 0
+		for i := 0; i < c.Combat.GadgetCount(); i++ {
+			if _, ok := c.Combat.Gadget(i).(*reactable.DendroCore); ok {
+				count++
+			}
+		}
+		return count, nil
+	default:
+		return 0, fmt.Errorf("bad gadgets (dendrocore) condition: invalid criteria %v", key)
+	}
+
+}
diff --git a/pkg/core/combat/combat.go b/pkg/core/combat/combat.go
index 7091a5b22..802c0fc8c 100644
--- a/pkg/core/combat/combat.go
+++ b/pkg/core/combat/combat.go
@@ -25,7 +25,7 @@ type Character interface {
 type Handler struct {
 	Opt
 	enemies     []Target
-	gadgets     []Target
+	gadgets     []Gadget
 	player      Target
 	TotalDamage float64
 	gccount     int
@@ -51,7 +51,7 @@ func New(opt Opt) *Handler {
 		keycount: 1,
 	}
 	h.enemies = make([]Target, 0, 5)
-	h.gadgets = make([]Target, 0, 10)
+	h.gadgets = make([]Gadget, 0, 10)
 
 	return h
 }
diff --git a/pkg/core/combat/gadget.go b/pkg/core/combat/gadget.go
index 8c24abd99..2189dd47c 100644
--- a/pkg/core/combat/gadget.go
+++ b/pkg/core/combat/gadget.go
@@ -1,33 +1,78 @@
 package combat
 
-func (h *Handler) RemoveGadget(i int) {
-	if i < 0 || i >= len(h.gadgets) {
-		return
-	}
-	//set to nil for now; we should clean up every so often???
-	//TODO: how often do we clean out nil entries? if at all?
-	h.gadgets[i] = nil
+import "math"
+
+type GadgetTyp int
+
+const (
+	GadgetTypUnknown GadgetTyp = iota
+	GadgetTypDendroCore
+	GadgetTypGuoba
+	GadgetTypTest
+	EndGadgetTyp
+)
+
+var gadgetLimits []int
+
+func init() {
+	gadgetLimits = make([]int, EndGadgetTyp)
+	gadgetLimits[GadgetTypDendroCore] = 5
+	gadgetLimits[GadgetTypTest] = 2
+}
+
+type Gadget interface {
+	Target
+	Src() int
+	GadgetTyp() GadgetTyp
 }
 
-func (h *Handler) AddGadget(t Target) {
+func (h *Handler) RemoveGadget(key int) {
+	h.ReplaceGadget(key, nil)
+}
+
+func (h *Handler) AddGadget(t Gadget) {
+	//check for hard coded limit
+	if gadgetLimits[t.GadgetTyp()] > 0 {
+		//should kill oldest one if > limit
+		f := math.MaxInt64
+		oldest := -1
+		count := 0
+		for i, v := range h.gadgets {
+			if v == nil || v.GadgetTyp() != t.GadgetTyp() {
+				continue
+			}
+			count++
+			if v.Src() < f {
+				f = v.Src()
+				oldest = i
+			}
+		}
+		if count == gadgetLimits[t.GadgetTyp()] {
+			h.gadgets[oldest].Kill()
+		}
+	}
 	h.gadgets = append(h.gadgets, t)
 	t.SetIndex(len(h.gadgets) - 1)
 	t.SetKey(h.nextkey())
 }
 
-func (h *Handler) ReplaceGadget(key int, t Target) {
+func (h *Handler) ReplaceGadget(key int, t Gadget) {
 	//do nothing if not found
 	for i, v := range h.gadgets {
-		if v.Key() == key {
+		if v != nil && v.Key() == key {
 			h.gadgets[i] = t
 		}
 	}
 }
 
-func (h *Handler) Gadget(i int) Target {
+func (h *Handler) Gadget(i int) Gadget {
 	return h.gadgets[i]
 }
 
+func (h *Handler) Gadgets() []Gadget {
+	return h.gadgets
+}
+
 func (h *Handler) GadgetCount() int {
 	count := 0
 	for _, v := range h.gadgets {
diff --git a/pkg/gadget/gadget.go b/pkg/gadget/gadget.go
index fad3e61f4..568741a0e 100644
--- a/pkg/gadget/gadget.go
+++ b/pkg/gadget/gadget.go
@@ -11,18 +11,23 @@ import (
 
 type Gadget struct {
 	*target.Target
+	src             int
+	gadgetTyp       combat.GadgetTyp
 	core            *core.Core
-	OnRemoved       func()
-	ThinkInterval   int //should be > 0
+	OnKill          func()
+	OnExpiry        func() // only called if gadget dies from expiry
+	ThinkInterval   int    //should be > 0
 	OnThinkInterval func()
 	Duration        int //how long gadget should live for; use -1 for infinite
 	//internal helper
 	sinceLastThink int
 }
 
-func New(core *core.Core, pos core.Coord) *Gadget {
+func New(core *core.Core, pos core.Coord, typ combat.GadgetTyp) *Gadget {
 	g := &Gadget{
-		core: core,
+		core:      core,
+		src:       core.F,
+		gadgetTyp: typ,
 	}
 	g.Target = target.New(core, pos.X, pos.Y, pos.R)
 	return g
@@ -30,13 +35,15 @@ func New(core *core.Core, pos core.Coord) *Gadget {
 
 func (g *Gadget) Kill() {
 	g.Target.Kill()
-	if g.OnRemoved != nil {
-		g.OnRemoved()
+	if g.OnKill != nil {
+		g.OnKill()
 	}
-	g.core.Combat.RemoveGadget(g.Index())
+	g.core.Combat.RemoveGadget(g.Key())
 }
 
 func (g *Gadget) Type() combat.TargettableType { return combat.TargettableGadget }
+func (g *Gadget) Src() int                     { return g.src }
+func (g *Gadget) GadgetTyp() combat.GadgetTyp  { return g.gadgetTyp }
 
 func (g *Gadget) Tick() {
 	if g.OnThinkInterval != nil && g.ThinkInterval > 0 {
@@ -52,7 +59,10 @@ func (g *Gadget) Tick() {
 	if g.Duration != -1 && g.Duration > 0 {
 		g.Duration--
 		if g.Duration == 0 {
-			g.Kill()
+			if g.OnExpiry != nil {
+				g.OnExpiry()
+			}
+			g.core.Combat.RemoveGadget(g.Key())
 		}
 	}
 }
diff --git a/pkg/gcs/ast/item.go b/pkg/gcs/ast/item.go
index 38b94691f..6fe8dd8d1 100644
--- a/pkg/gcs/ast/item.go
+++ b/pkg/gcs/ast/item.go
@@ -84,7 +84,6 @@ const (
 	keywordContinue    // continue
 	keywordFallthrough // fallthrough
 	keywordReturn      // return
-	keywordFor         // for
 	// Keywords after this are specific to Genshin (i.e. not generic scripting keywords)
 	// These are special char related keywords
 	keywordOptions           // options
diff --git a/pkg/gcs/ast/keys.go b/pkg/gcs/ast/keys.go
index d898e60fd..ad3034cc3 100644
--- a/pkg/gcs/ast/keys.go
+++ b/pkg/gcs/ast/keys.go
@@ -19,7 +19,6 @@ var key = map[string]TokenType{
 	"continue":    keywordContinue,
 	"fallthrough": keywordFallthrough,
 	"return":      keywordReturn,
-	"for":         keywordFor,
 	//genshin specific keywords
 	"options":             keywordOptions,
 	"add":                 keywordAdd,
diff --git a/pkg/gcs/ast/node.go b/pkg/gcs/ast/node.go
index 270a63d1b..5dbb7e742 100644
--- a/pkg/gcs/ast/node.go
+++ b/pkg/gcs/ast/node.go
@@ -111,15 +111,6 @@ type (
 		Condition  Expr       //TODO: this should be an expr?
 		WhileBlock *BlockStmt // What to execute if true
 	}
-
-	// ForStmt represents a for block
-	ForStmt struct {
-		Pos
-		Init Stmt // initialization statement; or nil
-		Cond Expr // condition; or nil
-		Post Stmt // post iteration statement; or nil
-		Body *BlockStmt
-	}
 )
 
 type CtrlTyp int
@@ -143,7 +134,6 @@ func (*SwitchStmt) stmtNode() {}
 func (*CaseStmt) stmtNode()   {}
 func (*FnStmt) stmtNode()     {}
 func (*WhileStmt) stmtNode()  {}
-func (*ForStmt) stmtNode()    {}
 
 // BlockStmt.
 func newBlockStmt(pos Pos) *BlockStmt {
@@ -583,60 +573,6 @@ func (w *WhileStmt) Copy() Node {
 	return w.CopyWhileStmt()
 }
 
-// ForStmt.
-
-func (f *ForStmt) String() string {
-	var sb strings.Builder
-	f.writeTo(&sb)
-	return sb.String()
-}
-
-func (f *ForStmt) writeTo(sb *strings.Builder) {
-	sb.WriteString("for ")
-	if f.Init != nil {
-		f.Init.writeTo(sb)
-	}
-	sb.WriteString("; ")
-	if f.Cond != nil {
-		f.Cond.writeTo(sb)
-	}
-	sb.WriteString("; ")
-	if f.Post != nil {
-		f.Post.writeTo(sb)
-	}
-	sb.WriteString(" {\n")
-	f.Body.writeTo(sb)
-	sb.WriteString("}")
-}
-
-func (f *ForStmt) CopyForStmt() *ForStmt {
-	if f == nil {
-		return nil
-	}
-	n := &ForStmt{
-		Pos:  f.Pos,
-		Body: f.Body.CopyBlock(),
-	}
-	if f.Init != nil {
-		n.Init = f.Init.CopyStmt()
-	}
-	if f.Cond != nil {
-		n.Cond = f.Cond.CopyExpr()
-	}
-	if f.Post != nil {
-		n.Post = f.Post.CopyStmt()
-	}
-	return n
-}
-
-func (f *ForStmt) CopyStmt() Stmt {
-	return f.CopyForStmt()
-}
-
-func (f *ForStmt) Copy() Node {
-	return f.CopyForStmt()
-}
-
 // Expr.
 
 type Expr interface {
diff --git a/pkg/gcs/ast/parse.go b/pkg/gcs/ast/parse.go
index ce4ba8fe7..235c82c31 100644
--- a/pkg/gcs/ast/parse.go
+++ b/pkg/gcs/ast/parse.go
@@ -214,9 +214,6 @@ func (p *Parser) parseStatement() (Node, error) {
 	case keywordWhile:
 		node, err = p.parseWhile()
 		hasSemi = false
-	case keywordFor:
-		node, err = p.parseFor()
-		hasSemi = false
 	case itemLeftBrace:
 		node, err = p.parseBlock()
 		hasSemi = false
@@ -471,79 +468,6 @@ func (p *Parser) parseWhile() (Stmt, error) {
 	return stmt, err
 }
 
-// for <init ;> <cond> <; post> { <body> }
-// for { <body > }
-func (p *Parser) existVarDecl() bool {
-	switch n := p.peek(); n.Typ {
-	case keywordLet:
-		return true
-	case itemIdentifier:
-		p.next()
-		b := p.peek().Typ == itemAssign
-		p.backup()
-		return b
-	}
-	return false
-}
-
-func (p *Parser) parseFor() (Stmt, error) {
-	n := p.next()
-
-	stmt := &ForStmt{
-		Pos: n.pos,
-	}
-
-	var err error
-
-	if n := p.peek(); n.Typ == itemLeftBrace {
-		stmt.Body, err = p.parseBlock() //parse block here
-		return stmt, err
-	}
-
-	//init
-	if p.existVarDecl() {
-		if n := p.peek(); n.Typ == keywordLet {
-			stmt.Init, err = p.parseLet()
-		} else {
-			stmt.Init, err = p.parseAssign()
-		}
-		if err != nil {
-			return nil, err
-		}
-
-		if n := p.peek(); n.Typ != itemTerminateLine {
-			return nil, fmt.Errorf("ln%v: expecting ; after statement, got %v", n.line, n.Val)
-		}
-		p.next() //skip ;
-	}
-
-	//cond
-	stmt.Cond, err = p.parseExpr(Lowest)
-	if err != nil {
-		return nil, err
-	}
-
-	//post
-	if n := p.peek(); n.Typ == itemTerminateLine {
-		p.next() //skip ;
-		if n := p.peek(); n.Typ != itemLeftBrace {
-			stmt.Post, err = p.parseAssign()
-			if err != nil {
-				return nil, err
-			}
-		}
-	}
-
-	//expecting a { next
-	if n := p.peek(); n.Typ != itemLeftBrace {
-		return nil, fmt.Errorf("ln%v: expecting { after for, got %v", n.line, n.Val)
-	}
-
-	stmt.Body, err = p.parseBlock() //parse block here
-
-	return stmt, err
-}
-
 func (p *Parser) parseFn() (Stmt, error) {
 	//fn ident(...ident){ block }
 	//consume fn
diff --git a/pkg/gcs/stmt.go b/pkg/gcs/stmt.go
index d029c530a..db83cc39f 100644
--- a/pkg/gcs/stmt.go
+++ b/pkg/gcs/stmt.go
@@ -26,8 +26,6 @@ func (e *Eval) evalStmt(s ast.Stmt, env *Env) (Obj, error) {
 		return e.evalIfStmt(v, env)
 	case *ast.WhileStmt:
 		return e.evalWhileStmt(v, env)
-	case *ast.ForStmt:
-		return e.evalForStmt(v, env)
 	case *ast.AssignStmt:
 		return e.evalAssignStmt(v, env)
 	case *ast.SwitchStmt:
@@ -76,8 +74,7 @@ func (e *Eval) evalLet(l *ast.LetStmt, env *Env) (Obj, error) {
 	if exist {
 		return nil, fmt.Errorf("variable %v already exists; cannot redeclare", l.Ident.Val)
 	}
-	num := *v //value copying
-	env.varMap[l.Ident.Val] = &num
+	env.varMap[l.Ident.Val] = v
 	return &null{}, nil
 }
 
@@ -206,42 +203,6 @@ func (e *Eval) evalWhileStmt(w *ast.WhileStmt, env *Env) (Obj, error) {
 	return &null{}, nil
 }
 
-func (e *Eval) evalForStmt(f *ast.ForStmt, env *Env) (Obj, error) {
-	scope := NewEnv(env)
-	if f.Init != nil {
-		e.evalStmt(f.Init, scope)
-	}
-
-	for {
-		if f.Cond != nil {
-			//if condition is false, break
-			cond, err := e.evalExpr(f.Cond, scope)
-			if err != nil {
-				return nil, err
-			}
-			if !otob(cond) {
-				break
-			}
-		}
-
-		//execute block
-		res, err := e.evalBlock(f.Body, scope)
-		if err != nil {
-			return nil, err
-		}
-
-		//if result is a break stmt, stop loo
-		if t, ok := res.(*ctrl); ok && t.typ == ast.CtrlBreak {
-			break
-		}
-
-		if f.Post != nil {
-			e.evalStmt(f.Post, scope)
-		}
-	}
-	return &null{}, nil
-}
-
 func (e *Eval) evalSwitchStmt(swt *ast.SwitchStmt, env *Env) (Obj, error) {
 	cond, err := e.evalExpr(swt.Condition, env)
 	if err != nil {
diff --git a/pkg/reactable/bloom.go b/pkg/reactable/bloom.go
index 0d62061d7..37433cd98 100644
--- a/pkg/reactable/bloom.go
+++ b/pkg/reactable/bloom.go
@@ -72,29 +72,32 @@ func (r *Reactable) tryQuickenBloom(a *combat.AttackEvent) {
 
 type DendroCore struct {
 	*gadget.Gadget
+	srcFrame int
 }
 
 func (r *Reactable) addBloomGadget(a *combat.AttackEvent) {
 	r.core.Tasks.Add(func() {
-		var t combat.Target = NewDendroCore(r.core, r.self, a)
+		var t combat.Gadget = NewDendroCore(r.core, r.self, a)
 		r.core.Combat.AddGadget(t)
 		r.core.Events.Emit(event.OnDendroCore, t)
 	}, DendroCoreDelay)
 }
 
 func NewDendroCore(c *core.Core, pos combat.Positional, a *combat.AttackEvent) *DendroCore {
-	s := &DendroCore{}
+	s := &DendroCore{
+		srcFrame: c.F,
+	}
 
 	x, y := pos.Pos()
 	// for simplicity, seeds spawn randomly within 1 radius of target
 	x = x + 2*c.Rand.Float64() - 1
 	y = y + 2*c.Rand.Float64() - 1
-	s.Gadget = gadget.New(c, core.Coord{X: x, Y: y, R: 0.2})
+	s.Gadget = gadget.New(c, core.Coord{X: x, Y: y, R: 0.2}, combat.GadgetTypDendroCore)
 	s.Gadget.Duration = 300 // ??
 
 	char := s.Core.Player.ByIndex(a.Info.ActorIndex)
 
-	s.Gadget.OnRemoved = func() {
+	explode := func() {
 		ai := NewBloomAttack(char, s)
 		c.QueueAttack(ai, combat.NewCircleHit(s, 5, false, combat.TargettableEnemy), -1, 1)
 
@@ -103,6 +106,9 @@ func NewDendroCore(c *core.Core, pos combat.Positional, a *combat.AttackEvent) *
 		ai.FlatDmg = 0.05 * ai.FlatDmg
 		c.QueueAttack(ai, combat.NewCircleHit(s.Gadget, 5, true, combat.TargettablePlayer), -1, 1)
 	}
+	//TODO: should bloom do damage if it blows up due to limit reached?
+	s.Gadget.OnExpiry = explode
+	s.Gadget.OnKill = explode
 
 	return s
 }
@@ -136,7 +142,8 @@ func (s *DendroCore) Attack(atk *combat.AttackEvent, evt glog.Event) (float64, b
 			s.Core.QueueAttack(ai, combat.NewCircleHit(s.Core.Combat.Enemy(enemies[0]), 1, true, combat.TargettablePlayer), -1, 5)
 		}
 
-		s.Core.Combat.RemoveGadget(s.Gadget.Index())
+		s.Gadget.OnKill = nil
+		s.Gadget.Kill()
 		s.Core.Events.Emit(event.OnHyperbloom, s.Gadget, atk)
 	case attributes.Pyro:
 		// trigger burgeon, aoe dendro damage
@@ -150,7 +157,8 @@ func (s *DendroCore) Attack(atk *combat.AttackEvent, evt glog.Event) (float64, b
 		ai.FlatDmg = 0.05 * ai.FlatDmg
 		s.Core.QueueAttack(ai, combat.NewCircleHit(s.Gadget, 5, true, combat.TargettablePlayer), -1, 1)
 
-		s.Core.Combat.RemoveGadget(s.Gadget.Index())
+		s.Gadget.OnKill = nil
+		s.Gadget.Kill()
 		s.Core.Events.Emit(event.OnBurgeon, s.Gadget, atk)
 	default:
 		return 0, false
diff --git a/pkg/reactable/burning.go b/pkg/reactable/burning.go
index c9f0d9da1..b0650334a 100644
--- a/pkg/reactable/burning.go
+++ b/pkg/reactable/burning.go
@@ -58,16 +58,6 @@ func (r *Reactable) tryBurning(a *combat.AttackEvent) {
 	}
 }
 
-func (r *Reactable) attachBurningFuel(dur combat.Durability, mult combat.Durability) {
-	//burning fuel always overwrites
-	r.Durability[ModifierBurningFuel] = mult * dur
-	decayRate := mult * dur / (6*dur + 420)
-	if decayRate < 10.0/60.0 {
-		decayRate = 10.0 / 60.0
-	}
-	r.DecayRate[ModifierBurningFuel] = decayRate
-}
-
 func (r *Reactable) calcBurningDmg(a *combat.AttackEvent) {
 	atk := combat.AttackInfo{
 		ActorIndex:       a.Info.ActorIndex,
diff --git a/pkg/reactable/reactable.go b/pkg/reactable/reactable.go
index 6deb231e1..b74b77f0b 100644
--- a/pkg/reactable/reactable.go
+++ b/pkg/reactable/reactable.go
@@ -219,6 +219,16 @@ func (r *Reactable) attachOverlapRefreshDuration(mod ReactableModifier, amt comb
 	r.DecayRate[mod] = amt / length
 }
 
+func (r *Reactable) attachBurningFuel(dur combat.Durability, mult combat.Durability) {
+	//burning fuel always overwrites
+	r.Durability[ModifierBurningFuel] = mult * dur
+	decayRate := mult * dur / (6*dur + 420)
+	if decayRate < 10.0/60.0 {
+		decayRate = 10.0 / 60.0
+	}
+	r.DecayRate[ModifierBurningFuel] = decayRate
+}
+
 func (r *Reactable) attachBurning() {
 	r.Durability[ModifierBurning] = 50
 	r.DecayRate[ModifierBurning] = 0
diff --git a/services/assets/Dockerfile b/services/assets/Dockerfile
deleted file mode 100644
index d3c14460c..000000000
--- a/services/assets/Dockerfile
+++ /dev/null
@@ -1,13 +0,0 @@
-FROM golang:latest
-# RUN mkdir /usr/src/app
-WORKDIR /app
-
-COPY go.mod go.sum ./
-RUN ls
-RUN go mod download
-COPY assets assets
-
-WORKDIR /app/assets
-RUN go build -o /assets-server 
-
-ENTRYPOINT ["/assets-server"]
diff --git a/services/assets/main.go b/services/assets/main.go
deleted file mode 100644
index 6685554c8..000000000
--- a/services/assets/main.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package main
-
-import (
-	"log"
-	"net/http"
-	"os"
-)
-
-func main() {
-	folder := os.Getenv("ASSETS_PATH")
-	fs := http.FileServer(http.Dir(folder))
-	http.Handle("/assets/", http.StripPrefix("/assets/", fs))
-
-	log.Print("listening on :3000 ...")
-	err := http.ListenAndServe(":3000", nil)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-}
diff --git a/services/docker-compose.yml b/services/docker-compose.yml
index 88b63bd55..a8c6b12ce 100644
--- a/services/docker-compose.yml
+++ b/services/docker-compose.yml
@@ -10,26 +10,12 @@ services:
     links:
       - postgrest:services
       - embed:services
-      - assets:services
     environment:
       # - TUNNEL_URL=http://postgrest:3000
       - CF_TUNNEL_TOKEN
     depends_on:
       - postgrest
       - embed
-      - assets
-
-  assets:
-    container_name: assets
-    build:
-      context: .
-      dockerfile: ./assets/Dockerfile
-    volumes:
-      - .:/app
-      - ../assets:/assets
-    environment:
-      - ASSETS_PATH=/assets
-    restart: unless-stopped
 
   postgrest:
     image: postgrest/postgrest
diff --git a/services/worker/src/assets/index.ts b/services/worker/src/assets/index.ts
deleted file mode 100644
index e1ea04f56..000000000
--- a/services/worker/src/assets/index.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export async function handleAssets(
-  request: Request,
-  event: FetchEvent
-): Promise<Response> {
-  const cacheUrl = new URL(request.url).pathname.replace("/api", "");
-  const endpoint = ASSETS_ENDPOINT + cacheUrl;
-  const cacheKey = new Request(endpoint, request);
-  console.log(`checking for cache key: ${endpoint}`);
-  const cache = caches.default;
-
-  let response = await cache.match(cacheKey);
-
-  if (!response) {
-    console.log(
-      `Response for request url: ${endpoint} not present in cache. Fetching and caching request.`
-    );
-
-    const resp = await fetch(new Request(endpoint), {
-      cf: {
-        cacheTtl: 60 * 24 * 60 * 60,
-        cacheEverything: true,
-      },
-    });
-
-    response = new Response(resp.body, resp);
-    response.headers.set('Cache-Control', 'max-age=5184000');
-
-    event.waitUntil(cache.put(cacheKey, response.clone()));
-  }
-
-  return response;
-}
diff --git a/services/worker/src/bindings.d.ts b/services/worker/src/bindings.d.ts
index 814b14a48..7cc40a7e8 100644
--- a/services/worker/src/bindings.d.ts
+++ b/services/worker/src/bindings.d.ts
@@ -4,8 +4,7 @@ declare global {
   const JWT_SECRET: string;
   const DISCORD_ID: string;
   const DISCORD_SECRET: string;
-  const POSTGREST_ENDPOINT: string; //secret url for POSTGREST backend
-  const PREVIEW_ENDPOINT: string; //secret url for embed server
-  const ASSETS_ENDPOINT: string; //secret url for assets server
+  const POSTGREST_ENDPOINT: string;
+  const PREVIEW_ENDPOINT: string;
   const USER_TOKENS: KVNamespace;
 }
diff --git a/services/worker/src/index.ts b/services/worker/src/index.ts
index cc77b3fa4..8e9407dc5 100644
--- a/services/worker/src/index.ts
+++ b/services/worker/src/index.ts
@@ -10,7 +10,6 @@
 
 import { PostgrestClient } from '@supabase/postgrest-js';
 import { Router } from 'itty-router';
-import { handleAssets } from './assets';
 import { handleAuth } from './auth';
 import { handleListDBChars, handleListDBSims } from './db';
 import { handleEnka } from './enka';
@@ -28,7 +27,6 @@ router.post('/api/share', handleShare);
 //cached
 router.get('/api/view/:key', handleView);
 router.get('/api/preview/:key', handlePreview);
-router.get('/api/assets/*', handleAssets);
 
 //enka
 router.get('/api/enka/:key', handleEnka);


Test patch:
diff --git a/internal/tests/reactable/bloom_test.go b/internal/tests/reactable/bloom_test.go
index a5d6ee679..70734b8db 100644
--- a/internal/tests/reactable/bloom_test.go
+++ b/internal/tests/reactable/bloom_test.go
@@ -1,6 +1,7 @@
 package reactable_test
 
 import (
+	"math"
 	"testing"
 
 	"github.com/genshinsim/gcsim/pkg/core/attributes"
@@ -43,7 +44,6 @@ func TestHydroBloom(t *testing.T) {
 	if trg[0].AuraContains(attributes.Hydro, attributes.Dendro) {
 		t.Errorf("expecting target to not contain any remaining hydro or dendro aura, got %v", trg[0].ActiveAuraString())
 	}
-
 }
 
 func TestDendroBloom(t *testing.T) {
@@ -81,8 +81,8 @@ func TestDendroBloom(t *testing.T) {
 	if trg[0].AuraContains(attributes.Hydro, attributes.Dendro) {
 		t.Errorf("expecting target to not contain any remaining hydro or dendro aura, got %v", trg[0].ActiveAuraString())
 	}
-
 }
+
 func TestECBloom(t *testing.T) {
 	c, trg := makeCore(1)
 	err := c.Init()
@@ -91,7 +91,7 @@ func TestECBloom(t *testing.T) {
 		t.FailNow()
 	}
 
-	//create 2 seeds with ec
+	// create 2 seeds with ec
 	c.QueueAttackEvent(&combat.AttackEvent{
 		Info: combat.AttackInfo{
 			Element:    attributes.Hydro,
@@ -126,5 +126,93 @@ func TestECBloom(t *testing.T) {
 	if trg[0].AuraContains(attributes.Hydro, attributes.Dendro) {
 		t.Errorf("expecting target to not contain any remaining hydro or dendro aura, got %v", trg[0].ActiveAuraString())
 	}
+}
+
+func TestBloomSeedLimit(t *testing.T) {
+	c, trg := makeCore(10)
+	err := c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+
+	c.QueueAttackEvent(&combat.AttackEvent{
+		Info: combat.AttackInfo{
+			Element:    attributes.Hydro,
+			Durability: 25,
+		},
+		Pattern: combat.NewCircleHit(trg[0], 100, false, combat.TargettableEnemy, combat.TargettableGadget),
+	}, 0)
+	advanceCoreFrame(c)
+	c.QueueAttackEvent(&combat.AttackEvent{
+		Info: combat.AttackInfo{
+			Element:    attributes.Dendro,
+			Durability: 25,
+		},
+		Pattern: combat.NewCircleHit(trg[0], 100, false, combat.TargettableEnemy, combat.TargettableGadget),
+	}, 0)
+	advanceCoreFrame(c)
+
+	for i := 0; i < reactable.DendroCoreDelay+1; i++ {
+		advanceCoreFrame(c)
+	}
+
+	if c.Combat.GadgetCount() != 5 {
+		t.Errorf("expected only 5 seeds remaining, got %v", c.Combat.GadgetCount())
+	}
+}
+
+func TestBloomOldestDeleted(t *testing.T) {
+	c, trg := makeCore(1)
+	err := c.Init()
+	if err != nil {
+		t.Errorf("error initializing core: %v", err)
+		t.FailNow()
+	}
+
+	//oldest should be the 2nd one, which is frame 3 ?
+	for i := 0; i < 6; i++ {
+		c.QueueAttackEvent(&combat.AttackEvent{
+			Info: combat.AttackInfo{
+				Element:    attributes.Hydro,
+				Durability: 25,
+			},
+			Pattern: combat.NewCircleHit(trg[0], 100, false, combat.TargettableEnemy, combat.TargettableGadget),
+		}, 0)
+		advanceCoreFrame(c)
+		c.QueueAttackEvent(&combat.AttackEvent{
+			Info: combat.AttackInfo{
+				Element:    attributes.Dendro,
+				Durability: 25,
+			},
+			Pattern: combat.NewCircleHit(trg[0], 100, false, combat.TargettableEnemy, combat.TargettableGadget),
+		}, 0)
+		advanceCoreFrame(c)
+	}
+
+	for i := 0; i < reactable.DendroCoreDelay+1; i++ {
+		advanceCoreFrame(c)
+	}
+
+	if c.Combat.GadgetCount() != 5 {
+		t.Errorf("expected only 5 seeds remaining, got %v", c.Combat.GadgetCount())
+	}
+
+	//find oldest
+	f := math.MaxInt64
+	oldest := -1
+	for i, v := range c.Combat.Gadgets() {
+		if v == nil || v.GadgetTyp() != combat.GadgetTypDendroCore {
+			continue
+		}
+		if v.Src() < f {
+			f = v.Src()
+			oldest = i
+		}
+	}
+	og := c.Combat.Gadget(oldest)
+	if og.Src() != 3+reactable.DendroCoreDelay {
+		t.Errorf("expecting oldest gadget to be from frame %v, got %v", reactable.DendroCoreDelay+3, og.Src())
+	}
 
 }
diff --git a/internal/tests/reactable/burning_test.go b/internal/tests/reactable/burning_test.go
index 35e008926..77da31dce 100644
--- a/internal/tests/reactable/burning_test.go
+++ b/internal/tests/reactable/burning_test.go
@@ -190,249 +190,5 @@ func TestBurningQuickenFuel(t *testing.T) {
 			break
 		}
 	}
-}
-
-func TestPyroDendroCoexist(t *testing.T) {
-	c, trg := makeCore(1)
-	err := c.Init()
-	if err != nil {
-		t.Errorf("error initializing core: %v", err)
-		t.FailNow()
-	}
-	//https://www.youtube.com/watch?v=dXzQTNCYfeU&list=PL10DrkffqpyuwG8i0JOq-TgcqPES6bsja&index=16
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 121)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Pyro,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 133)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 195)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Cryo,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 202)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Pyro,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 249)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Cryo,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 327)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Cryo,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 344)
-	//pyro ended 546, dendro ended 689
-
-	f := make(map[event.Event]int)
-	cb := func(evt event.Event) func(args ...interface{}) bool {
-		return func(args ...interface{}) bool {
-			f[evt] = c.F
-			return false
-		}
-	}
-	for i := event.ReactionEventStartDelim; i < event.ReactionEventEndDelim; i++ {
-		c.Events.Subscribe(i, cb(i), fmt.Sprintf("event-%v", i))
-	}
-	i := 0
-
-	for ; i < 1000; i++ {
-		advanceCoreFrame(c)
-		fmt.Printf("%v: %v\n", i, trg[0].ActiveAuraString())
-	}
-
-}
-
-func TestDendroDecayTry1(t *testing.T) {
-	c, trg := makeCore(1)
-	err := c.Init()
-	if err != nil {
-		t.Errorf("error initializing core: %v", err)
-		t.FailNow()
-	}
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 155)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Pyro,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 168)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 50,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 230)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Hydro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 263)
-
-	f := make(map[event.Event]int)
-	cb := func(evt event.Event) func(args ...interface{}) bool {
-		return func(args ...interface{}) bool {
-			f[evt] = c.F
-			return false
-		}
-	}
-	for i := event.ReactionEventStartDelim; i < event.ReactionEventEndDelim; i++ {
-		c.Events.Subscribe(i, cb(i), fmt.Sprintf("event-%v", i))
-	}
-	i := 0
-
-	for ; i < 1900; i++ {
-		if i == 263 {
-			fmt.Println("hi")
-		}
-		advanceCoreFrame(c)
-		fmt.Printf("%v: %v\n", i, trg[0].ActiveAuraString())
-	}
-
-}
-
-func TestDendroDecayTry2(t *testing.T) {
-	c, trg := makeCore(1)
-	err := c.Init()
-	if err != nil {
-		t.Errorf("error initializing core: %v", err)
-		t.FailNow()
-	}
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Pyro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 80)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 440)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Hydro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 453)
-
-	f := make(map[event.Event]int)
-	cb := func(evt event.Event) func(args ...interface{}) bool {
-		return func(args ...interface{}) bool {
-			f[evt] = c.F
-			return false
-		}
-	}
-	for i := event.ReactionEventStartDelim; i < event.ReactionEventEndDelim; i++ {
-		c.Events.Subscribe(i, cb(i), fmt.Sprintf("event-%v", i))
-	}
-	i := 0
-
-	for ; i < 1900; i++ {
-		if i == 460 {
-			fmt.Println("hi")
-		}
-		advanceCoreFrame(c)
-		fmt.Printf("%v: %v\n", i, trg[0].ActiveAuraString())
-	}
-
-}
-
-func TestQuickenBurningDecay(t *testing.T) {
-	c, trg := makeCore(1)
-	err := c.Init()
-	if err != nil {
-		t.Errorf("error initializing core: %v", err)
-		t.FailNow()
-	}
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Electro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 61)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Dendro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 128)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Pyro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 188)
-	c.QueueAttackEvent(&combat.AttackEvent{
-		Info: combat.AttackInfo{
-			Element:    attributes.Hydro,
-			Durability: 25,
-		},
-		Pattern: combat.NewDefSingleTarget(trg[0].Index(), combat.TargettableEnemy),
-	}, 206)
-
-	f := make(map[event.Event]int)
-	cb := func(evt event.Event) func(args ...interface{}) bool {
-		return func(args ...interface{}) bool {
-			f[evt] = c.F
-			return false
-		}
-	}
-	for i := event.ReactionEventStartDelim; i < event.ReactionEventEndDelim; i++ {
-		c.Events.Subscribe(i, cb(i), fmt.Sprintf("event-%v", i))
-	}
-	i := 0
-
-	for ; i < 1900; i++ {
-		if i == 460 {
-			fmt.Println("hi")
-		}
-		advanceCoreFrame(c)
-		fmt.Printf("%v: %v\n", i, trg[0].ActiveAuraString())
-	}
 
 }
diff --git a/internal/tests/reactable/dendrocore_test.go b/internal/tests/reactable/dendrocore_test.go
index 256c3d83f..288ca4ddd 100644
--- a/internal/tests/reactable/dendrocore_test.go
+++ b/internal/tests/reactable/dendrocore_test.go
@@ -34,10 +34,11 @@ func TestModifyDendroCore(t *testing.T) {
 		if g, ok := args[0].(*reactable.DendroCore); ok {
 			log.Println("replacing gadget on dendro core")
 			c.Combat.ReplaceGadget(g.Key(), &fakeCore{
-				Gadget: gadget.New(c, core.Coord{X: 0, Y: 0, R: 0.2}),
+				Gadget: gadget.New(c, core.Coord{X: 0, Y: 0, R: 0.2}, combat.GadgetTypDendroCore),
 			})
 			//prevent blowing up
-			g.OnRemoved = nil
+			g.OnKill = nil
+			g.OnExpiry = nil
 			g.OnCollision = nil
 		}
 		return false
diff --git a/pkg/core/combat/combat_test.go b/pkg/core/combat/combat_test.go
index a6faa8895..8dacb45f5 100644
--- a/pkg/core/combat/combat_test.go
+++ b/pkg/core/combat/combat_test.go
@@ -15,6 +15,9 @@ type (
 	testteam struct{}
 	testtarg struct {
 		typ         TargettableType
+		gadgetTyp   GadgetTyp
+		hdlr        *Handler
+		src         int //source of gadget
 		idx         int
 		key         int
 		shp         Shape
@@ -46,8 +49,10 @@ func (t *testtarg) IsAlive() bool                                   { return t.a
 func (t *testtarg) Attack(*AttackEvent, glog.Event) (float64, bool) { return 0, false }
 func (t *testtarg) ApplyDamage(*AttackEvent, float64)               {}
 func (t *testtarg) Tick()                                           {}
-func (t *testtarg) Kill()                                           { t.alive = false }
+func (t *testtarg) Kill()                                           { t.hdlr.RemoveGadget(t.Key()) }
 func (t *testtarg) CollidableWith(x TargettableType) bool           { return t.collideWith[x] }
+func (t *testtarg) GadgetTyp() GadgetTyp                            { return t.gadgetTyp }
+func (t *testtarg) Src() int                                        { return t.src }
 func (t *testtarg) CollidedWith(x Target) {
 	if t.onCollision != nil {
 		t.onCollision(x)
diff --git a/pkg/core/combat/gadget_test.go b/pkg/core/combat/gadget_test.go
index 5d2d2c8a1..f56f6b306 100644
--- a/pkg/core/combat/gadget_test.go
+++ b/pkg/core/combat/gadget_test.go
@@ -41,6 +41,7 @@ func TestGadgetCollision(t *testing.T) {
 	//make multiple gadgets in the same spot, so we should get gcount * 2 collision total
 	for i := 0; i < gcount; i++ {
 		v := &testtarg{
+			hdlr:        c,
 			typ:         TargettableGadget,
 			shp:         &Circle{0, 0, 0.1},
 			alive:       true,
@@ -62,6 +63,68 @@ func TestGadgetCollision(t *testing.T) {
 
 }
 
+func TestGadgetLimits(t *testing.T) {
+	c := newCombatCtrl()
+	const ecount = 2
+	const gcount = 20
+	//1 player
+	player := &testtarg{
+		typ:   TargettablePlayer,
+		shp:   &Circle{0, 0, 0.2},
+		alive: true,
+		onCollision: func(Target) {
+			log.Printf("collision shouldn't happen with player!!")
+			t.FailNow()
+		},
+	}
+	c.SetPlayer(player)
+	//2 enemies
+	for i := 0; i < ecount; i++ {
+		v := &testtarg{
+			typ:   TargettableEnemy,
+			shp:   &Circle{float64(i) * 0.5, 0, 0.2},
+			alive: true,
+			onCollision: func(Target) {
+				log.Printf("collision shouldn't happen with enemy!!")
+				t.FailNow()
+			},
+		}
+		c.AddEnemy(v)
+	}
+	//gadget should overlap player and first enemy
+	var cw [TargettableTypeCount]bool
+	cw[TargettableEnemy] = true
+	cw[TargettablePlayer] = true
+	count := 0
+	//make multiple gadgets; gadgets should not exceed 2
+	for i := 0; i < gcount; i++ {
+		v := &testtarg{
+			hdlr:        c,
+			typ:         TargettableGadget,
+			gadgetTyp:   GadgetTypTest,
+			shp:         &Circle{0, 0, 0.1},
+			alive:       true,
+			collideWith: cw,
+		}
+		c.AddGadget(v)
+	}
+
+	c.Tick()
+
+	//check how many we got
+	for _, v := range c.gadgets {
+		if v != nil && v.GadgetTyp() == GadgetTypTest {
+			count++
+		}
+	}
+
+	if count > 2 {
+		t.Errorf("Expecting max 2 gadgets, got %v", count)
+
+	}
+
+}
+
 func BenchmarkCollisionCheck(b *testing.B) {
 	c := newCombatCtrl()
 	const ecount = 2
@@ -142,17 +205,16 @@ func TestKillGadgetOnCollision(t *testing.T) {
 	count := 0
 	//make multiple gadgets in the same spot, so we should get gcount * 2 collision total
 	for i := 0; i < gcount; i++ {
-		idx := i
 		v := &testtarg{
 			typ:         TargettableGadget,
 			shp:         &Circle{0, 0, 0.1},
 			alive:       true,
 			collideWith: cw,
-			onCollision: func(t Target) {
-				count++
-				//kill self
-				c.RemoveGadget(idx)
-			},
+		}
+		v.onCollision = func(t Target) {
+			count++
+			//kill self
+			c.RemoveGadget(v.key)
 		}
 		c.AddGadget(v)
 	}
diff --git a/pkg/gcs/ast/lex_test.go b/pkg/gcs/ast/lex_test.go
index 62e25a8ec..2204b2b8c 100644
--- a/pkg/gcs/ast/lex_test.go
+++ b/pkg/gcs/ast/lex_test.go
@@ -38,10 +38,6 @@ func TestBasicToken(t *testing.T) {
 		}
 	}
 	
-	for x = 0; x < 5; x = x + 1 {
-		let i = y(x);
-	}
-
 	-1
 	1
 	-
@@ -130,33 +126,6 @@ func TestBasicToken(t *testing.T) {
 		{Typ: itemRightBrace, Val: "}"},
 		//end while
 		{Typ: itemRightBrace, Val: "}"},
-		//for loop
-		{Typ: keywordFor, Val: "for"},
-		{Typ: itemIdentifier, Val: "x"},
-		{Typ: itemAssign, Val: "="},
-		{Typ: itemNumber, Val: "0"},
-		{Typ: itemTerminateLine, Val: ";"},
-		{Typ: itemIdentifier, Val: "x"},
-		{Typ: OpLessThan, Val: "<"},
-		{Typ: itemNumber, Val: "5"},
-		{Typ: itemTerminateLine, Val: ";"},
-		{Typ: itemIdentifier, Val: "x"},
-		{Typ: itemAssign, Val: "="},
-		{Typ: itemIdentifier, Val: "x"},
-		{Typ: ItemPlus, Val: "+"},
-		{Typ: itemNumber, Val: "1"},
-		{Typ: itemLeftBrace, Val: "{"},
-		//body
-		{Typ: keywordLet, Val: "let"},
-		{Typ: itemIdentifier, Val: "i"},
-		{Typ: itemAssign, Val: "="},
-		{Typ: itemIdentifier, Val: "y"},
-		{Typ: itemLeftParen, Val: "("},
-		{Typ: itemIdentifier, Val: "x"},
-		{Typ: itemRightParen, Val: ")"},
-		{Typ: itemTerminateLine, Val: ";"},
-		//end for
-		{Typ: itemRightBrace, Val: "}"},
 		//misc tests
 		{Typ: itemNumber, Val: "-1"},
 		{Typ: itemNumber, Val: "1"},
diff --git a/pkg/gcs/ast/parse_test.go b/pkg/gcs/ast/parse_test.go
index 672aaf3f8..1970be9e5 100644
--- a/pkg/gcs/ast/parse_test.go
+++ b/pkg/gcs/ast/parse_test.go
@@ -109,9 +109,6 @@ const cfg = `
 			break;
 		}
 	}
-	for x = 0; x < 5; x = x + 1 {
-		let i = y(x);
-	}
 `
 
 func TestCfg(t *testing.T) {