본문으로 건너뛰기

무기 시스템 구현 계획

v1.0 | 2026-03-31

Coder가 바로 구현할 수 있는 구체적 개발 계획. 우선순위: P0(필수) > P1(중요) > P2(검증)


변경 개요

#우선순위제목대상 파일
1P0투사체 무기 데미지 팝업 미표시 수정enemy_base.gd, boss_base.gd
2P1무기 범위 디버그 시각화weapon_base.gd, game_manager.gd
3P2weapons.json 수치-구현 일치 검증(테스트)

1. [P0] 투사체 무기 데미지 팝업 수정

문제 분석

  • 범위/근접/궤도 무기: _apply_damage_to() 내에서 EventBus.damage_dealt.emit(enemy, dmg, self) 직접 호출 -> DamagePopupManager가 수신 -> 팝업 표시 (정상)
  • 투사체 무기(부적, 붓, 횃불, 쇠뇌, 돌팔매): ProjectileBase -> HitboxComponent.area_entered -> HurtboxComponent.take_hit() -> hurt 시그널 -> enemy_base._on_hurt() -> health.take_damage() + EventBus.enemy_damaged.emit()
    • 문제: _on_hurt()enemy_damaged만 emit하고 damage_dealt는 emit하지 않음
    • DamagePopupManager는 damage_dealt만 수신하므로 투사체 무기의 데미지 팝업이 전혀 표시되지 않음

해결 방안

방안: enemy_base._on_hurt()에서 damage_dealt emit 추가

이 방안이 최선인 이유:

  • 모든 투사체(현재 5종 + 미래 진화 무기 포함)가 자동으로 팝업 표시
  • 기존 범위/근접/궤도 무기의 _apply_damage_to()에서도 HurtboxComponent.take_hit()을 호출하므로, _on_hurt에서 damage_dealt를 emit하면 이중 emit 발생
  • 따라서 기존 _apply_damage_to()damage_dealt.emit() 호출을 제거하고, _on_hurt()에서만 emit하도록 통일

구체적 변경 사항

1-1. game/src/enemies/enemy_base.gd

# 변경 전 (line 115~118)
func _on_hurt(hitbox_source: HitboxComponent) -> void:
health.take_damage(hitbox_source.damage)
EventBus.enemy_damaged.emit(self, hitbox_source.damage)
_play_hit_flash()

# 변경 후
func _on_hurt(hitbox_source: HitboxComponent) -> void:
health.take_damage(hitbox_source.damage)
EventBus.enemy_damaged.emit(self, hitbox_source.damage)
# source를 hitbox의 owner로 추적 (투사체 → 무기 팝업 표시용)
var source: Node2D = hitbox_source.get_parent() as Node2D
EventBus.damage_dealt.emit(self, hitbox_source.damage, source)
_play_hit_flash()

1-2. game/src/enemies/bosses/boss_base.gd

동일한 패턴 적용. _on_hurt()damage_dealt emit 추가.

# boss_base.gd의 _on_hurt에도 동일하게 추가
var source: Node2D = hitbox_source.get_parent() as Node2D
EventBus.damage_dealt.emit(self, hitbox_source.damage, source)

1-3. 기존 무기의 중복 emit 제거

_apply_damage_to() 함수를 사용하는 모든 무기에서 EventBus.damage_dealt.emit() 호출을 제거해야 함. _on_hurt()에서 통합 emit하므로 중복 방지.

대상 파일 (7개):

  • game/src/weapons/bell/bell_weapon.gd (line 59)
  • game/src/weapons/club/club_weapon.gd
  • game/src/weapons/twin_blades/twin_blades_weapon.gd
  • game/src/weapons/ginseng/ginseng_weapon.gd
  • game/src/weapons/wind_horn/wind_horn_weapon.gd
  • game/src/weapons/haegeum/haegeum_weapon.gd
  • game/src/weapons/goblin_fire/goblin_fire_weapon.gd

각 파일의 _apply_damage_to() 함수에서 EventBus.damage_dealt.emit(...) 줄을 삭제.

예시 (bell_weapon.gd):

# 변경 전
func _apply_damage_to(enemy: Node2D, dmg: float, kb: float) -> void:
var hurtbox: HurtboxComponent = _find_hurtbox(enemy)
if hurtbox:
var fake_hitbox: HitboxComponent = HitboxComponent.new()
fake_hitbox.damage = dmg
fake_hitbox.knockback_force = kb
hurtbox.take_hit(fake_hitbox)
fake_hitbox.queue_free()
EventBus.damage_dealt.emit(enemy, dmg, self) # <-- 삭제

# 변경 후
func _apply_damage_to(enemy: Node2D, dmg: float, kb: float) -> void:
var hurtbox: HurtboxComponent = _find_hurtbox(enemy)
if hurtbox:
var fake_hitbox: HitboxComponent = HitboxComponent.new()
fake_hitbox.damage = dmg
fake_hitbox.knockback_force = kb
hurtbox.take_hit(fake_hitbox)
fake_hitbox.queue_free()
# damage_dealt는 enemy_base._on_hurt()에서 통합 emit

주의: _apply_damage_to()에서 fake_hitbox의 parent가 없으므로 hitbox_source.get_parent()가 null이 됨. 해결: fake_hitbox에 meta를 설정하여 source 추적.

1-4. fake_hitbox에 source 정보 전달

범위/근접 무기는 HitboxComponent를 임시 생성(fake_hitbox)하는데, parent가 없어서 _on_hurt()에서 get_parent()가 null이 됨. source 추적을 위해 meta 사용:

# weapon의 _apply_damage_to() — fake_hitbox 생성 시
fake_hitbox.set_meta("weapon_source", self)

# enemy_base._on_hurt() — source 결정 로직
func _on_hurt(hitbox_source: HitboxComponent) -> void:
health.take_damage(hitbox_source.damage)
EventBus.enemy_damaged.emit(self, hitbox_source.damage)
var source: Node2D = null
if hitbox_source.has_meta("weapon_source"):
source = hitbox_source.get_meta("weapon_source") as Node2D
else:
source = hitbox_source.get_parent() as Node2D
EventBus.damage_dealt.emit(self, hitbox_source.damage, source)
_play_hit_flash()

검증 방법

  • 부적/붓/횃불/쇠뇌/돌팔매 투사체가 적에게 히트 시 데미지 팝업 표시 확인
  • 기존 범위/근접/궤도 무기의 데미지 팝업이 여전히 1회만 표시되는지 확인 (중복 없음)
  • 영혼 스킬의 데미지 팝업도 정상 표시 확인 (soul_skill meta 유지)

2. [P1] 무기 범위 디버그 시각화

설계

디버그 모드에서 F3 키를 토글하면 무기 범위를 반투명 원/호로 상시 표시.

시각화 규칙

무기 타입시각화 형태색상위치
범위(area)반투명 원청색(#3498db, alpha 0.15)플레이어 위치
근접(melee)반투명 호(arc)적색(#e74c3c, alpha 0.15)플레이어 전방
궤도(orbit)반투명 원 (궤도 반경)녹색(#2ecc71, alpha 0.15)플레이어 위치
투사체(projectile)표시 없음-- (범위 개념 없음)

투사체는 area: 0이고 충돌 영역이 히트박스 크기로 결정되므로 범위 시각화 대상 아님.

구체적 변경 사항

2-1. game/src/autoloads/game_manager.gd

디버그 모드 플래그 추가:

var debug_show_weapon_range: bool = false

2-2. game/src/autoloads/game_manager.gd

F3 토글 입력 처리:

func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if (event as InputEventKey).keycode == KEY_F3:
debug_show_weapon_range = not debug_show_weapon_range

2-3. game/src/weapons/weapon_base.gd

_draw() 오버라이드로 범위 시각화:

func _process(_delta: float) -> void:
if GameManager.debug_show_weapon_range:
queue_redraw()

func _draw() -> void:
if not GameManager.debug_show_weapon_range:
return
var weapon_type: String = weapon_data.get("type", "")
var area_radius: float = weapon_data.get("area", 0.0)
if area_radius <= 0.0:
return
match weapon_type:
"area":
draw_arc(Vector2.ZERO, area_radius, 0.0, TAU, 64, Color(0.204, 0.596, 0.859, 0.15), 2.0)
draw_arc(Vector2.ZERO, area_radius, 0.0, TAU, 64, Color(0.204, 0.596, 0.859, 0.5), 1.0)
"melee":
# 전방 120도 호
var half_angle: float = deg_to_rad(60.0)
draw_arc(Vector2.ZERO, area_radius, -half_angle, half_angle, 32, Color(0.906, 0.298, 0.235, 0.15), 2.0)
draw_arc(Vector2.ZERO, area_radius, -half_angle, half_angle, 32, Color(0.906, 0.298, 0.235, 0.5), 1.0)
"orbit":
draw_arc(Vector2.ZERO, area_radius, 0.0, TAU, 64, Color(0.180, 0.800, 0.443, 0.15), 2.0)
draw_arc(Vector2.ZERO, area_radius, 0.0, TAU, 64, Color(0.180, 0.800, 0.443, 0.5), 1.0)

주의: weapon_base.gd에 이미 _process()가 있는 서브클래스(bell 등)는 super._process()를 호출하거나, 이 로직을 _physics_process에 넣어야 할 수 있음. 서브클래스에서 _draw()를 오버라이드하는 경우(bell_weapon.gd 등)에는 super._draw()를 호출하도록 수정 필요.

서브클래스의 _draw()에서 기존 이펙트와 디버그 범위를 모두 표시하려면 서브클래스 _draw() 시작 부분에 super._draw() 호출 추가.

영향 받는 서브클래스 _draw() 수정

현재 _draw()를 오버라이드하는 무기:

  • bell_weapon.gd — 링 이펙트
  • club_weapon.gd — 스윙 이펙트
  • twin_blades_weapon.gd — 슬래시 이펙트
  • wind_horn_weapon.gd — 바람 이펙트
  • ginseng_weapon.gd — 독무 이펙트
  • haegeum_weapon.gd — 음파 이펙트

각 서브클래스의 _draw() 시작에 추가:

func _draw() -> void:
super._draw() # 디버그 범위 시각화
# ... 기존 이펙트 코드

검증 방법

  • F3 토글 시 범위/근접/궤도 무기의 범위가 반투명으로 표시
  • F3 해제 시 디버그 시각화 사라짐
  • 기존 무기 이펙트(링, 스윙 등)가 정상 표시

3. [P2] weapons.json 수치-구현 일치 검증

체크리스트

모든 12종 기본 무기에 대해 다음을 수동/테스트로 검증:

검증 항목방법
base_damage 일치_get_damage() 반환값 == JSON 수치 (Lv.1 기준)
cooldown 일치_get_cooldown() 반환값 == JSON 수치 (Lv.1 기준)
area 일치범위/근접/궤도 무기의 히트 범위 == JSON area 값
knockback 일치HitboxComponent.knockback_force == JSON knockback
speed 일치투사체 speed == JSON speed
pierce 일치투사체 pierce_remaining == JSON pierce
special_effects 작동slow(풍각,해금), DoT(산삼), ground_effect(횃불), orbit(도깨비불)
레벨 스케일링Lv.8에서 damage, cooldown, projectile_count 정확히 계산

추천 테스트 코드 (GdUnit4)

game/test/weapons/test_weapon_data_integrity.gd

각 무기 ID에 대해 setup → _get_damage(), _get_cooldown(), _get_projectile_count()의 Lv.1, Lv.8 반환값을 JSON 수치와 비교하는 단위 테스트.


작업 순서 요약

1. [P0] enemy_base.gd / boss_base.gd 수정 (damage_dealt emit 추가)
2. [P0] 7개 무기의 _apply_damage_to()에서 중복 emit 제거 + fake_hitbox meta 추가
3. [P0] 테스트: 투사체 무기 데미지 팝업 표시 확인
4. [P1] game_manager.gd에 디버그 플래그 + F3 토글 추가
5. [P1] weapon_base.gd에 디버그 _draw() 추가
6. [P1] 서브클래스 6개의 _draw()에 super._draw() 호출 추가
7. [P2] 수치 일치 검증 테스트 작성/실행

참고 파일 전체 목록

파일변경 유형
game/src/enemies/enemy_base.gd수정 (P0)
game/src/enemies/bosses/boss_base.gd수정 (P0)
game/src/weapons/bell/bell_weapon.gd수정 (P0)
game/src/weapons/club/club_weapon.gd수정 (P0)
game/src/weapons/twin_blades/twin_blades_weapon.gd수정 (P0)
game/src/weapons/ginseng/ginseng_weapon.gd수정 (P0)
game/src/weapons/wind_horn/wind_horn_weapon.gd수정 (P0)
game/src/weapons/haegeum/haegeum_weapon.gd수정 (P0)
game/src/weapons/goblin_fire/goblin_fire_weapon.gd수정 (P0)
game/src/autoloads/game_manager.gd수정 (P1)
game/src/weapons/weapon_base.gd수정 (P1)
game/test/weapons/test_weapon_data_integrity.gd신규 (P2)