diff --git a/play/assets/tmpGame/script/utils.js b/play/assets/tmpGame/script/utils.js
index 83ccee9986bcb703dc8ca5a0fea2c1462f6d6f54..16e03db6950cf4d70a0dce5145f56c7ecbb4b7e5 100644
--- a/play/assets/tmpGame/script/utils.js
+++ b/play/assets/tmpGame/script/utils.js
@@ -74,13 +74,31 @@ export async function asyncTweenTo(node, duration, obj, ease = undefined) {
     });
 }
 
-export async function asyncPlayDragonBoneAnimation(node, animationName, time = 1) {
+export async function asyncTweenBy(node, duration, obj, ease = undefined) {
+    return new Promise((resolve, reject) => {
+        cc.tween(node)
+            .by(duration, obj, ease)
+            .call(() => {
+                resolve();
+            })
+            .start();
+    });
+}
+
+export async function asyncPlayDragonBoneAnimation(node, animationName, time = 1, onFrameEvent) {
     return new Promise((resolve, reject) => {
         node.getComponent(dragonBones.ArmatureDisplay)
             .once(dragonBones.EventObject.COMPLETE, () => {
-                console.log('COMPLETE');
                 resolve();
             });
+
+        node.getComponent(dragonBones.ArmatureDisplay)
+            .on(dragonBones.EventObject.FRAME_EVENT, ({ name }) => {
+                if (onFrameEvent && typeof (onFrameEvent) == 'function') {
+                    onFrameEvent(name);
+                }
+            });
+
         node.getComponent(dragonBones.ArmatureDisplay)
             .playAnimation(animationName, time);
     });
@@ -91,6 +109,70 @@ export async function asyncPlayEffectByUrl(url, loop = false) {
         cc.assetManager.loadRemote(url, (err, clip) => {
             console.log(clip);
             cc.audioEngine.playEffect(clip, loop);
+            resolve();
         });
     });
+}
+
+export async function jelly(node) {
+    return new Promise((resolve, reject) => {
+        cc.tween(node)
+            .to(0.1, { scaleX: 0.9, scaleY: 1.1 })
+            .to(0.1, { scaleX: 1.1, scaleY: 0.9 })
+            .to(0.1, { scaleX: 1, scaleY: 1 })
+            .call(resolve)
+            .start();
+    });
+}
+
+export async function asyncDelay(time) {
+    return new Promise((resolve, reject) => {
+        setTimeout(() => {
+            resolve();
+        }, time * 1000);
+    })
+}
+
+
+
+export async function showFireworks(baseNode, nodeList, pos = cc.v2(0, 0), side = cc.v2(0, 100), range = 50, number = 100) {
+    for (let i = 0; i < number; i++) {
+        const node = cc.instantiate(nodeList[RandomInt(nodeList.length)]);
+        node.parent = baseNode;
+        node.active = true;
+        node.x = pos.x;
+        node.y = pos.y;
+        node.angle = 360 * Math.random();
+        node.scale = 0.5;
+
+        const rate = Math.random();
+        const angle = Math.PI * (Math.random() * 2 - 1);
+
+        cc.tween(node)
+            .by(0.3, {
+                x: side.x * rate + Math.cos(angle) * range * rate,
+                y: side.y * rate + Math.sin(angle) * range * rate
+            })
+            .call(async () => {
+                for (let i = 0; i < 8; i++) {
+                    await rabbonFall(node);
+                }
+                cc.tween(node)
+                    .to(0.3, { opacity: 0 })
+                    .call(() => {
+                        node.active = false;
+                        node.parent = null;
+                        node = null;
+                    })
+                    .start();
+            })
+            .by(8, { y: -2000 })
+            .start();
+    }
+}
+
+async function rabbonFall(node) {
+    const time = 1 + Math.random();
+    const offsetX = RandomInt(-200, 200) * time;
+    await asyncTweenBy(node, time, { x: offsetX, angle: offsetX * 60 / 200 });
 }
\ No newline at end of file