From be80cb17b0935b759aa1914d701ea9dbee63b971 Mon Sep 17 00:00:00 2001 From: Horoli Date: Fri, 22 May 2026 13:27:19 +0900 Subject: [PATCH] Add kill heal effect animation --- public/assets/effects/heal/Heal_Effect.png | Bin 0 -> 933 bytes src/constants.js | 4 +++ src/game/combat.js | 39 ++++++++++++++++++++- src/game/fighterAssets.js | 36 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 public/assets/effects/heal/Heal_Effect.png diff --git a/public/assets/effects/heal/Heal_Effect.png b/public/assets/effects/heal/Heal_Effect.png new file mode 100644 index 0000000000000000000000000000000000000000..9a240c5fbbb17a56556522f2836592e93f550245 GIT binary patch literal 933 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCr*NG}t*|GKq|4B165cQ3sA?w(pF!-R=X7=ilN zL}X;Tcs|{#S8Lv5?;Y>$C3f+lZ;Rf!s4KO5%XVk?{rAbjmAVr@QBEZHE85W*?_X4Rxy&mNjR#_Dt*P z_*^XSV7&gz!o^zC`E=LcI;Aw>$eliS_fWM$&GsiwJz3Wr^lHDaudB}RpY47B=CiOB z?%Dp=->-iL^w!tk+V5NUZoKf-?sedrV|OO5zIfH>jKL@F%;MCzq)=mK&cwD!*DID? zy7K&M@{YRtPFE$v+`M+NjjOyAyyTzGE^~VP-fdfEX7RM3P$t7J-TM(vGp}7${j(+M z)|~}CS2te^6w6ll36%aI)~?w$Z?C`9CwgnDOTX>3mIlWD zvwKYI`95r%b9?F0#~bJ5s>tLe34uON~snS*5}Lx8_&nyUWimFQ~GPv-zmeT`?uq w_oK$QL+@tJekz+kX&O{75osHT5(nFVOf46iG-Oi_@% literal 0 HcmV?d00001 diff --git a/src/constants.js b/src/constants.js index ab9d85b..63a2764 100644 --- a/src/constants.js +++ b/src/constants.js @@ -33,6 +33,10 @@ export const FIGHTER_HITBOX_OFFSET_Y = 40; export const FIGHTER_MAX_HP = 100; // 적 처치 시 현재 체력 기준으로 회복되는 비율입니다. export const KILL_HEALTH_RECOVERY_RATIO = 0.3; +// 처치 회복 이펙트 스프라이트시트의 프레임 수입니다. +export const KILL_HEAL_EFFECT_FRAMES = 4; +// 처치 회복 이펙트 애니메이션의 초당 프레임 수입니다. +export const KILL_HEAL_EFFECT_FRAME_RATE = 12; // 적 처치 시 크기, 공격속도, 이동속도에 누적 적용되는 배율입니다. export const KILL_GROWTH_MULTIPLIER = 1.25; // 처치 성장 연출 tween 지속 시간(ms)입니다. diff --git a/src/game/combat.js b/src/game/combat.js index e88eda3..916a6d6 100644 --- a/src/game/combat.js +++ b/src/game/combat.js @@ -33,6 +33,8 @@ import { fighterAttackEffectAnimationKey, fighterAttackEffectKey, fighterProjectileKey, + healEffectAnimationKey, + healEffectKey, } from "./fighterAssets.js"; export function updateFighter(scene, fighter, time, onWinner) { @@ -357,12 +359,18 @@ function applyKillReward(winner) { winner.killCount = (winner.killCount ?? 0) + 1; const rewardMultiplier = KILL_GROWTH_MULTIPLIER ** winner.killCount; + const previousHp = winner.hp; + const nextHp = recoveredHealth(winner); winner.killRewardMultiplier = rewardMultiplier; - winner.hp = recoveredHealth(winner); + winner.hp = nextHp; const nextScaleX = (winner.baseScaleX ?? FIGHTER_SCALE) * rewardMultiplier; const nextScaleY = (winner.baseScaleY ?? FIGHTER_SCALE) * rewardMultiplier; + if (nextHp > previousHp) { + spawnKillHealEffect(winner, Math.max(Math.abs(nextScaleX), Math.abs(nextScaleY))); + } + winner.scene.tweens.add({ targets: winner, scaleX: nextScaleX, @@ -379,6 +387,35 @@ function recoveredHealth(fighter) { return Math.min(maxHp, fighter.hp + recovery); } +function spawnKillHealEffect(fighter, effectScale) { + const scene = fighter.scene; + const effect = scene.add + .sprite(fighter.x, fighter.y, healEffectKey()) + .setDepth(fighter.depth + 1) + .setScale(effectScale); + + const syncEffectPosition = () => { + if (!effect.active || !fighter.active) { + return; + } + + effect.setPosition(fighter.x, fighter.y); + effect.setDepth(fighter.depth + 1); + }; + + effect.cleanup = () => { + scene.events.off(Phaser.Scenes.Events.UPDATE, syncEffectPosition); + }; + + scene.events.on(Phaser.Scenes.Events.UPDATE, syncEffectPosition); + effect.play(healEffectAnimationKey()); + trackCombatObject(scene, effect); + + effect.once(Phaser.Animations.Events.ANIMATION_COMPLETE, () => { + disposeCombatObject(scene, effect); + }); +} + function findNearestEnemy(fighters, fighter) { let nearestEnemy; let nearestDistance = Number.POSITIVE_INFINITY; diff --git a/src/game/fighterAssets.js b/src/game/fighterAssets.js index c62e5b1..42be910 100644 --- a/src/game/fighterAssets.js +++ b/src/game/fighterAssets.js @@ -2,6 +2,8 @@ import { FIGHTER_ANIMATION_OPTIONS, FIGHTER_FRAME_HEIGHT, FIGHTER_FRAME_WIDTH, + KILL_HEAL_EFFECT_FRAME_RATE, + KILL_HEAL_EFFECT_FRAMES, SELECTED_FIGHTER_OUTLINE_ALPHA, SELECTED_FIGHTER_OUTLINE_BLUE, SELECTED_FIGHTER_OUTLINE_GAP, @@ -11,6 +13,9 @@ import { } from "../constants.js"; const SOURCE_ALPHA_THRESHOLD = 8; +const HEAL_EFFECT_PATH = "assets/effects/heal/Heal_Effect.png"; +const HEAL_EFFECT_KEY = "kill-heal-effect"; +const HEAL_EFFECT_ANIMATION_KEY = `${HEAL_EFFECT_KEY}-anim`; export function fighterSheetKey(skin, action) { return `${skin.key}-${action}`; @@ -40,7 +45,20 @@ export function fighterProjectileKey(skin) { return `${skin.key}-projectile`; } +export function healEffectKey() { + return HEAL_EFFECT_KEY; +} + +export function healEffectAnimationKey() { + return HEAL_EFFECT_ANIMATION_KEY; +} + export function preloadFighterSheets(scene, skins) { + scene.load.spritesheet(healEffectKey(), HEAL_EFFECT_PATH, { + frameWidth: FIGHTER_FRAME_WIDTH, + frameHeight: FIGHTER_FRAME_HEIGHT, + }); + skins.forEach((skin) => { Object.entries(skin.animations).forEach(([action, animation]) => { scene.load.spritesheet( @@ -78,6 +96,8 @@ export function createFighterAnimations(scene, skins) { createAttackEffectAnimation(scene, skin); }); + + createHealEffectAnimation(scene); } function preloadCombatAssets(scene, skin) { @@ -121,6 +141,22 @@ function createAttackEffectAnimation(scene, skin) { }); } +function createHealEffectAnimation(scene) { + if (scene.anims.exists(healEffectAnimationKey())) { + return; + } + + scene.anims.create({ + key: healEffectAnimationKey(), + frames: scene.anims.generateFrameNumbers(healEffectKey(), { + start: 0, + end: KILL_HEAL_EFFECT_FRAMES - 1, + }), + frameRate: KILL_HEAL_EFFECT_FRAME_RATE, + repeat: 0, + }); +} + function createFighterOutlineSheet(scene, skin, action, frameCount) { const key = fighterOutlineSheetKey(skin, action);