diff --git a/public/assets/effects/heal/Heal_Effect.png b/public/assets/effects/heal/Heal_Effect.png new file mode 100644 index 0000000..9a240c5 Binary files /dev/null and b/public/assets/effects/heal/Heal_Effect.png differ 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);