BT7-Plan Phase 1: VS 순수형 자동 발동 + 하트 분할 시스템
- PlayerAttackTicker.cs 신설 (주기 타이머 자동 발동, SRP 분리) - PlayerAttack.cs 주석 재작성 (수동→자동 전환 명시) - Health.cs: maxHearts 필드 + IncreaseMaxHearts·Heal 메서드 (하트 1개=4쿼터 HP) - PlayerController.cs: Attack 입력 로직 제거, Facing public - AttackHitbox.cs: Decrement(damage) 통합 - InputSystem_Actions.inputactions: Attack 액션·바인딩 완전 제거 - PlayerAttackTests.cs: 10→13 (maxHearts·Ticker·InputActions 부재 검증) BT 레포 commit 3bdda53 연계 (BurningTimesAi main)
This commit is contained in:
parent
898fcf327b
commit
b5ef4d5f05
|
|
@ -12,8 +12,8 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
|
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
|
||||||
m_Name: Universal Render Pipeline Asset
|
m_Name: Universal Render Pipeline Asset
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
k_AssetVersion: 12
|
k_AssetVersion: 13
|
||||||
k_AssetPreviousVersion: 12
|
k_AssetPreviousVersion: 13
|
||||||
m_RendererType: 1
|
m_RendererType: 1
|
||||||
m_RendererData: {fileID: 0}
|
m_RendererData: {fileID: 0}
|
||||||
m_RendererDataList:
|
m_RendererDataList:
|
||||||
|
|
@ -53,6 +53,7 @@ MonoBehaviour:
|
||||||
m_AdditionalLightsShadowResolutionTierHigh: 1024
|
m_AdditionalLightsShadowResolutionTierHigh: 1024
|
||||||
m_ReflectionProbeBlending: 0
|
m_ReflectionProbeBlending: 0
|
||||||
m_ReflectionProbeBoxProjection: 0
|
m_ReflectionProbeBoxProjection: 0
|
||||||
|
m_ReflectionProbeAtlas: 1
|
||||||
m_ShadowDistance: 50
|
m_ShadowDistance: 50
|
||||||
m_ShadowCascadeCount: 1
|
m_ShadowCascadeCount: 1
|
||||||
m_Cascade2Split: 0.25
|
m_Cascade2Split: 0.25
|
||||||
|
|
@ -127,8 +128,14 @@ MonoBehaviour:
|
||||||
m_PrefilterSoftShadowsQualityHigh: 0
|
m_PrefilterSoftShadowsQualityHigh: 0
|
||||||
m_PrefilterSoftShadows: 0
|
m_PrefilterSoftShadows: 0
|
||||||
m_PrefilterScreenCoord: 0
|
m_PrefilterScreenCoord: 0
|
||||||
|
m_PrefilterScreenSpaceIrradiance: 0
|
||||||
m_PrefilterNativeRenderPass: 0
|
m_PrefilterNativeRenderPass: 0
|
||||||
m_PrefilterUseLegacyLightmaps: 0
|
m_PrefilterUseLegacyLightmaps: 0
|
||||||
|
m_PrefilterBicubicLightmapSampling: 0
|
||||||
|
m_PrefilterReflectionProbeRotation: 0
|
||||||
|
m_PrefilterReflectionProbeBlending: 0
|
||||||
|
m_PrefilterReflectionProbeBoxProjection: 0
|
||||||
|
m_PrefilterReflectionProbeAtlas: 0
|
||||||
m_ShaderVariantLogLevel: 0
|
m_ShaderVariantLogLevel: 0
|
||||||
m_ShadowCascades: 0
|
m_ShadowCascades: 0
|
||||||
m_Textures:
|
m_Textures:
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,24 @@ using static Platformer.Core.Simulation;
|
||||||
namespace Platformer.Gameplay
|
namespace Platformer.Gameplay
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when the player triggers an attack (mouse left click / touch attack button).
|
/// Player attack event — fires on a periodic timer (no manual input).
|
||||||
/// EerieVillage BT5-Dev 2단계 신설 — 기획 04 §5 근거리 공격 1종 파일럿.
|
/// BT7-Plan PD 지시 2026-04-24 — VS 순수형 자동 발동 전환 (공격 버튼 제거, 이동·점프만 남기고 공격은 주기 타이머).
|
||||||
/// OnExecute 훅으로 카드 효과·특성 효과가 결합될 확장 지점 (개발 02 §2-1).
|
/// Schedule<PlayerAttack>은 PlayerAttackTicker 컴포넌트가 일정 주기로 발화한다.
|
||||||
|
/// OnExecute 훅으로 카드 효과·특성 효과가 결합될 확장 지점 유지 (개발 02 §2-1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerAttack : Simulation.Event<PlayerAttack>
|
public class PlayerAttack : Simulation.Event<PlayerAttack>
|
||||||
{
|
{
|
||||||
public PlayerController player;
|
public PlayerController player;
|
||||||
public Vector2 direction; // 플레이어 facing 방향 (±1, 0)
|
public Vector2 direction; // 플레이어 facing 방향 (±1, 0) — PlayerController.facing 기반
|
||||||
|
|
||||||
public override void Execute()
|
public override void Execute()
|
||||||
{
|
{
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
|
|
||||||
// 공격 애니메이션 트리거 (Animator에 "attack" trigger 있으면 재생 — 정적 스프라이트면 무시)
|
// 공격 애니메이션 트리거 (Animator에 "attack" trigger 있으면 재생)
|
||||||
if (player.animator != null)
|
if (player.animator != null)
|
||||||
{
|
{
|
||||||
var attackHash = Animator.StringToHash("attack");
|
var attackHash = Animator.StringToHash("attack");
|
||||||
// SetTrigger는 미존재 파라미터여도 예외 없음
|
|
||||||
player.animator.SetTrigger(attackHash);
|
player.animator.SetTrigger(attackHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
using Platformer.Mechanics;
|
||||||
|
using UnityEngine;
|
||||||
|
using static Platformer.Core.Simulation;
|
||||||
|
|
||||||
|
namespace Platformer.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Periodic PlayerAttack event emitter — BT7-Plan PD 지시 2026-04-24 VS 순수형 자동 발동.
|
||||||
|
/// Player GameObject에 부착되어 <see cref="attackInterval"/> 초마다 <see cref="PlayerAttack"/> 이벤트를 Schedule.
|
||||||
|
/// <para>
|
||||||
|
/// 기획 balance/01 v0.2 확정 전 임시 기본값: 0.5초 간격 · Phase 3-B 튠 대상.
|
||||||
|
/// 카드·특성 효과로 주기 수정이 필요해지면 외부에서 <see cref="attackInterval"/>을 직접 조작하거나
|
||||||
|
/// <see cref="ApplyIntervalMultiplier"/>를 통해 합산한다 (P13-1 공용 모듈 인터페이스 전략).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>설계 근거</b>: PlayerAttack.Execute는 이벤트 처리 로직, 본 컴포넌트는 발화 트리거.
|
||||||
|
/// 단일 책임 원칙 + 범용성 (C11) — 타이머 컴포넌트만 교체하면 발화 방식 전환 가능.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(PlayerController))]
|
||||||
|
public class PlayerAttackTicker : MonoBehaviour
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 공격 자동 발동 주기(초). balance/01 v0.2 확정 후 튠 대상.
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("공격 자동 발동 주기(초). 카드·특성으로 증감 가능.")]
|
||||||
|
public float attackInterval = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 게임 시작 직후 첫 발화까지 지연(초). 플레이어 소환 애니메이션 여유.
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("게임 시작 직후 첫 발화까지의 지연(초). Spawn 애니메이션 여유.")]
|
||||||
|
public float startupDelay = 0.3f;
|
||||||
|
|
||||||
|
PlayerController player;
|
||||||
|
float attackTimer;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
player = GetComponent<PlayerController>();
|
||||||
|
attackTimer = Mathf.Max(startupDelay, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// 컨트롤 비활성 상태(마을 이동·메뉴 등)에서는 자동 공격 중단
|
||||||
|
if (player == null || !player.controlEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유효 간격이 0 이하이면 tick 중단 (디자이너 실수 방지)
|
||||||
|
if (attackInterval <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attackTimer -= Time.deltaTime;
|
||||||
|
if (attackTimer <= 0f)
|
||||||
|
{
|
||||||
|
attackTimer += attackInterval;
|
||||||
|
|
||||||
|
var ev = Schedule<PlayerAttack>();
|
||||||
|
ev.player = player;
|
||||||
|
ev.direction = player.Facing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 카드·특성 효과가 공격 주기를 배수로 조정할 때 사용하는 편의 API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="multiplier">1.0 미만이면 주기 단축(발동 가속), 1.0 초과면 둔화.</param>
|
||||||
|
public void ApplyIntervalMultiplier(float multiplier)
|
||||||
|
{
|
||||||
|
if (multiplier <= 0f) return;
|
||||||
|
attackInterval = Mathf.Max(0.05f, attackInterval * multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c7d8e9f0a1b20314253647586978a9b0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -61,11 +61,10 @@ namespace Platformer.Mechanics
|
||||||
if (alreadyHit.Contains(health)) continue;
|
if (alreadyHit.Contains(health)) continue;
|
||||||
|
|
||||||
alreadyHit.Add(health);
|
alreadyHit.Add(health);
|
||||||
for (int i = 0; i < damage; i++)
|
// BT7-Plan TODO (2026-04-24): 적 Health도 하트 분할 시스템 대상이지만
|
||||||
{
|
// i-frame 구조 탓에 Decrement() 반복 호출은 첫 호출 후 무효화된다.
|
||||||
health.Decrement();
|
// Health.Decrement(int damage) 단일 호출로 쿼터 단위 다중 피해 전달.
|
||||||
if (!health.IsAlive) break;
|
health.Decrement(damage);
|
||||||
}
|
|
||||||
|
|
||||||
// Enemy 즉사 시 EnemyController 기반 EnemyDeath 체인 발동 (선택)
|
// Enemy 즉사 시 EnemyController 기반 EnemyDeath 체인 발동 (선택)
|
||||||
if (!health.IsAlive)
|
if (!health.IsAlive)
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,40 @@ using static Platformer.Core.Simulation;
|
||||||
namespace Platformer.Mechanics
|
namespace Platformer.Mechanics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represebts the current vital statistics of some game entity.
|
/// Represents the current vital statistics of some game entity.
|
||||||
|
/// <para>
|
||||||
|
/// BT7-Plan PD 지시 2026-04-24 — 하트 분할 시스템 (젤다 방식). 하트 1개 = 4 쿼터(HP).
|
||||||
|
/// 카드(패시브)·성장으로 <see cref="maxHearts"/>를 증가시킬 수 있으며, 그에 따라
|
||||||
|
/// <see cref="maxHP"/>가 `maxHearts * QuartersPerHeart`로 자동 산정된다.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// 적 ATK는 쿼터 단위. 피해 1 = 1 쿼터 감소, 피해 2 = 반조각, 피해 4 = 하트 1개 소멸.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Health : MonoBehaviour
|
public class Health : MonoBehaviour
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum hit points for the entity.
|
/// 하트 1개당 HP(쿼터) 수 — 젤다 방식 고정값 4.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int maxHP = 1;
|
public const int QuartersPerHeart = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 초기 보유 하트 수. Player는 기본 1(= 4 HP), 카드·성장으로 <see cref="IncreaseMaxHearts"/>를 통해 증가.
|
||||||
|
/// Enemy는 기본 1(= 4 HP)이나 향후 balance/01 v0.2 수치 테이블에 따라 재설정.
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("초기 보유 하트 수. 하트 1개 = 4 HP.")]
|
||||||
|
public int maxHearts = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 산정된 최대 HP(쿼터 단위). 직접 설정 대신 <see cref="maxHearts"/>를 통해 조정한다 (시리얼라이즈 호환 유지).
|
||||||
|
/// BT7-Plan 이전에는 1로 고정되어 있었으나, 현재 PD 지시로 하트 분할 시스템 전환.
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("산정된 최대 HP(쿼터). maxHearts * 4. Inspector에서 직접 수정하지 말 것.")]
|
||||||
|
public int maxHP = QuartersPerHeart;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 무적 시간 (i-frame) 지속 초 단위. 기획 04 §3-2 후보 0.4~0.8s.
|
/// 무적 시간 (i-frame) 지속 초 단위. 기획 04 §3-2 후보 0.4~0.8s.
|
||||||
/// 라이프 1 전제에서 연속 히트 인정 금지를 위한 핵심 장치 (BT5-Dev 2단계 신설).
|
/// 연속 히트 방지용 (하트 분할 전환 후에도 강한 단일 공격을 한 번에 과도 피해 받지 않도록 유지).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float invulnerableDuration = 0.6f;
|
public float invulnerableDuration = 0.6f;
|
||||||
|
|
||||||
|
|
@ -31,11 +53,17 @@ namespace Platformer.Mechanics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAlive => currentHP > 0;
|
public bool IsAlive => currentHP > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 HP(쿼터). HUD는 이 값과 <see cref="maxHP"/>를 함께 읽어 하트 분할 UI를 그린다.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentHP => currentHP;
|
||||||
|
|
||||||
int currentHP;
|
int currentHP;
|
||||||
float invulnerableUntil = -1f;
|
float invulnerableUntil = -1f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Increment the HP of the entity.
|
/// Increment the HP of the entity by a single quarter.
|
||||||
|
/// 카드·픽업으로 하트 1/4 회복 (기존 API 호환).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Increment()
|
public void Increment()
|
||||||
{
|
{
|
||||||
|
|
@ -43,18 +71,39 @@ namespace Platformer.Mechanics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decrement the HP of the entity. Will trigger a HealthIsZero event when
|
/// 쿼터 단위 회복. 하트 1개 전체 회복은 quarters=4 호출.
|
||||||
/// current HP reaches 0. 무적 시간 (i-frame) 내에는 호출되어도 스킵된다.
|
/// </summary>
|
||||||
|
/// <param name="quarters">회복할 쿼터 수 (0 이하면 무시).</param>
|
||||||
|
public void Heal(int quarters)
|
||||||
|
{
|
||||||
|
if (quarters <= 0) return;
|
||||||
|
currentHP = Mathf.Clamp(currentHP + quarters, 0, maxHP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 단일 피해(기본 1 쿼터). 무적 시간 내에는 호출되어도 스킵된다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Decrement()
|
public void Decrement()
|
||||||
{
|
{
|
||||||
|
Decrement(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 쿼터 단위 피해 처리. BT7-Plan 2026-04-24 — 적 ATK는 쿼터 단위(`PlayerHP -= EnemyATK`).
|
||||||
|
/// 무적 시간 내에는 완전 스킵(부분 히트도 인정하지 않음 — 연타 방지 원칙).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="damage">피해량(쿼터). 1 이상 값만 유효.</param>
|
||||||
|
public void Decrement(int damage)
|
||||||
|
{
|
||||||
|
if (damage <= 0) return;
|
||||||
|
|
||||||
// BT5-Dev 2단계: i-frame 보호 — 무적 시간 내 중복 피격 차단
|
// BT5-Dev 2단계: i-frame 보호 — 무적 시간 내 중복 피격 차단
|
||||||
if (Time.time < invulnerableUntil)
|
if (Time.time < invulnerableUntil)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentHP = Mathf.Clamp(currentHP - 1, 0, maxHP);
|
currentHP = Mathf.Clamp(currentHP - damage, 0, maxHP);
|
||||||
|
|
||||||
// 피격 성공 시 무적 시간 활성화 (다음 피격 대비)
|
// 피격 성공 시 무적 시간 활성화 (다음 피격 대비)
|
||||||
if (invulnerableDuration > 0f)
|
if (invulnerableDuration > 0f)
|
||||||
|
|
@ -70,26 +119,55 @@ namespace Platformer.Mechanics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decrement the HP of the entitiy until HP reaches 0.
|
/// 최대 하트 수를 증가시키고 현재 HP도 같은 쿼터만큼 비례 증가.
|
||||||
|
/// BT7-Plan 2026-04-24 — 패시브 카드 `[방호]`·`[회복]` 효과 훅.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">증가시킬 하트 수 (음수 허용하지만 currentHP가 maxHP를 초과하지 않도록 clamp).</param>
|
||||||
|
public void IncreaseMaxHearts(int delta)
|
||||||
|
{
|
||||||
|
if (delta == 0) return;
|
||||||
|
int beforeMax = maxHP;
|
||||||
|
maxHearts = Mathf.Max(0, maxHearts + delta);
|
||||||
|
maxHP = maxHearts * QuartersPerHeart;
|
||||||
|
|
||||||
|
if (delta > 0)
|
||||||
|
{
|
||||||
|
int added = maxHP - beforeMax;
|
||||||
|
currentHP = Mathf.Clamp(currentHP + added, 0, maxHP);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentHP = Mathf.Clamp(currentHP, 0, maxHP);
|
||||||
|
if (currentHP == 0 && maxHP > 0)
|
||||||
|
{
|
||||||
|
var ev = Schedule<HealthIsZero>();
|
||||||
|
ev.health = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrement the HP of the entity until HP reaches 0.
|
||||||
/// i-frame 우회하여 즉사 처리 (낙사·승리 이탈 등 시스템 강제 사망).
|
/// i-frame 우회하여 즉사 처리 (낙사·승리 이탈 등 시스템 강제 사망).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Die()
|
public void Die()
|
||||||
{
|
{
|
||||||
invulnerableUntil = -1f; // i-frame 무효화
|
invulnerableUntil = -1f; // i-frame 무효화
|
||||||
while (currentHP > 0)
|
if (currentHP > 0)
|
||||||
{
|
{
|
||||||
currentHP = Mathf.Clamp(currentHP - 1, 0, maxHP);
|
currentHP = 0;
|
||||||
if (currentHP == 0)
|
var ev = Schedule<HealthIsZero>();
|
||||||
{
|
ev.health = this;
|
||||||
var ev = Schedule<HealthIsZero>();
|
|
||||||
ev.health = this;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
|
// maxHearts가 설정되어 있고 maxHP가 구식 값(1)으로 남아있는 프리팹 호환 처리.
|
||||||
|
// 정상 흐름: maxHP = maxHearts * 4, currentHP = maxHP로 초기화.
|
||||||
|
if (maxHearts <= 0) maxHearts = 1;
|
||||||
|
int expected = maxHearts * QuartersPerHeart;
|
||||||
|
if (maxHP != expected) maxHP = expected;
|
||||||
currentHP = maxHP;
|
currentHP = maxHP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ namespace Platformer.Mechanics
|
||||||
public AudioClip respawnAudio;
|
public AudioClip respawnAudio;
|
||||||
public AudioClip ouchAudio;
|
public AudioClip ouchAudio;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attack sound effect (BT5-Dev 2단계 신설). 미지정 시 무음.
|
/// Attack sound effect. 자동 발동 시 PlayerAttack.Execute가 PlayOneShot으로 재생.
|
||||||
|
/// BT7-Plan 2026-04-24 — VS 순수형 자동 발동 전환 후에도 audio hook 유지.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AudioClip attackAudio;
|
public AudioClip attackAudio;
|
||||||
|
|
||||||
|
|
@ -32,13 +33,9 @@ namespace Platformer.Mechanics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float jumpTakeOffSpeed = 7;
|
public float jumpTakeOffSpeed = 7;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cooldown between attacks in seconds (기획 04 §5-1 Phase 3-B 튠 대상).
|
|
||||||
/// </summary>
|
|
||||||
public float attackCooldown = 0.35f;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크).
|
/// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크).
|
||||||
|
/// 공격은 PlayerAttackTicker가 주기적으로 Schedule<PlayerAttack>을 발화하여 실행.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AttackHitbox attackHitbox;
|
public AttackHitbox attackHitbox;
|
||||||
|
|
||||||
|
|
@ -57,13 +54,14 @@ namespace Platformer.Mechanics
|
||||||
|
|
||||||
private InputAction m_MoveAction;
|
private InputAction m_MoveAction;
|
||||||
private InputAction m_JumpAction;
|
private InputAction m_JumpAction;
|
||||||
private InputAction m_AttackAction;
|
|
||||||
|
|
||||||
// 마지막 공격 시각 — attackCooldown과 비교해 연타 제한
|
// 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지).
|
||||||
float nextAttackTime = 0f;
|
// BT7-Plan 2026-04-24 — PlayerAttackTicker가 자동 발동 시 참조하므로 public 노출.
|
||||||
// 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지)
|
|
||||||
Vector2 facing = Vector2.right;
|
Vector2 facing = Vector2.right;
|
||||||
|
|
||||||
|
/// <summary>현재 플레이어 facing 방향. PlayerAttackTicker가 Schedule 시점에 참조.</summary>
|
||||||
|
public Vector2 Facing => facing;
|
||||||
|
|
||||||
public Bounds Bounds => collider2d.bounds;
|
public Bounds Bounds => collider2d.bounds;
|
||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
|
|
@ -77,11 +75,9 @@ namespace Platformer.Mechanics
|
||||||
|
|
||||||
m_MoveAction = InputSystem.actions.FindAction("Player/Move");
|
m_MoveAction = InputSystem.actions.FindAction("Player/Move");
|
||||||
m_JumpAction = InputSystem.actions.FindAction("Player/Jump");
|
m_JumpAction = InputSystem.actions.FindAction("Player/Jump");
|
||||||
m_AttackAction = InputSystem.actions.FindAction("Player/Attack");
|
|
||||||
|
|
||||||
m_MoveAction.Enable();
|
m_MoveAction.Enable();
|
||||||
m_JumpAction.Enable();
|
m_JumpAction.Enable();
|
||||||
if (m_AttackAction != null) m_AttackAction.Enable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
|
@ -96,15 +92,7 @@ namespace Platformer.Mechanics
|
||||||
stopJump = true;
|
stopJump = true;
|
||||||
Schedule<PlayerStopJump>().player = this;
|
Schedule<PlayerStopJump>().player = this;
|
||||||
}
|
}
|
||||||
|
// 공격은 PlayerAttackTicker가 자동 주기로 Schedule<PlayerAttack>을 발화 (BT7-Plan VS 순수형).
|
||||||
// 공격 입력 처리 (마우스 좌클릭 / 게임패드 RT / 모바일 터치 — Phase 3-B UX)
|
|
||||||
if (m_AttackAction != null && m_AttackAction.WasPressedThisFrame() && Time.time >= nextAttackTime)
|
|
||||||
{
|
|
||||||
nextAttackTime = Time.time + attackCooldown;
|
|
||||||
var ev = Schedule<PlayerAttack>();
|
|
||||||
ev.player = this;
|
|
||||||
ev.direction = facing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,6 @@
|
||||||
"interactions": "",
|
"interactions": "",
|
||||||
"initialStateCheck": false
|
"initialStateCheck": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Attack",
|
|
||||||
"type": "Button",
|
|
||||||
"id": "c9d8e7f6-a5b4-4c3d-2e1f-0a9b8c7d6e5f",
|
|
||||||
"expectedControlType": "",
|
|
||||||
"processors": "",
|
|
||||||
"interactions": "",
|
|
||||||
"initialStateCheck": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Menu",
|
"name": "Menu",
|
||||||
"type": "Button",
|
"type": "Button",
|
||||||
|
|
@ -209,28 +200,6 @@
|
||||||
"isComposite": false,
|
"isComposite": false,
|
||||||
"isPartOfComposite": false
|
"isPartOfComposite": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"id": "d7e6f5a4-b3c2-4d1e-9f8a-7b6c5d4e3f2a",
|
|
||||||
"path": "<Mouse>/leftButton",
|
|
||||||
"interactions": "",
|
|
||||||
"processors": "",
|
|
||||||
"groups": ";Keyboard&Mouse",
|
|
||||||
"action": "Attack",
|
|
||||||
"isComposite": false,
|
|
||||||
"isPartOfComposite": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"id": "e8f7a6b5-c4d3-4e2f-1a9b-8c7d6e5f4a3b",
|
|
||||||
"path": "<Gamepad>/rightTrigger",
|
|
||||||
"interactions": "",
|
|
||||||
"processors": "",
|
|
||||||
"groups": ";Gamepad",
|
|
||||||
"action": "Attack",
|
|
||||||
"isComposite": false,
|
|
||||||
"isPartOfComposite": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"id": "bf91b079-c2a3-41a7-ab9e-6e5fbc7e9f4b",
|
"id": "bf91b079-c2a3-41a7-ab9e-6e5fbc7e9f4b",
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,34 @@ using UnityEngine;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EerieVillage BT5-Dev 2단계 — Player 근거리 공격 체계 EditMode 테스트.
|
/// EerieVillage BT7-Plan 통합 EditMode 테스트 — Player 근거리 공격 체계·하트 분할 HP·자동 발동 타이머·아틀라스 참조.
|
||||||
/// Prefab 자산의 컴포넌트 구성이 기획 04 §5-1 (근거리 공격 1종) 을 충족하는지 검증.
|
/// Prefab/Script 자산의 컴포넌트·필드·YAML 구성이 기획 방향(BT7-Plan 2026-04-24)을 충족하는지 검증.
|
||||||
/// Play 모드 실행 불요 — prefab YAML 직렬화 상태를 직접 검증하여 회귀 방지.
|
/// Play 모드 실행 불요 — prefab/script YAML 직렬화 상태를 직접 검증하여 회귀 방지.
|
||||||
///
|
///
|
||||||
/// 2026-04-23 개정: Platformer.* 네임스페이스 직접 참조 제거 (Scripts/ 하위에 asmdef 부재로
|
/// 2026-04-23 BT5-Dev: reflection 기반 전환 (Scripts/ 하위 asmdef 부재 대응).
|
||||||
/// 테스트 어셈블리가 Assembly-CSharp 를 참조 불가한 구조 — reflection 기반으로 전환).
|
/// 2026-04-24 BT7-Plan: VS 순수형 자동 발동 전환에 맞춰 수동 입력(Attack 액션·attackCooldown) 검증 제거 +
|
||||||
|
/// PlayerAttackTicker·Health.maxHearts/maxHP·IncreaseMaxHearts·Heal 필드 검증 신규.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerAttackTests
|
public class PlayerAttackTests
|
||||||
{
|
{
|
||||||
const string PlayerPrefabPath = "Assets/Prefabs/Player.prefab";
|
const string PlayerPrefabPath = "Assets/Prefabs/Player.prefab";
|
||||||
const string EnemyPrefabPath = "Assets/Prefabs/Enemy.prefab";
|
const string EnemyPrefabPath = "Assets/Prefabs/Enemy.prefab";
|
||||||
|
|
||||||
// Platformer.* 는 Assembly-CSharp 에 속함. 테스트 어셈블리에서 직접 타입 참조 불가하므로
|
|
||||||
// GetComponents<Component>() + GetType().FullName 매칭으로 검증.
|
|
||||||
const string AttackHitboxType = "Platformer.Mechanics.AttackHitbox";
|
const string AttackHitboxType = "Platformer.Mechanics.AttackHitbox";
|
||||||
const string HealthType = "Platformer.Mechanics.Health";
|
const string HealthType = "Platformer.Mechanics.Health";
|
||||||
const string PlayerControllerType = "Platformer.Mechanics.PlayerController";
|
const string PlayerControllerType = "Platformer.Mechanics.PlayerController";
|
||||||
const string EnemyControllerType = "Platformer.Mechanics.EnemyController";
|
const string EnemyControllerType = "Platformer.Mechanics.EnemyController";
|
||||||
|
const string PlayerAttackTickerType = "Platformer.Gameplay.PlayerAttackTicker";
|
||||||
|
|
||||||
|
static System.Type FindTypeByFullName(string fullName)
|
||||||
|
{
|
||||||
|
foreach (var asm in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
var t = asm.GetType(fullName, throwOnError: false);
|
||||||
|
if (t != null) return t;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Component FindComponentByFullName(GameObject go, string fullName)
|
static Component FindComponentByFullName(GameObject go, string fullName)
|
||||||
{
|
{
|
||||||
|
|
@ -42,6 +52,8 @@ public class PlayerAttackTests
|
||||||
return prop?.GetValue(obj);
|
return prop?.GetValue(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Player/Enemy Prefab 컴포넌트 구성 =====
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Player_Prefab_Has_AttackHitbox_Component()
|
public void Player_Prefab_Has_AttackHitbox_Component()
|
||||||
{
|
{
|
||||||
|
|
@ -84,7 +96,7 @@ public class PlayerAttackTests
|
||||||
var damage = GetFieldOrProperty(hitbox, "damage");
|
var damage = GetFieldOrProperty(hitbox, "damage");
|
||||||
Assert.IsNotNull(damage, "AttackHitbox.damage 필드/프로퍼티 접근 불가");
|
Assert.IsNotNull(damage, "AttackHitbox.damage 필드/프로퍼티 접근 불가");
|
||||||
Assert.AreEqual(1, System.Convert.ToInt32(damage),
|
Assert.AreEqual(1, System.Convert.ToInt32(damage),
|
||||||
"기본 대미지 1 (기획 04 §5-1, Phase 3-B 튠 전 파일럿 값).");
|
"기본 대미지 1 (Phase 3-B balance/01 v0.2 튠 전 파일럿 값).");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -109,23 +121,9 @@ public class PlayerAttackTests
|
||||||
var health = FindComponentByFullName(prefab, HealthType);
|
var health = FindComponentByFullName(prefab, HealthType);
|
||||||
Assert.IsNotNull(health,
|
Assert.IsNotNull(health,
|
||||||
"Enemy.prefab 에 Health 컴포넌트 누락. " +
|
"Enemy.prefab 에 Health 컴포넌트 누락. " +
|
||||||
"BT5-Dev 2단계 재위임 집행분 (2026-04-23) 이 prefab YAML 에 반영되어야 함. " +
|
|
||||||
"Health 없으면 AttackHitbox.Update 의 Decrement 호출이 불가 → EnemyDeath 체인 미발동.");
|
"Health 없으면 AttackHitbox.Update 의 Decrement 호출이 불가 → EnemyDeath 체인 미발동.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Enemy_Prefab_Health_MaxHP_Is_One()
|
|
||||||
{
|
|
||||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(EnemyPrefabPath);
|
|
||||||
var health = FindComponentByFullName(prefab, HealthType);
|
|
||||||
Assert.IsNotNull(health);
|
|
||||||
|
|
||||||
var maxHP = GetFieldOrProperty(health, "maxHP");
|
|
||||||
Assert.IsNotNull(maxHP, "Health.maxHP 필드/프로퍼티 접근 불가");
|
|
||||||
Assert.AreEqual(1, System.Convert.ToInt32(maxHP),
|
|
||||||
"일반 적 기본 maxHP 1 (코어 룰 7 정합, 첫 세팅).");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Enemy_Prefab_Has_EnemyController_Component()
|
public void Enemy_Prefab_Has_EnemyController_Component()
|
||||||
{
|
{
|
||||||
|
|
@ -135,21 +133,103 @@ public class PlayerAttackTests
|
||||||
"EnemyDeath 체인에 EnemyController 필수 (AttackHitbox.Update 에서 Schedule<EnemyDeath>().enemy = enemy).");
|
"EnemyDeath 체인에 EnemyController 필수 (AttackHitbox.Update 에서 Schedule<EnemyDeath>().enemy = enemy).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== BT5-Dev 3단계 PlayerTestGirl 아틀라스 적용 검증 (2026-04-24) =====
|
// ===== BT7-Plan 하트 분할 시스템 검증 (2026-04-24) =====
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Health_Script_Defines_MaxHearts_And_IncreaseMaxHearts()
|
||||||
|
{
|
||||||
|
// PlayerAttack.Execute 등에서 Health 타입을 참조할 때의 계약. 필드·메서드 모두 존재해야 함.
|
||||||
|
var healthType = FindTypeByFullName(HealthType);
|
||||||
|
Assert.IsNotNull(healthType, "Platformer.Mechanics.Health 타입 로드 실패.");
|
||||||
|
|
||||||
|
var maxHeartsField = healthType.GetField("maxHearts",
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||||
|
Assert.IsNotNull(maxHeartsField, "Health.maxHearts 필드 부재 (BT7-Plan 하트 분할 전제).");
|
||||||
|
|
||||||
|
var maxHPField = healthType.GetField("maxHP",
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||||
|
Assert.IsNotNull(maxHPField, "Health.maxHP 필드 부재 (기존 API 호환 유지 필요).");
|
||||||
|
|
||||||
|
var increaseMethod = healthType.GetMethod("IncreaseMaxHearts",
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||||
|
Assert.IsNotNull(increaseMethod, "Health.IncreaseMaxHearts(int) 메서드 부재 — 패시브 카드 훅 필수.");
|
||||||
|
|
||||||
|
var healMethod = healthType.GetMethod("Heal",
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||||
|
Assert.IsNotNull(healMethod, "Health.Heal(int) 메서드 부재 — 쿼터 단위 회복 필수.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Player_Prefab_MaxHP_Reflects_Heart_Quarters()
|
||||||
|
{
|
||||||
|
// Player.prefab의 Health 직렬화 값: maxHearts와 maxHP의 정합성 확인.
|
||||||
|
// maxHP는 maxHearts * 4 또는 0(Awake 재계산 신규 프리팹). 런타임 Awake에서 재산정되지만
|
||||||
|
// Inspector 기본값이 "1"인 구식 프리팹이 섞여 있으면 감지 필요.
|
||||||
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PlayerPrefabPath);
|
||||||
|
var health = FindComponentByFullName(prefab, HealthType);
|
||||||
|
Assert.IsNotNull(health);
|
||||||
|
|
||||||
|
var maxHearts = System.Convert.ToInt32(GetFieldOrProperty(health, "maxHearts"));
|
||||||
|
var maxHP = System.Convert.ToInt32(GetFieldOrProperty(health, "maxHP"));
|
||||||
|
Assert.GreaterOrEqual(maxHearts, 1, "Player 초기 maxHearts는 1 이상 (BT7-Plan 기본 하트 1개).");
|
||||||
|
Assert.IsTrue(maxHP == maxHearts * 4 || maxHP == 1,
|
||||||
|
$"Player.prefab Health.maxHP({maxHP})는 maxHearts*4({maxHearts * 4}) 또는 구식 기본값 1이어야 함. " +
|
||||||
|
"Awake에서 재산정되나 prefab 직렬화 값 점검으로 회귀 방지.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BT7-Plan 자동 발동 타이머 검증 (2026-04-24) =====
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PlayerAttackTicker_Script_Exists_With_AttackInterval()
|
||||||
|
{
|
||||||
|
// PlayerAttackTicker가 존재하고 attackInterval 필드를 노출하는지 검증.
|
||||||
|
// 실제 Player.prefab 부착 여부는 Play 모드 검증 외에 YAML로도 확인 가능하나,
|
||||||
|
// 컴포넌트 첨부가 Unity 수동 작업 or 향후 prefab 편집으로 분리되어도 Script 계약만큼은 유지되어야 함.
|
||||||
|
var tickerType = FindTypeByFullName(PlayerAttackTickerType);
|
||||||
|
Assert.IsNotNull(tickerType,
|
||||||
|
"Platformer.Gameplay.PlayerAttackTicker 타입 로드 실패. " +
|
||||||
|
"BT7-Plan VS 순수형 자동 발동 전환 전제.");
|
||||||
|
|
||||||
|
var intervalField = tickerType.GetField("attackInterval",
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||||
|
Assert.IsNotNull(intervalField, "PlayerAttackTicker.attackInterval 필드 부재.");
|
||||||
|
Assert.AreEqual(typeof(float), intervalField.FieldType,
|
||||||
|
"attackInterval은 float이어야 함 (Time.deltaTime 누적용).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BT7-Plan 공격 버튼 제거 검증 =====
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InputActions_Player_Map_Has_No_Attack_Action()
|
||||||
|
{
|
||||||
|
// InputSystem_Actions.inputactions의 Player 맵에 Attack 액션이 완전히 제거되었는지 YAML 검증.
|
||||||
|
var path = System.IO.Path.GetFullPath("Assets/Settings/InputSystem_Actions.inputactions");
|
||||||
|
Assert.IsTrue(System.IO.File.Exists(path), $"InputActions 파일 부재 — {path}");
|
||||||
|
var json = System.IO.File.ReadAllText(path);
|
||||||
|
|
||||||
|
// Player 맵 내부에 "Attack" 이름의 액션이 없어야 함. UI 맵 범위는 무시.
|
||||||
|
var playerMapStart = json.IndexOf("\"name\": \"Player\"", System.StringComparison.Ordinal);
|
||||||
|
Assert.Greater(playerMapStart, 0, "Player 맵 식별 실패.");
|
||||||
|
var uiMapStart = json.IndexOf("\"name\": \"UI\"", playerMapStart, System.StringComparison.Ordinal);
|
||||||
|
Assert.Greater(uiMapStart, playerMapStart, "UI 맵 식별 실패.");
|
||||||
|
|
||||||
|
var playerMapSection = json.Substring(playerMapStart, uiMapStart - playerMapStart);
|
||||||
|
Assert.IsFalse(
|
||||||
|
playerMapSection.Contains("\"name\": \"Attack\""),
|
||||||
|
"Player 맵에 Attack 액션 잔존. BT7-Plan VS 순수형 전환으로 제거되어야 함.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BT5-Dev 3단계 PlayerTestGirl 아틀라스 적용 검증 (2026-04-24 계승) =====
|
||||||
|
|
||||||
const string PlayerTestGirlGuid = "44ad58ba82191ca4d818108ab01d3baa";
|
const string PlayerTestGirlGuid = "44ad58ba82191ca4d818108ab01d3baa";
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Player_Prefab_SpriteRenderer_References_PlayerTestGirl()
|
public void Player_Prefab_SpriteRenderer_References_PlayerTestGirl()
|
||||||
{
|
{
|
||||||
// Player.prefab YAML 직접 파싱 — SpriteRenderer.m_Sprite 의 guid 가
|
|
||||||
// PlayerTestGirl.png.meta 의 guid 와 일치하는지 검증. 기존 PlayerIdle(
|
|
||||||
// ba86c7b200abe499cb750833482830b3) 에서 PlayerTestGirl 로 교체되었는지 회귀 확인.
|
|
||||||
var path = System.IO.Path.GetFullPath(PlayerPrefabPath);
|
var path = System.IO.Path.GetFullPath(PlayerPrefabPath);
|
||||||
Assert.IsTrue(System.IO.File.Exists(path), $"Player.prefab 부재 — {path}");
|
Assert.IsTrue(System.IO.File.Exists(path), $"Player.prefab 부재 — {path}");
|
||||||
var yaml = System.IO.File.ReadAllText(path);
|
var yaml = System.IO.File.ReadAllText(path);
|
||||||
|
|
||||||
// m_Sprite: {fileID: ..., guid: <GUID>, type: 3}
|
|
||||||
var match = System.Text.RegularExpressions.Regex.Match(
|
var match = System.Text.RegularExpressions.Regex.Match(
|
||||||
yaml,
|
yaml,
|
||||||
@"m_Sprite:\s*\{fileID:\s*[-0-9]+,\s*guid:\s*([a-f0-9]{32}),\s*type:\s*3\}");
|
@"m_Sprite:\s*\{fileID:\s*[-0-9]+,\s*guid:\s*([a-f0-9]{32}),\s*type:\s*3\}");
|
||||||
|
|
@ -162,22 +242,17 @@ public class PlayerAttackTests
|
||||||
[Test]
|
[Test]
|
||||||
public void Player_Controller_Has_Attack_Parameter_And_State()
|
public void Player_Controller_Has_Attack_Parameter_And_State()
|
||||||
{
|
{
|
||||||
// Player.controller YAML 직접 파싱 — attack Trigger 파라미터 + Player-Attack State 존재 검증.
|
|
||||||
// BT5-Dev 3단계에서 PlayerAttack.cs 의 Schedule<PlayerAttack> 과 연동되는 State Machine 요소가
|
|
||||||
// controller 에 명시되었는지 회귀 확인.
|
|
||||||
var path = System.IO.Path.GetFullPath("Assets/Character/Animations/Player.controller");
|
var path = System.IO.Path.GetFullPath("Assets/Character/Animations/Player.controller");
|
||||||
Assert.IsTrue(System.IO.File.Exists(path), $"Player.controller 부재 — {path}");
|
Assert.IsTrue(System.IO.File.Exists(path), $"Player.controller 부재 — {path}");
|
||||||
var yaml = System.IO.File.ReadAllText(path);
|
var yaml = System.IO.File.ReadAllText(path);
|
||||||
|
|
||||||
// m_Name: attack 파라미터 + m_Type: 9 (Trigger)
|
|
||||||
Assert.IsTrue(
|
Assert.IsTrue(
|
||||||
System.Text.RegularExpressions.Regex.IsMatch(
|
System.Text.RegularExpressions.Regex.IsMatch(
|
||||||
yaml,
|
yaml,
|
||||||
@"m_Name:\s*attack\s*\r?\n\s*m_Type:\s*9"),
|
@"m_Name:\s*attack\s*\r?\n\s*m_Type:\s*9"),
|
||||||
"Player.controller 에 attack Trigger 파라미터(m_Type:9) 누락. " +
|
"Player.controller 에 attack Trigger 파라미터(m_Type:9) 누락. " +
|
||||||
"BT5-Dev 3단계 PlayerAttack State 연동 전제.");
|
"자동 발동 PlayerAttack → Animator.SetTrigger 연동 전제.");
|
||||||
|
|
||||||
// Player-Attack State 존재
|
|
||||||
Assert.IsTrue(
|
Assert.IsTrue(
|
||||||
yaml.Contains("m_Name: Player-Attack"),
|
yaml.Contains("m_Name: Player-Attack"),
|
||||||
"Player.controller 에 Player-Attack State 누락. " +
|
"Player.controller 에 Player-Attack State 누락. " +
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Solution>
|
||||||
|
<Project Path="Assembly-CSharp.csproj" />
|
||||||
|
<Project Path="EerieVillage.Tests.Editor.csproj" />
|
||||||
|
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||||
|
<Project Path="Unity.2DPlatformer.Tutorials.csproj" />
|
||||||
|
<Project Path="Unity.Platformer.Tutorials.csproj" />
|
||||||
|
</Solution>
|
||||||
|
|
@ -487,6 +487,43 @@ PlayerSettings:
|
||||||
m_Height: 36
|
m_Height: 36
|
||||||
m_Kind: 0
|
m_Kind: 0
|
||||||
m_SubKind:
|
m_SubKind:
|
||||||
|
- m_BuildTarget: tvOS
|
||||||
|
m_Icons:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 1280
|
||||||
|
m_Height: 768
|
||||||
|
m_Kind: 0
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 800
|
||||||
|
m_Height: 480
|
||||||
|
m_Kind: 0
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 400
|
||||||
|
m_Height: 240
|
||||||
|
m_Kind: 0
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 4640
|
||||||
|
m_Height: 1440
|
||||||
|
m_Kind: 1
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 2320
|
||||||
|
m_Height: 720
|
||||||
|
m_Kind: 1
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 3840
|
||||||
|
m_Height: 1440
|
||||||
|
m_Kind: 1
|
||||||
|
m_SubKind:
|
||||||
|
- m_Textures: []
|
||||||
|
m_Width: 1920
|
||||||
|
m_Height: 720
|
||||||
|
m_Kind: 1
|
||||||
|
m_SubKind:
|
||||||
m_BuildTargetBatching:
|
m_BuildTargetBatching:
|
||||||
- m_BuildTarget: Standalone
|
- m_BuildTarget: Standalone
|
||||||
m_StaticBatching: 1
|
m_StaticBatching: 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue