Fix : 보스 레이드 시스템 업데이트

This commit is contained in:
user 2026-03-30 21:51:29 +09:00
parent 4aa22756e5
commit 71b9521372
62 changed files with 1658 additions and 40 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,21 +2,35 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: cfxr fire circle plain hdr ab
m_Shader: {fileID: 4800000, guid: e198026df304af84a85675be636192ea, type: 3}
m_ShaderKeywords: _ALPHATEST_ON _FADING_ON _CFXR_HDR_BOOST _CFXR_SINGLE_CHANNEL
_CFXR_EDGE_FADING _CFXR_UV_DISTORTION
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
- _ALPHATEST_ON
- _CFXR_EDGE_FADING
- _CFXR_HDR_BOOST
- _CFXR_SINGLE_CHANNEL
- _CFXR_UV_DISTORTION
- _FADING_ON
m_InvalidKeywords:
- _
- _CFXR_DITHERED_SHADOWS_OFF
- _CFXR_OVERLAYBLEND_RGBA
- _CFXR_OVERLAYTEX_OFF
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -52,6 +66,7 @@ Material:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BacklightTransmittance: 1
- _BlendingType: 0
@ -64,6 +79,7 @@ Material:
- _DirectLightingRamp: 1
- _DissolveSmooth: 0.1
- _Distort: 0.3
- _DoubleDissolve: 0
- _DstBlend: 10
- _EdgeFadePow: 1
- _FadeAlongU: 1
@ -85,6 +101,7 @@ Material:
- _UseAlphaClip: 1
- _UseBackLighting: 0
- _UseDissolve: 0
- _UseDissolveOffsetUV: 0
- _UseEF: 1
- _UseEmission: 0
- _UseFB: 0
@ -99,7 +116,10 @@ Material:
- _UseUVDistortion: 1
- _ZWrite: 0
m_Colors:
- _DissolveScroll: {r: 0, g: 0, b: 0, a: 0}
- _DistortScrolling: {r: 0, g: -1, b: 1, a: 1}
- _OverlayTex_Scroll: {r: 0.1, g: 0.1, b: 1, a: 1}
- _ShadowColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticlesFadeDistance: {r: 0, g: 1, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -2,21 +2,35 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: cfxr fire stepped fade hdr ab
m_Shader: {fileID: 4800000, guid: e198026df304af84a85675be636192ea, type: 3}
m_ShaderKeywords: _ALPHATEST_ON _CFXR_EDGE_FADING _CFXR_HDR_BOOST _CFXR_SINGLE_CHANNEL
_CFXR_UV_DISTORTION _FADING_ON
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
- _ALPHATEST_ON
- _CFXR_EDGE_FADING
- _CFXR_HDR_BOOST
- _CFXR_SINGLE_CHANNEL
- _CFXR_UV_DISTORTION
- _FADING_ON
m_InvalidKeywords:
- _
- _CFXR_DITHERED_SHADOWS_OFF
- _CFXR_OVERLAYBLEND_RGBA
- _CFXR_OVERLAYTEX_OFF
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -52,6 +66,7 @@ Material:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BacklightTransmittance: 1
- _BlendingType: 0
@ -64,6 +79,7 @@ Material:
- _DirectLightingRamp: 1
- _DissolveSmooth: 0.1
- _Distort: 0.75
- _DoubleDissolve: 0
- _DstBlend: 10
- _EdgeFadePow: 2
- _FadeAlongU: 1
@ -85,6 +101,7 @@ Material:
- _UseAlphaClip: 1
- _UseBackLighting: 0
- _UseDissolve: 0
- _UseDissolveOffsetUV: 0
- _UseEF: 1
- _UseEmission: 0
- _UseFB: 0
@ -99,7 +116,10 @@ Material:
- _UseUVDistortion: 1
- _ZWrite: 0
m_Colors:
- _DissolveScroll: {r: 0, g: 0, b: 0, a: 0}
- _DistortScrolling: {r: 0, g: -1.2, b: 1, a: 1}
- _OverlayTex_Scroll: {r: 0.1, g: 0.1, b: 1, a: 1}
- _ShadowColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticlesFadeDistance: {r: 0, g: 1, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -2,21 +2,32 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: cfxr magic star hdr ab
m_Shader: {fileID: 4800000, guid: e198026df304af84a85675be636192ea, type: 3}
m_ShaderKeywords: _CFXR_DITHERED_SHADOWS_ON _CFXR_HDR_BOOST _CFXR_SINGLE_CHANNEL
_FADING_ON
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
- _CFXR_DITHERED_SHADOWS_ON
- _CFXR_HDR_BOOST
- _CFXR_SINGLE_CHANNEL
- _FADING_ON
m_InvalidKeywords:
- _
- _CFXR_OVERLAYBLEND_RGBA
- _CFXR_OVERLAYTEX_OFF
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -52,6 +63,7 @@ Material:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BacklightTransmittance: 1
- _BlendingType: 0
@ -64,6 +76,7 @@ Material:
- _DirectLightingRamp: 1
- _DissolveSmooth: 0.1
- _Distort: 0.1
- _DoubleDissolve: 0
- _DstBlend: 10
- _EdgeFadePow: 1
- _FadeAlongU: 0
@ -84,6 +97,7 @@ Material:
- _UseAlphaClip: 0
- _UseBackLighting: 0
- _UseDissolve: 0
- _UseDissolveOffsetUV: 0
- _UseEF: 0
- _UseEmission: 0
- _UseFB: 0
@ -99,7 +113,10 @@ Material:
- _ZWrite: 0
m_Colors:
- _Color: {r: 3.8792846, g: 3.8792846, b: 3.8792846, a: 0}
- _DissolveScroll: {r: 0, g: 0, b: 0, a: 0}
- _DistortScrolling: {r: 0, g: 0, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 0}
- _OverlayTex_Scroll: {r: 0.1, g: 0.1, b: 1, a: 1}
- _ShadowColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -2,21 +2,32 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: cfxr stretch rectangle ray hdr ab
m_Shader: {fileID: 4800000, guid: e198026df304af84a85675be636192ea, type: 3}
m_ShaderKeywords: _ALPHABLEND_ON _CFXR_DITHERED_SHADOWS_ON _CFXR_HDR_BOOST _CFXR_SINGLE_CHANNEL
_FADING_ON
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
- _CFXR_HDR_BOOST
- _CFXR_SINGLE_CHANNEL
- _FADING_ON
m_InvalidKeywords:
- _
- _CFXR_DITHERED_SHADOWS_OFF
- _CFXR_OVERLAYBLEND_RGBA
- _CFXR_OVERLAYTEX_OFF
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -52,6 +63,7 @@ Material:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AmbientColor: 0.5
- _AmbientIntensity: 1
@ -66,6 +78,7 @@ Material:
- _DirectLightingRamp: 1
- _DissolveSmooth: 0.1
- _Distort: 0.1
- _DoubleDissolve: 0
- _DstBlend: 10
- _EdgeFadePow: 1
- _FadeAlongU: 0
@ -87,6 +100,7 @@ Material:
- _UseAlphaClip: 0
- _UseBackLighting: 0
- _UseDissolve: 0
- _UseDissolveOffsetUV: 0
- _UseEF: 0
- _UseEmission: 0
- _UseFB: 0
@ -102,7 +116,10 @@ Material:
- _ZWrite: 0
m_Colors:
- _Color: {r: 2, g: 2, b: 2, a: 0}
- _DissolveScroll: {r: 0, g: 0, b: 0, a: 0}
- _DistortScrolling: {r: 0, g: 0, b: 1, a: 1}
- _OverlayTex_Scroll: {r: 0.1, g: 0.1, b: 1, a: 1}
- _ShadowColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticlesFadeDistance: {r: 0, g: 1, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

Binary file not shown.

View File

@ -31,6 +31,16 @@ namespace Streamingle.Contents.BossRaid
public const string SFX_DAMAGE_POPUP = "sfx_damage_popup";
public const string SFX_PHASE_TRANSITION = "sfx_phase_transition";
// 보스 공격
public const string SFX_BOSS_BREATH = "sfx_boss_breath";
public const string SFX_BOSS_SLAM = "sfx_boss_slam";
public const string SFX_BOSS_CHARGE = "sfx_boss_charge";
// 유저 피격
public const string SFX_PLAYER_HIT = "sfx_player_hit";
public const string SFX_PLAYER_DEATH = "sfx_player_death";
public const string SFX_GAME_OVER = "sfx_game_over";
// BGM 키
public const string BGM_BATTLE_NORMAL = "bgm_battle_normal";
public const string BGM_BATTLE_RAGE = "bgm_battle_rage";
@ -182,6 +192,8 @@ namespace Streamingle.Contents.BossRaid
SFX_HIT_NORMAL, SFX_HIT_CRITICAL, SFX_HIT_MISS,
SFX_BOSS_APPEAR, SFX_BOSS_RAGE, SFX_BOSS_DEATH,
SFX_VICTORY, SFX_DAMAGE_POPUP, SFX_PHASE_TRANSITION,
SFX_BOSS_BREATH, SFX_BOSS_SLAM, SFX_BOSS_CHARGE,
SFX_PLAYER_HIT, SFX_PLAYER_DEATH, SFX_GAME_OVER,
BGM_BATTLE_NORMAL, BGM_BATTLE_RAGE, BGM_VICTORY,
};

View File

@ -0,0 +1,236 @@
using System;
using System.Collections;
using UnityEngine;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 보스 공격 연출을 관리합니다.
/// 웹/인스펙터에서 수동 트리거합니다.
/// </summary>
public class BossAttack : MonoBehaviour
{
#region Enums
public enum AttackType
{
Breath,
Slam,
RageBurst
}
#endregion
#region Events
/// <summary>공격이 유저에게 데미지를 줄 때 (attackType, damage)</summary>
public event Action<AttackType, int> OnAttackHit;
#endregion
#region Fields
[Header("브레스")]
[SerializeField] private int breathDamage = 15;
[SerializeField] private float breathChargeTime = 0.5f;
[SerializeField] private float breathDuration = 2f;
[SerializeField] private int breathTickCount = 4;
[Header("내려찍기")]
[SerializeField] private int slamDamage = 25;
[SerializeField] private float slamChargeTime = 0.8f;
[Header("분노 폭발")]
[SerializeField] private int rageDamage = 30;
[Header("파티클 프리팹 (비어있으면 Resources/Particles에서 자동 로드)")]
public GameObject breathFXPrefab;
public GameObject slamImpactFXPrefab;
public GameObject rageBurstFXPrefab;
public GameObject playerHitFXPrefab;
private BossRaidAudio _audio;
private ScreenEffect _screenEffect;
private BossVisualEffect _bossVisual;
private Transform _bossTransform;
private ParticlePool _particlePool;
private Coroutine _attackCoroutine;
private bool _isAttacking;
#endregion
#region Properties
public bool IsAttacking => _isAttacking;
public int BreathDamage { get => breathDamage; set => breathDamage = value; }
public int SlamDamage { get => slamDamage; set => slamDamage = value; }
public int RageDamage { get => rageDamage; set => rageDamage = value; }
#endregion
#region Public Methods
public void Initialize(Transform bossTransform, BossRaidAudio audio, ScreenEffect screenEffect, BossVisualEffect bossVisual, ParticlePool particlePool)
{
_bossTransform = bossTransform;
_audio = audio;
_screenEffect = screenEffect;
_bossVisual = bossVisual;
_particlePool = particlePool;
// 비어있는 파티클은 Resources에서 자동 로드
if (breathFXPrefab == null) breathFXPrefab = Resources.Load<GameObject>("Particles/BreathFX");
if (slamImpactFXPrefab == null) slamImpactFXPrefab = Resources.Load<GameObject>("Particles/SlamImpactFX");
if (rageBurstFXPrefab == null) rageBurstFXPrefab = Resources.Load<GameObject>("Particles/RageBurstFX");
if (playerHitFXPrefab == null) playerHitFXPrefab = Resources.Load<GameObject>("Particles/PlayerHitFX");
int loaded = (breathFXPrefab != null ? 1 : 0) + (slamImpactFXPrefab != null ? 1 : 0)
+ (rageBurstFXPrefab != null ? 1 : 0) + (playerHitFXPrefab != null ? 1 : 0);
Debug.Log($"[BossAttack] 파티클 {loaded}/4개 로드 완료");
// 파티클 웜업
if (_particlePool != null)
{
_particlePool.Warmup(breathFXPrefab, 1);
_particlePool.Warmup(slamImpactFXPrefab, 1);
_particlePool.Warmup(rageBurstFXPrefab, 1);
_particlePool.Warmup(playerHitFXPrefab, 2);
}
}
/// <summary>
/// 브레스 공격 실행
/// </summary>
public void ExecuteBreath()
{
if (_isAttacking) return;
if (_attackCoroutine != null) StopCoroutine(_attackCoroutine);
_attackCoroutine = StartCoroutine(BreathCoroutine());
}
/// <summary>
/// 내려찍기 공격 실행
/// </summary>
public void ExecuteSlam()
{
if (_isAttacking) return;
if (_attackCoroutine != null) StopCoroutine(_attackCoroutine);
_attackCoroutine = StartCoroutine(SlamCoroutine());
}
/// <summary>
/// 분노 폭발 실행
/// </summary>
public void ExecuteRageBurst()
{
if (_isAttacking) return;
if (_attackCoroutine != null) StopCoroutine(_attackCoroutine);
_attackCoroutine = StartCoroutine(RageBurstCoroutine());
}
public void CancelAttack()
{
if (_attackCoroutine != null)
{
StopCoroutine(_attackCoroutine);
_attackCoroutine = null;
}
_isAttacking = false;
}
#endregion
#region Private Methods
private Vector3 BossPos => _bossTransform != null ? _bossTransform.position : Vector3.zero;
private IEnumerator BreathCoroutine()
{
_isAttacking = true;
// 차징
_audio?.PlaySFX(BossRaidAudio.SFX_BOSS_CHARGE);
yield return new WaitForSeconds(breathChargeTime);
// 브레스 발사 (보스 forward 방향, 프리팹 회전 유지)
_audio?.PlaySFX(BossRaidAudio.SFX_BOSS_BREATH);
Quaternion bossRot = _bossTransform != null ? _bossTransform.rotation : Quaternion.identity;
Vector3 forward = _bossTransform != null ? _bossTransform.forward : Vector3.forward;
Vector3 spawnPos = BossPos + Vector3.up * 1.5f + forward * 1f;
_particlePool?.Spawn(breathFXPrefab, spawnPos, bossRot);
// 틱 데미지
float tickInterval = breathDuration / breathTickCount;
for (int i = 0; i < breathTickCount; i++)
{
OnAttackHit?.Invoke(AttackType.Breath, breathDamage / breathTickCount);
_screenEffect?.PlayShake(0.15f, 0.1f);
yield return new WaitForSeconds(tickInterval);
}
_isAttacking = false;
_attackCoroutine = null;
}
private IEnumerator SlamCoroutine()
{
_isAttacking = true;
// 차징 (보스 위로 솟기)
_audio?.PlaySFX(BossRaidAudio.SFX_BOSS_CHARGE);
Vector3 originalPos = BossPos;
if (_bossTransform != null)
{
float elapsed = 0f;
while (elapsed < slamChargeTime)
{
elapsed += Time.deltaTime;
float t = elapsed / slamChargeTime;
float yOffset = Mathf.Sin(t * Mathf.PI) * 1.5f;
_bossTransform.position = originalPos + Vector3.up * yOffset;
yield return null;
}
_bossTransform.position = originalPos;
}
// 내려찍기
_audio?.PlaySFX(BossRaidAudio.SFX_BOSS_SLAM);
Vector3 impactPos = BossPos;
impactPos.y = 0f;
_particlePool?.Spawn(slamImpactFXPrefab, impactPos);
_screenEffect?.PlayShake(0.6f, 0.3f);
_screenEffect?.PlayFlash(new Color(1f, 0.8f, 0.3f), 0.15f);
OnAttackHit?.Invoke(AttackType.Slam, slamDamage);
yield return new WaitForSeconds(0.3f);
_isAttacking = false;
_attackCoroutine = null;
}
private IEnumerator RageBurstCoroutine()
{
_isAttacking = true;
// 차징 (강한 흔들림)
_audio?.PlaySFX(BossRaidAudio.SFX_BOSS_RAGE);
_screenEffect?.PlayShake(0.3f, 1f);
yield return new WaitForSeconds(1f);
// 폭발
_particlePool?.Spawn(rageBurstFXPrefab, BossPos);
_screenEffect?.PlayFlash(new Color(1f, 0.2f, 0.1f), 0.3f);
_screenEffect?.PlayShake(0.8f, 0.5f);
OnAttackHit?.Invoke(AttackType.RageBurst, rageDamage);
yield return new WaitForSeconds(0.5f);
_isAttacking = false;
_attackCoroutine = null;
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 71e07d6d0d75ea8439d98479b164ce19

View File

@ -36,6 +36,12 @@ namespace Streamingle.Contents.BossRaid
[SerializeField] private BossData bossData;
[SerializeField] private Transform bossSpawnPoint;
[Header("유저")]
[SerializeField] private PlayerController playerController;
[SerializeField]
[Tooltip("유저 HP (0이면 BossData 기본값 사용)")]
private int playerMaxHP = 100;
[Header("설정")]
[SerializeField]
[Tooltip("보스 등장 연출 시간 (초)")]
@ -62,6 +68,10 @@ namespace Streamingle.Contents.BossRaid
private BossRaidAudio _audio;
private PhaseTransition _phaseTransition;
private ParticlePool _particlePool;
private PlayerHPBar _playerHPBar;
private PlayerHitEffect _playerHitEffect;
private GameOverScreen _gameOverScreen;
private BossAttack _bossAttack;
private RaidState _currentState = RaidState.Idle;
private GameObject _bossInstance;
@ -82,6 +92,8 @@ namespace Streamingle.Contents.BossRaid
public RaidState CurrentState => _currentState;
public BossController Boss => _bossController;
public BossRaidSafety Safety => _safety;
public PlayerController Player => playerController;
public BossAttack Attack => _bossAttack;
#endregion
@ -102,6 +114,10 @@ namespace Streamingle.Contents.BossRaid
_victoryScreen = GetComponentInChildren<VictoryScreen>();
_audio = GetComponentInChildren<BossRaidAudio>();
_phaseTransition = GetComponentInChildren<PhaseTransition>();
_playerHPBar = GetComponentInChildren<PlayerHPBar>();
_playerHitEffect = GetComponentInChildren<PlayerHitEffect>();
_gameOverScreen = GetComponentInChildren<GameOverScreen>();
_bossAttack = GetComponentInChildren<BossAttack>();
// 파티클 풀 초기화
_particlePool = GetComponentInChildren<ParticlePool>();
@ -156,6 +172,14 @@ namespace Streamingle.Contents.BossRaid
_hpBar?.Hide();
_comboCounter?.Hide();
_victoryScreen?.Hide();
_playerHPBar?.Hide();
_gameOverScreen?.Hide();
_bossAttack?.CancelAttack();
UnsubscribePlayerEvents();
if (_bossAttack != null)
_bossAttack.OnAttackHit -= HandleBossAttackHit;
if (playerController != null)
playerController.RestoreColor();
if (_phaseAuraInstance != null)
_phaseAuraInstance.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
SetState(RaidState.Idle);
@ -233,6 +257,28 @@ namespace Streamingle.Contents.BossRaid
yield return new WaitForSeconds(appearDuration);
// === 플레이어 초기화 ===
if (playerController != null)
{
playerController.Initialize(playerMaxHP);
playerController.OnDamaged += HandlePlayerDamaged;
playerController.OnDeath += HandlePlayerDeath;
playerController.OnHPChanged += HandlePlayerHPChanged;
if (_playerHPBar != null && playerController.HeadTransform != null)
{
_playerHPBar.Initialize(playerController.HeadTransform, "", playerController.HPBarOffset);
_playerHPBar.Show();
}
}
// === 보스 공격 초기화 ===
if (_bossAttack != null && _bossInstance != null)
{
_bossAttack.Initialize(_bossInstance.transform, _audio, _screenEffect, _bossVisual, _particlePool);
_bossAttack.OnAttackHit += HandleBossAttackHit;
}
// === Battle ===
SetState(RaidState.Battle);
_audio?.PlayBGM(BossRaidAudio.BGM_BATTLE_NORMAL);
@ -241,7 +287,7 @@ namespace Streamingle.Contents.BossRaid
Debug.Log("[BossRaidManager] 전투 시작!");
// Battle은 OnBossDeath 이벤트로 종료됨
// Battle은 OnBossDeath / OnPlayerDeath 이벤트로 종료됨
}
private IEnumerator DefeatSequence()
@ -490,6 +536,83 @@ namespace Streamingle.Contents.BossRaid
#endregion
#region Private Methods - Player Events
private void HandleBossAttackHit(BossAttack.AttackType type, int damage)
{
if (playerController == null || playerController.IsDead) return;
playerController.TakeDamage(damage);
// 유저 피격 파티클 (HitEffectPoint 우선, 없으면 머리)
if (_bossAttack?.playerHitFXPrefab != null && playerController.HitEffectPoint != null)
{
Vector3 pos = playerController.HitEffectPoint.position;
_particlePool?.Spawn(_bossAttack.playerHitFXPrefab, pos);
}
}
private void HandlePlayerDamaged(int damage)
{
_audio?.PlaySFX(BossRaidAudio.SFX_PLAYER_HIT);
_playerHitEffect?.Play(playerController);
_screenEffect?.PlayShake(0.2f, 0.15f);
}
private void HandlePlayerDeath()
{
_audio?.PlaySFX(BossRaidAudio.SFX_PLAYER_DEATH);
_hitDetector.IsActive = false;
_bossAttack?.CancelAttack();
StartCoroutine(GameOverSequence());
}
private void HandlePlayerHPChanged(int currentHP, int maxHP)
{
float ratio = maxHP > 0 ? (float)currentHP / maxHP : 0f;
_playerHPBar?.SetHP(ratio);
}
private void UnsubscribePlayerEvents()
{
if (playerController != null)
{
playerController.OnDamaged -= HandlePlayerDamaged;
playerController.OnDeath -= HandlePlayerDeath;
playerController.OnHPChanged -= HandlePlayerHPChanged;
}
if (_bossAttack != null)
_bossAttack.OnAttackHit -= HandleBossAttackHit;
}
private IEnumerator GameOverSequence()
{
yield return new WaitForSeconds(0.5f);
_audio?.StopBGM();
_audio?.PlaySFX(BossRaidAudio.SFX_GAME_OVER);
_gameOverScreen?.Show();
Debug.Log("[BossRaidManager] GAME OVER!");
}
#endregion
#region Public Methods - Boss Attack (/Stream Deck )
public void ExecuteBreath() => _bossAttack?.ExecuteBreath();
public void ExecuteSlam() => _bossAttack?.ExecuteSlam();
public void ExecuteRageBurst() => _bossAttack?.ExecuteRageBurst();
public void SetPlayerHP(float ratio) => playerController?.SetHPRatio(ratio);
public void HealPlayer() => playerController?.FullHeal();
public void SetPlayerGodMode(bool on)
{
if (playerController != null) playerController.GodMode = on;
}
#endregion
#region Private Methods - Utility
private void SetState(RaidState newState)
@ -548,6 +671,18 @@ namespace Streamingle.Contents.BossRaid
{
if (bossData == null || _particlePool == null) return;
// 비어있는 파티클은 Resources에서 자동 로드
if (bossData.hitParticlePrefab == null)
bossData.hitParticlePrefab = Resources.Load<GameObject>("Particles/HitSpark");
if (bossData.criticalParticlePrefab == null)
bossData.criticalParticlePrefab = Resources.Load<GameObject>("Particles/CriticalHit");
if (bossData.deathParticlePrefab == null)
bossData.deathParticlePrefab = Resources.Load<GameObject>("Particles/BossExplosion");
if (bossData.victoryParticlePrefab == null)
bossData.victoryParticlePrefab = Resources.Load<GameObject>("Particles/VictoryConfetti");
if (bossData.phaseAuraParticlePrefab == null)
bossData.phaseAuraParticlePrefab = Resources.Load<GameObject>("Particles/PhaseAura");
// 히트 파티클은 자주 쓰이므로 넉넉하게
_particlePool.Warmup(bossData.hitParticlePrefab, 5);
_particlePool.Warmup(bossData.criticalParticlePrefab, 3);

View File

@ -56,7 +56,7 @@ namespace Streamingle.Contents.BossRaid
/// 풀에서 파티클을 가져와 위치에 배치하고 재생합니다.
/// 재생 완료 후 자동으로 풀에 반환됩니다.
/// </summary>
public GameObject Spawn(GameObject prefab, Vector3 position)
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion? rotation = null)
{
if (prefab == null) return null;
@ -69,7 +69,6 @@ namespace Streamingle.Contents.BossRaid
if (pool.Count > 0)
{
go = pool.Dequeue();
// 풀에서 꺼낸 것이 파괴되었으면 새로 생성
if (go == null)
go = CreateInstance(prefab);
}
@ -79,7 +78,7 @@ namespace Streamingle.Contents.BossRaid
}
go.transform.position = position;
go.transform.rotation = Quaternion.identity;
go.transform.rotation = rotation ?? Quaternion.identity;
go.SetActive(true);
var ps = go.GetComponent<ParticleSystem>();

View File

@ -0,0 +1,195 @@
using System;
using UnityEngine;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 유저(아바타)의 HP, 피격, 무적, 사망을 관리합니다.
/// </summary>
public class PlayerController : MonoBehaviour
{
#region Events
public event Action<int, int> OnHPChanged;
public event Action<int> OnDamaged;
public event Action OnDeath;
#endregion
#region Fields
[Header("데이터")]
[SerializeField]
[Tooltip("비어있으면 Resources에서 자동 로드")]
private PlayerData playerData;
[Header("참조")]
[SerializeField]
[Tooltip("아바타의 NiloToonPerCharacterRenderController")]
private MonoBehaviour niloToonController;
[SerializeField]
[Tooltip("HP바를 띄울 머리 Transform")]
private Transform headTransform;
[SerializeField]
[Tooltip("HP바 위치 오프셋 (머리 기준)")]
private Vector3 hpBarOffset = new Vector3(0f, 0.3f, 0f);
[SerializeField]
[Tooltip("피격 이펙트 위치 Transform (비어있으면 머리 사용)")]
private Transform hitEffectPoint;
private int _currentHP;
private bool _isDead;
private bool _isInvincible;
private float _invincibleTimer;
private bool _godMode;
// NiloToon 색상 캐시
private Color _originalTintColor = Color.white;
#endregion
#region Properties
public int CurrentHP => _currentHP;
public int MaxHP => playerData != null ? playerData.maxHP : 100;
public float HPRatio => MaxHP > 0 ? (float)_currentHP / MaxHP : 0f;
public PlayerData Data => playerData;
public bool IsDead => _isDead;
public bool IsInvincible => _isInvincible || _godMode;
public Transform HeadTransform => headTransform;
public Vector3 HPBarOffset => hpBarOffset;
public Transform HitEffectPoint => hitEffectPoint != null ? hitEffectPoint : headTransform;
public bool GodMode
{
get => _godMode;
set
{
_godMode = value;
Debug.Log($"[PlayerController] 무적 모드: {(_godMode ? "ON" : "OFF")}");
}
}
#endregion
#region Unity Messages
private void Update()
{
if (_isInvincible && !_godMode)
{
_invincibleTimer -= Time.deltaTime;
if (_invincibleTimer <= 0f)
_isInvincible = false;
}
}
private float InvincibilityDuration => playerData != null ? playerData.invincibilityDuration : 1.5f;
#endregion
#region Public Methods
public void Initialize(int hp = -1)
{
// PlayerData 자동 로드
if (playerData == null)
playerData = Resources.Load<PlayerData>("PlayerData");
if (hp > 0 && playerData != null)
playerData.maxHP = hp;
_currentHP = MaxHP;
_isDead = false;
_isInvincible = false;
_godMode = false;
// NiloToon 원본 색상 저장
if (niloToonController != null)
{
var field = niloToonController.GetType().GetField("perCharacterTintColor",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (field != null)
_originalTintColor = (Color)field.GetValue(niloToonController);
}
OnHPChanged?.Invoke(_currentHP, MaxHP);
Debug.Log($"[PlayerController] 초기화 완료. HP: {_currentHP}/{MaxHP}");
}
public void TakeDamage(int damage)
{
if (_isDead || IsInvincible) return;
_currentHP = Mathf.Max(0, _currentHP - damage);
OnDamaged?.Invoke(damage);
OnHPChanged?.Invoke(_currentHP, MaxHP);
// 무적 시작
_isInvincible = true;
_invincibleTimer = InvincibilityDuration;
Debug.Log($"[PlayerController] 피격! -{damage} | HP: {_currentHP}/{MaxHP}");
if (_currentHP <= 0)
{
_isDead = true;
OnDeath?.Invoke();
Debug.Log("[PlayerController] 사망!");
}
}
public void SetHPRatio(float ratio)
{
ratio = Mathf.Clamp01(ratio);
_currentHP = Mathf.RoundToInt(MaxHP * ratio);
_isDead = false;
_isInvincible = false;
OnHPChanged?.Invoke(_currentHP, MaxHP);
}
public void FullHeal()
{
_currentHP = MaxHP;
_isDead = false;
_isInvincible = false;
OnHPChanged?.Invoke(_currentHP, MaxHP);
Debug.Log("[PlayerController] HP 전체 회복");
}
/// <summary>
/// NiloToon 피격 색상 플래시
/// </summary>
public void FlashHitColor(Color color)
{
SetNiloToonTint(color);
}
/// <summary>
/// NiloToon 원래 색상 복원
/// </summary>
public void RestoreColor()
{
SetNiloToonTint(_originalTintColor);
}
#endregion
#region Private Methods
private void SetNiloToonTint(Color color)
{
if (niloToonController == null) return;
var field = niloToonController.GetType().GetField("perCharacterTintColor",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (field != null)
field.SetValue(niloToonController, color);
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2b316dc2527714246985aaf40f4ba95a

View File

@ -0,0 +1,33 @@
using System;
using UnityEngine;
namespace Streamingle.Contents.BossRaid
{
[CreateAssetMenu(fileName = "PlayerData", menuName = "Streamingle/Contents/BossRaid/Player Data")]
public class PlayerData : ScriptableObject
{
[Header("스탯")]
[Min(1)]
public int maxHP = 100;
[Header("무적")]
[Tooltip("피격 후 무적 시간 (초)")]
[Range(0f, 5f)]
public float invincibilityDuration = 1.5f;
[Header("피격 연출")]
[Tooltip("NiloToon 피격 틴트 색상")]
public Color hitTintColor = new Color(1f, 0.3f, 0.3f);
[Tooltip("피격 틴트 지속 시간")]
[Range(0.05f, 1f)]
public float hitTintDuration = 0.3f;
[Tooltip("비네트 색상")]
public Color vignetteColor = new Color(1f, 0f, 0f, 0.4f);
[Tooltip("비네트 지속 시간")]
[Range(0.1f, 1f)]
public float vignetteDuration = 0.4f;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ff44b8d33db660441900bc40df940114

View File

@ -62,7 +62,24 @@ namespace Streamingle.Contents.BossRaid.Editor
webObj.transform.SetParent(root.transform);
webObj.AddComponent<BossRaidWebServer>();
// === 11. 파티클 프리팹 연결 ===
// === 11. 유저 시스템 ===
PlayerData playerData = FindOrCreatePlayerData();
var playerObj = new GameObject("Player");
playerObj.transform.SetParent(root.transform);
playerObj.AddComponent<PlayerHPBar>();
playerObj.AddComponent<PlayerHitEffect>();
playerObj.AddComponent<GameOverScreen>();
Debug.Log("[BossRaid] PlayerController는 아바타에 직접 부착 후 BossRaidManager의 Player Controller에 할당하세요.");
Debug.Log("[BossRaid] PlayerController에 Head Transform(머리)과 NiloToonController를 할당하세요.");
// === 12. 보스 공격 ===
var attackObj = new GameObject("BossAttack");
attackObj.transform.SetParent(root.transform);
attackObj.AddComponent<BossAttack>();
// === 13. 파티클 프리팹 연결 ===
AssignParticlePrefabs(bossData);
// === 완료 ===
@ -107,6 +124,31 @@ namespace Streamingle.Contents.BossRaid.Editor
return data;
}
static PlayerData FindOrCreatePlayerData()
{
string[] guids = AssetDatabase.FindAssets("t:PlayerData");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
var existing = AssetDatabase.LoadAssetAtPath<PlayerData>(path);
if (existing != null)
{
Debug.Log($"[BossRaid] 기존 PlayerData 사용: {path}");
return existing;
}
}
string dataPath = "Assets/Scripts/Contents/BossRaid/Resources";
EnsureFolder(dataPath);
var data = ScriptableObject.CreateInstance<PlayerData>();
string assetPath = dataPath + "/PlayerData.asset";
AssetDatabase.CreateAsset(data, assetPath);
AssetDatabase.SaveAssets();
Debug.Log($"[BossRaid] PlayerData 생성: {assetPath}");
return data;
}
static void EnsureParticlePrefabs()
{
string particlePath = "Assets/Scripts/Contents/BossRaid/Resources/Particles";

View File

@ -512,6 +512,184 @@ def gen_phase_transition():
save_wav("sfx_phase_transition.wav", result)
def gen_boss_breath():
"""보스 브레스 발사 - 에너지 차징 → 발사"""
# 차징 (고주파 수렴)
charge = pitch_sweep(1500, 400, 0.3, exponential=True)
t_ch = np.linspace(0, 1, len(charge))
charge = charge * (0.3 + 0.7 * np.power(t_ch, 0.5))
charge = adsr(charge, attack=0.05, decay=0.05, sustain=0.9, release=0.02) * 0.4
charge_noise = periodic_noise(3000, 0.3) * 0.2
charge_noise = charge_noise * np.power(t_ch, 1.5)
charge_part = mix_layers((charge, 1.0), (charge_noise, 0.5))
# 발사 (폭발적 방출)
blast = pitch_sweep(300, 60, 0.4, exponential=True)
blast = adsr(blast, attack=0.001, decay=0.25, sustain=0.1, release=0.15) * 0.8
blast_noise = noise(0.35) * 0.6
blast_noise = adsr(blast_noise, attack=0.001, decay=0.2, sustain=0.1, release=0.15)
sizzle = periodic_noise(5000, 0.3) * 0.3
sizzle = adsr(sizzle, attack=0.001, decay=0.15, sustain=0.1, release=0.15)
blast_part = mix_layers((blast, 1.0), (blast_noise, 0.6), (sizzle, 0.4))
blast_part = delay_effect(blast_part, delay_time=0.06, feedback=0.3, mix_amount=0.2)
result = concat(charge_part, blast_part)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_breath.wav", result)
def gen_boss_slam():
"""보스 내려찍기 - 무거운 임팩트"""
windup = pitch_sweep(200, 600, 0.08, exponential=True)
windup = adsr(windup, attack=0.01, decay=0.04, sustain=0.3, release=0.03) * 0.3
impact = pitch_sweep(150, 20, 0.25, exponential=True)
impact = adsr(impact, attack=0.001, decay=0.18, sustain=0.05, release=0.07) * 0.9
sub = triangle_wave(35, 0.2) * 0.6
sub = adsr(sub, attack=0.001, decay=0.15, sustain=0.0, release=0.05)
crumble = noise(0.3) * 0.7
crumble = adsr(crumble, attack=0.001, decay=0.15, sustain=0.15, release=0.15)
debris1 = pitch_sweep(400, 150, 0.06, exponential=True) * 0.2
debris1 = adsr(debris1, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
debris2 = pitch_sweep(500, 200, 0.05, exponential=True) * 0.15
debris2 = adsr(debris2, attack=0.001, decay=0.03, sustain=0.0, release=0.02)
impact_part = mix_layers((impact, 1.0), (sub, 0.7), (crumble, 0.5))
impact_part = delay_effect(impact_part, delay_time=0.08, feedback=0.35, mix_amount=0.25)
result = concat(windup, impact_part, silence(0.05), debris1, silence(0.03), debris2)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_slam.wav", result)
def gen_boss_charge():
"""보스 차징 예고 - 위협적인 경고음"""
beep1 = square_wave(880, 0.06) * 0.5
beep1 = adsr(beep1, attack=0.003, decay=0.02, sustain=0.4, release=0.02)
beep2 = square_wave(880, 0.06) * 0.5
beep2 = adsr(beep2, attack=0.003, decay=0.02, sustain=0.4, release=0.02)
charge = pitch_sweep(100, 900, 0.4, exponential=True)
t_ch = np.linspace(0, 1, len(charge))
trem_freq = np.linspace(4, 25, len(charge))
trem_env = 0.5 + 0.5 * np.sin(np.cumsum(2 * np.pi * trem_freq / SAMPLE_RATE))
charge = charge * trem_env
charge = adsr(charge, attack=0.05, decay=0.05, sustain=0.8, release=0.05) * 0.5
ch_noise = periodic_noise(2000, 0.4) * 0.25
ch_noise = ch_noise * np.power(t_ch, 1.5)
hi = pulse_wave(1200, 0.4, duty=0.125) * 0.15
hi = hi * np.power(t_ch, 2)
hi = adsr(hi, attack=0.1, decay=0.1, sustain=0.7, release=0.05)
charge_part = mix_layers((charge, 1.0), (ch_noise, 0.5), (hi, 0.4))
result = concat(beep1, silence(0.04), beep2, silence(0.06), charge_part)
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.05, feedback=0.2, mix_amount=0.15)
save_wav("sfx_boss_charge.wav", result)
def gen_player_hit():
"""아바타 피격 - 플레이어가 맞는 소리"""
shock = pitch_sweep(1800, 400, 0.08, exponential=True)
shock = adsr(shock, attack=0.001, decay=0.05, sustain=0.1, release=0.03) * 0.6
hit = square_wave(300, 0.03) * 0.7
hit = adsr(hit, attack=0.001, decay=0.02, sustain=0.0, release=0.01)
n = noise(0.06) * 0.5
n = adsr(n, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
wobble = vibrato(250, 0.12, vib_freq=20, vib_depth=0.08)
wobble = adsr(wobble, attack=0.01, decay=0.06, sustain=0.15, release=0.05) * 0.3
sub = triangle_wave(80, 0.06) * 0.3
sub = adsr(sub, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
first = mix_layers((shock, 0.8), (hit, 1.0), (n, 0.6), (sub, 0.5))
result = concat(first, wobble)
result = quantize_8bit(result, levels=64)
save_wav("sfx_player_hit.wav", result)
def gen_player_death():
"""아바타 사망 - 슬픈 하강음"""
def sad_note(freq, dur):
w = square_wave(freq, dur) * 0.45
h = triangle_wave(freq, dur) * 0.2
combined = mix_layers((w, 1.0), (h, 0.5))
return adsr(combined, attack=0.005, decay=0.05, sustain=0.5, release=0.05)
p = silence(0.03)
n1 = sad_note(784, 0.15) # G5
n2 = sad_note(659, 0.15) # E5
n3 = sad_note(523, 0.15) # C5
n4 = sad_note(415, 0.35) # Ab4
melody = concat(n1, p, n2, p, n3, p, n4)
bass = pitch_sweep(200, 60, 0.8, wave_type='triangle', exponential=True)
bass = adsr(bass, attack=0.01, decay=0.4, sustain=0.2, release=0.3) * 0.25
max_len = max(len(melody), len(bass))
if len(melody) < max_len:
melody = np.pad(melody, (0, max_len - len(melody)))
if len(bass) < max_len:
bass = np.pad(bass, (0, max_len - len(bass)))
result = mix_layers((melody, 1.0), (bass, 0.5))
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.1, feedback=0.4, mix_amount=0.3)
save_wav("sfx_player_death.wav", result)
def gen_game_over():
"""게임 오버 - 무거운 패배 징글"""
doom1 = chord([196, 233, 294], 0.25) # Gm
doom1 = adsr(doom1, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
doom2 = chord([175, 220, 262], 0.25) # F
doom2 = adsr(doom2, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
doom3 = chord([165, 196, 247], 0.25) # Em
doom3 = adsr(doom3, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
final = chord([131, 156, 196], 0.6) # Cm
final = adsr(final, attack=0.01, decay=0.2, sustain=0.4, release=0.35) * 0.55
p = silence(0.04)
melody = concat(doom1, p, doom2, p, doom3, p, final)
bass_drone = pitch_sweep(80, 40, 1.5, wave_type='triangle', exponential=True)
bass_drone = adsr(bass_drone, attack=0.05, decay=0.5, sustain=0.3, release=0.5) * 0.3
dark_noise = noise(1.5) * 0.1
dark_noise = adsr(dark_noise, attack=0.1, decay=0.5, sustain=0.15, release=0.5)
max_len = max(len(melody), len(bass_drone), len(dark_noise))
if len(melody) < max_len:
melody = np.pad(melody, (0, max_len - len(melody)))
if len(bass_drone) < max_len:
bass_drone = np.pad(bass_drone, (0, max_len - len(bass_drone)))
if len(dark_noise) < max_len:
dark_noise = np.pad(dark_noise, (0, max_len - len(dark_noise)))
result = mix_layers((melody, 1.0), (bass_drone, 0.5), (dark_noise, 0.4))
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.12, feedback=0.4, mix_amount=0.3)
save_wav("sfx_game_over.wav", result)
# ============================================================
if __name__ == "__main__":
print("Generating 8-bit SFX v2 (enhanced quality)...")
@ -526,5 +704,11 @@ if __name__ == "__main__":
gen_victory()
gen_damage_popup()
gen_phase_transition()
gen_boss_breath()
gen_boss_slam()
gen_boss_charge()
gen_player_hit()
gen_player_death()
gen_game_over()
print("\nDone! 9 SFX files generated (v2).")
print("\nDone! 15 SFX files generated (v2).")

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: cd4815834216deb458eb99d267bdbb05
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: b8f77e6e668266546bb9d1bccf93af50
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 955407f9973c3b74386bdb0e3db3301f
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 27d7dcce9905f6f46b1455f4e5b820dc
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 914d0a76cfde63d4da4a8671fb6c8b21
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 29176d805e336a34991f09a62a0e77d1
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Scripts/Contents/BossRaid/Resources/BossData.asset (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: dab8858c439e1ce44b241189a90bfdae
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a00fb5f394dc72145bdb0da9fae2e036
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3ce6e1aea4cdaf244b71dbaf1750aad2
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 95bbbe340f352a044bb8682c9a7ac460
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Scripts/Contents/BossRaid/Resources/PlayerData.asset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c93755ab47a5cf4ba387129fc315eb4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -186,7 +186,7 @@ namespace Streamingle.Contents.BossRaid
_rootRect.anchorMin = new Vector2(1f, 0.5f);
_rootRect.anchorMax = new Vector2(1f, 0.5f);
_rootRect.pivot = new Vector2(1f, 0.5f);
_rootRect.sizeDelta = new Vector2(160f, 110f);
_rootRect.sizeDelta = new Vector2(300f, 200f);
_rootRect.anchoredPosition = new Vector2(OffScreenX, 0f);
// 콤보 숫자
@ -204,6 +204,7 @@ namespace Streamingle.Contents.BossRaid
_comboText.alignment = TextAnchor.MiddleCenter;
_comboText.fontStyle = FontStyle.Bold;
_comboText.horizontalOverflow = HorizontalWrapMode.Overflow;
_comboText.verticalOverflow = VerticalWrapMode.Overflow;
_comboText.raycastTarget = false;
if (_font != null) _comboText.font = _font;
var outline = comboObj.AddComponent<Outline>();

View File

@ -0,0 +1,139 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// GAME OVER 화면. 유저 HP 0 시 표시됩니다.
/// </summary>
public class GameOverScreen : MonoBehaviour
{
#region Fields
[Header("비주얼")]
[SerializeField] private Color titleColor = new Color(0.9f, 0.15f, 0.1f);
[SerializeField] private Color bgColor = new Color(0f, 0f, 0f, 0.6f);
private Canvas _canvas;
private CanvasGroup _canvasGroup;
private Text _titleText;
private Coroutine _showCoroutine;
private Font _font;
#endregion
#region Unity Messages
private void Awake()
{
_font = BossRaidFontLoader.Load();
CreateUI();
_canvasGroup.alpha = 0f;
}
#endregion
#region Public Methods
public void Show()
{
if (_showCoroutine != null) StopCoroutine(_showCoroutine);
_showCoroutine = StartCoroutine(ShowCoroutine());
}
public void Hide()
{
if (_showCoroutine != null)
{
StopCoroutine(_showCoroutine);
_showCoroutine = null;
}
_canvasGroup.alpha = 0f;
}
#endregion
#region Private Methods
private IEnumerator ShowCoroutine()
{
_titleText.text = "";
_canvasGroup.alpha = 0f;
// 페이드 인
float elapsed = 0f;
while (elapsed < 0.5f)
{
elapsed += Time.deltaTime;
_canvasGroup.alpha = elapsed / 0.5f;
yield return null;
}
_canvasGroup.alpha = 1f;
// 텍스트 등장 (펀치 스케일)
_titleText.text = "GAME OVER";
var rect = _titleText.rectTransform;
rect.localScale = Vector3.one * 2f;
elapsed = 0f;
while (elapsed < 0.4f)
{
elapsed += Time.deltaTime;
float t = elapsed / 0.4f;
float ease = 1f + 2.7f * Mathf.Pow(t - 1f, 3f) + 1.7f * Mathf.Pow(t - 1f, 2f);
rect.localScale = Vector3.one * Mathf.LerpUnclamped(2f, 1f, ease);
yield return null;
}
rect.localScale = Vector3.one;
}
private void CreateUI()
{
var canvasObj = new GameObject("BossRaid_GameOver");
canvasObj.transform.SetParent(transform);
_canvas = canvasObj.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = 201;
_canvasGroup = canvasObj.AddComponent<CanvasGroup>();
_canvasGroup.interactable = false;
_canvasGroup.blocksRaycasts = false;
var scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
// 배경
var bgObj = new GameObject("BG");
bgObj.transform.SetParent(canvasObj.transform, false);
var bgRect = bgObj.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = Vector2.zero;
bgRect.offsetMax = Vector2.zero;
var bgImg = bgObj.AddComponent<Image>();
bgImg.color = bgColor;
bgImg.raycastTarget = false;
// 타이틀
var titleObj = new GameObject("Title");
titleObj.transform.SetParent(canvasObj.transform, false);
var titleRect = titleObj.AddComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0.1f, 0.3f);
titleRect.anchorMax = new Vector2(0.9f, 0.7f);
titleRect.offsetMin = Vector2.zero;
titleRect.offsetMax = Vector2.zero;
_titleText = titleObj.AddComponent<Text>();
_titleText.fontSize = 90;
_titleText.color = titleColor;
_titleText.alignment = TextAnchor.MiddleCenter;
_titleText.fontStyle = FontStyle.Bold;
_titleText.raycastTarget = false;
if (_font != null) _titleText.font = _font;
var outline = titleObj.AddComponent<Outline>();
outline.effectColor = Color.black;
outline.effectDistance = new Vector2(3f, -3f);
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 77c10ab592aba794bb52d7cf2c6047f7

View File

@ -0,0 +1,166 @@
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 유저 HP바. 머리 위 월드스페이스 캔버스로 표시됩니다.
/// </summary>
public class PlayerHPBar : MonoBehaviour
{
#region Fields
[Header("설정")]
[SerializeField] private Vector3 offset = new Vector3(0f, 0.3f, 0f);
[SerializeField] private float barWidth = 1.2f;
[SerializeField] private float barHeight = 0.12f;
[SerializeField] private float smoothSpeed = 8f;
[Header("색상")]
[SerializeField] private Color hpColor = new Color(0.2f, 0.85f, 0.3f);
[SerializeField] private Color hpColorLow = new Color(0.95f, 0.2f, 0.15f);
[SerializeField] private Color bgColor = new Color(0.1f, 0.1f, 0.1f, 0.85f);
[SerializeField] private Color frameColor = new Color(0.3f, 0.3f, 0.35f);
private Canvas _canvas;
private Image _hpFill;
private RectTransform _fillRect;
private Transform _followTarget;
private float _targetRatio = 1f;
private float _displayRatio = 1f;
private bool _isVisible;
private Font _font;
private Text _nameText;
#endregion
#region Unity Messages
private void LateUpdate()
{
if (!_isVisible || _followTarget == null) return;
// 머리 위 따라다님
transform.position = _followTarget.position + offset;
// 카메라를 향하도록
if (Camera.main != null)
_canvas.transform.rotation = Camera.main.transform.rotation;
// 부드러운 HP 감소
_displayRatio = Mathf.Lerp(_displayRatio, _targetRatio, Time.deltaTime * smoothSpeed);
_fillRect.anchorMax = new Vector2(Mathf.Clamp01(_displayRatio), 1f);
_hpFill.color = _displayRatio > 0.3f ? hpColor : hpColorLow;
}
#endregion
#region Public Methods
public void Initialize(Transform headTarget, string playerName = "", Vector3? customOffset = null)
{
_font = BossRaidFontLoader.Load();
_followTarget = headTarget;
if (customOffset.HasValue)
offset = customOffset.Value;
CreateUI();
_isVisible = true;
if (_nameText != null && !string.IsNullOrEmpty(playerName))
_nameText.text = playerName;
}
public void SetHP(float ratio)
{
_targetRatio = Mathf.Clamp01(ratio);
}
public void Show()
{
_isVisible = true;
if (_canvas != null) _canvas.gameObject.SetActive(true);
}
public void Hide()
{
_isVisible = false;
if (_canvas != null) _canvas.gameObject.SetActive(false);
}
#endregion
#region Private Methods
private void CreateUI()
{
var canvasObj = new GameObject("PlayerHPBar_Canvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = Vector3.zero;
_canvas = canvasObj.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.WorldSpace;
_canvas.sortingOrder = 90;
var rt = canvasObj.GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(barWidth, barHeight + 0.15f);
rt.localScale = Vector3.one;
// 프레임
var frameObj = new GameObject("Frame");
frameObj.transform.SetParent(canvasObj.transform, false);
var frameRect = frameObj.AddComponent<RectTransform>();
frameRect.anchorMin = new Vector2(0f, 0f);
frameRect.anchorMax = new Vector2(1f, 0.7f);
frameRect.offsetMin = Vector2.zero;
frameRect.offsetMax = Vector2.zero;
var frameImg = frameObj.AddComponent<Image>();
frameImg.color = frameColor;
frameImg.raycastTarget = false;
// 배경
var bgObj = new GameObject("BG");
bgObj.transform.SetParent(frameObj.transform, false);
var bgRect = bgObj.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = new Vector2(0.01f, 0.01f);
bgRect.offsetMax = new Vector2(-0.01f, -0.01f);
var bgImg = bgObj.AddComponent<Image>();
bgImg.color = bgColor;
bgImg.raycastTarget = false;
// HP Fill
var fillObj = new GameObject("Fill");
fillObj.transform.SetParent(bgObj.transform, false);
_fillRect = fillObj.AddComponent<RectTransform>();
_fillRect.anchorMin = Vector2.zero;
_fillRect.anchorMax = Vector2.one;
_fillRect.offsetMin = Vector2.zero;
_fillRect.offsetMax = Vector2.zero;
_hpFill = fillObj.AddComponent<Image>();
_hpFill.color = hpColor;
_hpFill.raycastTarget = false;
// 이름 텍스트 (바 위)
var nameObj = new GameObject("Name");
nameObj.transform.SetParent(canvasObj.transform, false);
var nameRect = nameObj.AddComponent<RectTransform>();
nameRect.anchorMin = new Vector2(0f, 0.7f);
nameRect.anchorMax = Vector2.one;
nameRect.offsetMin = Vector2.zero;
nameRect.offsetMax = Vector2.zero;
_nameText = nameObj.AddComponent<Text>();
_nameText.text = "";
_nameText.fontSize = 3;
_nameText.color = Color.white;
_nameText.alignment = TextAnchor.MiddleCenter;
_nameText.raycastTarget = false;
if (_font != null) _nameText.font = _font;
var outline = nameObj.AddComponent<Outline>();
outline.effectColor = Color.black;
outline.effectDistance = new Vector2(0.3f, -0.3f);
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b9275b0b06b5094bb7bedc8038afc2a

View File

@ -0,0 +1,121 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 유저 피격 시 빨간 비네트 오버레이 + NiloToon 색상 플래시.
/// </summary>
public class PlayerHitEffect : MonoBehaviour
{
#region Fields
// PlayerData에서 값을 읽되, 없으면 기본값 사용
private Color vignetteColor = new Color(1f, 0f, 0f, 0.4f);
private float vignetteDuration = 0.4f;
private Color hitTintColor = new Color(1f, 0.3f, 0.3f);
private float tintDuration = 0.3f;
private Canvas _canvas;
private Image _vignetteImage;
private Coroutine _vignetteCoroutine;
private Coroutine _tintCoroutine;
private PlayerController _player;
#endregion
#region Unity Messages
private void Awake()
{
CreateVignetteUI();
}
#endregion
#region Public Methods
public void Play(PlayerController player)
{
_player = player;
// PlayerData에서 값 읽기
if (player?.Data != null)
{
vignetteColor = player.Data.vignetteColor;
vignetteDuration = player.Data.vignetteDuration;
hitTintColor = player.Data.hitTintColor;
tintDuration = player.Data.hitTintDuration;
}
if (_vignetteCoroutine != null) StopCoroutine(_vignetteCoroutine);
_vignetteCoroutine = StartCoroutine(VignetteCoroutine());
if (_tintCoroutine != null) StopCoroutine(_tintCoroutine);
_tintCoroutine = StartCoroutine(TintCoroutine());
}
#endregion
#region Private Methods
private IEnumerator VignetteCoroutine()
{
_vignetteImage.color = vignetteColor;
float elapsed = 0f;
while (elapsed < vignetteDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / vignetteDuration;
var c = vignetteColor;
c.a = vignetteColor.a * (1f - t);
_vignetteImage.color = c;
yield return null;
}
_vignetteImage.color = Color.clear;
_vignetteCoroutine = null;
}
private IEnumerator TintCoroutine()
{
if (_player == null) yield break;
_player.FlashHitColor(hitTintColor);
yield return new WaitForSeconds(tintDuration * 0.3f);
// 부드럽게 원래 색상으로
float elapsed = 0f;
float fadeDur = tintDuration * 0.7f;
while (elapsed < fadeDur)
{
elapsed += Time.deltaTime;
float t = elapsed / fadeDur;
Color c = Color.Lerp(hitTintColor, Color.white, t);
_player.FlashHitColor(c);
yield return null;
}
_player.RestoreColor();
_tintCoroutine = null;
}
private void CreateVignetteUI()
{
var canvasObj = new GameObject("BossRaid_PlayerVignette");
canvasObj.transform.SetParent(transform);
_canvas = canvasObj.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = 997;
_vignetteImage = canvasObj.AddComponent<Image>();
_vignetteImage.color = Color.clear;
_vignetteImage.raycastTarget = false;
}
#endregion
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d0dc1f4dbc9ce184586f621b3705c8bd

View File

@ -32,6 +32,7 @@ namespace Streamingle.Contents.BossRaid
private BossController _bossController;
private BossRaidSafety _safety;
private BossRaidAudio _audio;
private PlayerController _playerController;
private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
@ -288,6 +289,7 @@ namespace Streamingle.Contents.BossRaid
CancelInvoke(nameof(CheckBossEvents));
UnsubscribeBossEvents();
UnsubscribePlayerEvents();
}
private void CheckBossEvents()
@ -303,6 +305,17 @@ namespace Streamingle.Contents.BossRaid
_bossController.OnPhaseChanged += OnPhaseChanged;
_bossController.OnDeath += OnBossDeath;
}
// 플레이어 이벤트
var newPlayer = _raidManager?.Player;
if (newPlayer != null && newPlayer != _playerController)
{
UnsubscribePlayerEvents();
_playerController = newPlayer;
_playerController.OnHPChanged += OnPlayerHPChanged;
_playerController.OnDamaged += OnPlayerDamaged;
_playerController.OnDeath += OnPlayerDeath;
}
}
private void UnsubscribeBossEvents()
@ -317,10 +330,24 @@ namespace Streamingle.Contents.BossRaid
}
}
private void UnsubscribePlayerEvents()
{
if (_playerController != null)
{
_playerController.OnHPChanged -= OnPlayerHPChanged;
_playerController.OnDamaged -= OnPlayerDamaged;
_playerController.OnDeath -= OnPlayerDeath;
_playerController = null;
}
}
private void OnRaidStateChanged(BossRaidManager.RaidState state) => BroadcastState();
private void OnHPChanged(int current, int max) => BroadcastState();
private void OnDamaged(int dmg, bool crit, int hp, int max) => BroadcastState();
private void OnPhaseChanged(int idx, BossData.PhaseData phase) => BroadcastState();
private void OnPlayerHPChanged(int current, int max) => BroadcastState();
private void OnPlayerDamaged(int dmg) => BroadcastState();
private void OnPlayerDeath() => BroadcastState();
private void OnBossDeath() => BroadcastState();
#endregion
@ -345,6 +372,13 @@ namespace Streamingle.Contents.BossRaid
hpLockRatio = _safety?.HPLockRatio ?? 0f,
bgmVolume = _audio?.BGMVolume ?? 0.5f,
sfxVolume = _audio?.SFXVolume ?? 1f,
// 유저
playerHP = _raidManager?.Player?.CurrentHP ?? 0,
playerMaxHP = _raidManager?.Player?.MaxHP ?? 0,
playerHPRatio = _raidManager?.Player?.HPRatio ?? 0f,
playerDead = _raidManager?.Player?.IsDead ?? false,
playerGodMode = _raidManager?.Player?.GodMode ?? false,
bossAttacking = _raidManager?.Attack?.IsAttacking ?? false,
};
}
@ -414,6 +448,31 @@ namespace Streamingle.Contents.BossRaid
if (_audio != null) _audio.SFXVolume = sfxVol;
BroadcastState();
break;
// 보스 공격
case "boss_breath":
_raidManager?.ExecuteBreath();
break;
case "boss_slam":
_raidManager?.ExecuteSlam();
break;
case "boss_rage_burst":
_raidManager?.ExecuteRageBurst();
break;
// 유저 제어
case "set_player_hp":
float pRatio = msg["ratio"]?.Value<float>() ?? 1f;
_raidManager?.SetPlayerHP(pRatio);
BroadcastState();
break;
case "heal_player":
_raidManager?.HealPlayer();
BroadcastState();
break;
case "toggle_god_mode":
bool god = msg["value"]?.Value<bool>() ?? false;
_raidManager?.SetPlayerGodMode(god);
BroadcastState();
break;
case "get_state":
BroadcastState();
break;

BIN
streamingle-urp-analysis.md (Stored with Git LFS) Normal file

Binary file not shown.