Commit 17e73e22 authored by Gavin An's avatar Gavin An

영웅의 공격방식 변경

parent 48b5d744
No preview for this file type
No preview for this file type
...@@ -12,6 +12,22 @@ signal skill_cooldown_changed(skill_slot: String, remaining: float, duration: fl ...@@ -12,6 +12,22 @@ signal skill_cooldown_changed(skill_slot: String, remaining: float, duration: fl
# 영웅 능력치 한계 상수 # 영웅 능력치 한계 상수
const MAX_HERO_LEVEL: int = 20 const MAX_HERO_LEVEL: int = 20
## 근접 영웅 기본 수동 공격 범위입니다.
## 각 영웅 스크립트에서 값을 덮어써 영웅마다 다른 전방 공격 반경을 줄 수 있습니다.
@export var manual_attack_distance: float = 3.0
## 근접 영웅 수동 공격의 전방 판정 각도입니다.
## 110도면 영웅 정면을 중심으로 좌우 55(110 / 2)도 안의 몬스터가 피해를 받습니다.
@export var manual_attack_angle_degrees: float = 110.0
## 근접 영웅 공격 범위 표시가 화면에 남아 있는 시간입니다.
## 공격 판정 순간만 확인할 수 있도록 짧게 표시합니다.
@export var manual_attack_indicator_duration: float = 0.28
## 근접 영웅 공격 범위 표시 색상입니다.
## 투명도를 포함한 Color 값이므로 영웅과 몬스터를 가리지 않는 반투명 색을 사용합니다.
@export var manual_attack_indicator_color: Color = Color(1.0, 0.25, 0.08, 0.34)
# 레벨 및 경험치 상태 변수 # 레벨 및 경험치 상태 변수
var level: int = 1: var level: int = 1:
set(value): set(value):
...@@ -43,6 +59,8 @@ var skill_mana_costs: Dictionary = { ...@@ -43,6 +59,8 @@ var skill_mana_costs: Dictionary = {
# 3D 이중 선택 링 캐싱 # 3D 이중 선택 링 캐싱
@onready var _selection_ring_outer: MeshInstance3D = $SelectionRingOuter @onready var _selection_ring_outer: MeshInstance3D = $SelectionRingOuter
var _manual_attack_indicator_material: StandardMaterial3D = null
func _ready() -> void: func _ready() -> void:
super._ready() super._ready()
# 영웅 그룹에 별도로 등록 # 영웅 그룹에 별도로 등록
...@@ -69,6 +87,39 @@ func _physics_process(delta: float) -> void: ...@@ -69,6 +87,39 @@ func _physics_process(delta: float) -> void:
if _selection_ring_outer: if _selection_ring_outer:
_selection_ring_outer.rotate_y(-delta * 2.0) _selection_ring_outer.rotate_y(-delta * 2.0)
## 영웅 전용 수동 공격 진입점입니다.
## 일반 유닛은 자동으로 사거리 안의 적을 공격하지만, 근접 영웅은 Attack 버튼을 누를 때만 이 함수로 공격합니다.
func manual_attack() -> void:
if GameManager.is_game_over or not unit_data:
return
if _attack_timer > 0.0:
return
if unit_data.is_ranged:
_manual_ranged_attack()
else:
_manual_melee_area_attack()
## 근접 영웅은 일반 유닛처럼 자동 타겟을 찾지 않습니다.
## 원거리 영웅이 추가될 경우에는 기존 BaseUnit 자동 공격 탐색을 그대로 사용합니다.
func _find_nearest_target() -> void:
if unit_data and unit_data.is_ranged:
super._find_nearest_target()
else:
_target_monster = null
## 근접 영웅은 타겟을 잡고 자동 공격 로직에 들어가지 않습니다.
## 이동 명령/조이스틱 이동은 유지하되, 공격은 manual_attack()에서만 발생하게 분리합니다.
func _process_attack_logic(delta: float) -> void:
if unit_data and unit_data.is_ranged:
super._process_attack_logic(delta)
return
_target_monster = null
_is_currently_attacking = false
_process_normal_logic(delta)
## 경험치 획득 처리 함수 ## 경험치 획득 처리 함수
func gain_xp(amount: int) -> void: func gain_xp(amount: int) -> void:
if level >= MAX_HERO_LEVEL: if level >= MAX_HERO_LEVEL:
...@@ -112,6 +163,181 @@ func _play_level_up_visual_effect() -> void: ...@@ -112,6 +163,181 @@ func _play_level_up_visual_effect() -> void:
tween.tween_property(mat, "emission_energy_multiplier", 10.0, 0.2) tween.tween_property(mat, "emission_energy_multiplier", 10.0, 0.2)
tween.tween_property(mat, "emission_energy_multiplier", 2.0, 0.5) tween.tween_property(mat, "emission_energy_multiplier", 2.0, 0.5)
## 근접 영웅 수동 공격 처리입니다.
## 영웅 정면 기준 manual_attack_radius 안에 있고, manual_attack_angle_degrees 부채꼴 안에 들어온 모든 몬스터에게 피해를 줍니다.
func _manual_melee_area_attack() -> void:
var attack_origin: Vector3 = global_position
var attack_forward: Vector3 = _get_manual_attack_forward()
var hit_monsters: Array[Node3D] = _get_monsters_in_manual_attack_arc(attack_origin, attack_forward)
_show_manual_attack_range_indicator(attack_origin, attack_forward)
if hit_monsters.is_empty():
_play_manual_attack_motion()
_lock_manual_attack_movement_until_hit()
_start_manual_attack_cooldown()
return
# 첫 번째 피격 몬스터는 스킬 Q 같은 타겟 기반 스킬 참조용으로만 기록합니다.
# 공격 방향은 버튼을 누른 순간 영웅이 바라보던 방향을 그대로 사용하며, 몬스터 쪽으로 자동 회전하지 않습니다.
_target_monster = hit_monsters[0]
_play_manual_attack_motion()
_lock_manual_attack_movement_until_hit()
_start_manual_attack_cooldown()
var damage_delay: float = _get_attack_damage_delay(_attack_lock_timer)
var damage_amount: float = unit_data.damage
if damage_delay > 0.0:
get_tree().create_timer(damage_delay).timeout.connect(func() -> void:
_apply_manual_attack_damage(hit_monsters, damage_amount)
)
else:
_apply_manual_attack_damage(hit_monsters, damage_amount)
## 원거리 영웅이 추가될 경우를 위한 기본 수동 공격 fallback입니다.
## 원거리 타입은 기존 자동 공격 구조를 유지하므로, 버튼 입력 시 가장 가까운 적을 즉시 타겟으로 지정하는 정도만 수행합니다.
func _manual_ranged_attack() -> void:
_find_nearest_target()
if _target_monster:
_process_attack_logic(0.0)
## 수동 공격의 애니메이션 락/재생 상태를 설정합니다.
func _play_manual_attack_motion() -> void:
var anim_length: float = _get_attack_animation_length()
var play_speed: float = _get_attack_timing_play_speed()
_attack_lock_timer = _get_attack_lock_duration(anim_length, play_speed)
_is_currently_attacking = true
_is_moving = false
_nav_agent.target_position = global_position
velocity.x = 0.0
velocity.z = 0.0
_update_animations()
## 근접 수동 공격의 타격 확정 전까지 이동을 막습니다.
## 조이스틱이 계속 move_to()를 보내더라도 BaseUnit.move_to()가 이동 명령을 큐에 보관하고,
## 데미지 판정 시점이 지나 후딜레이가 시작되면 큐된 이동을 적용해 자연스럽게 빠져나가게 합니다.
func _lock_manual_attack_movement_until_hit() -> void:
var damage_delay: float = _get_attack_damage_delay(_attack_lock_timer)
_set_melee_move_lock_until_hit(damage_delay)
## 수동 공격 쿨다운을 시작합니다.
## 기존 BaseUnit 공격 쿨다운 필드를 공유해 일반 공격/수동 공격 타이밍 계산을 한곳에서 유지합니다.
func _start_manual_attack_cooldown() -> void:
_attack_timer = unit_data.cooldown
## 현재 영웅의 수동 공격 정면 방향을 반환합니다.
## 표시와 피해 판정이 같은 방향을 쓰도록 공격 순간 한 번만 계산해 두는 데 사용합니다.
func _get_manual_attack_forward() -> Vector3:
var forward: Vector3 = -global_transform.basis.z
forward.y = 0.0
if forward.length() <= 0.01:
return Vector3.FORWARD
return forward.normalized()
## 전방 부채꼴 안의 몬스터 목록을 거리순으로 반환합니다.
## attack_origin과 attack_forward는 표시용 Mesh에도 그대로 전달되어 시각 범위와 실제 판정이 완전히 같은 기준을 씁니다.
func _get_monsters_in_manual_attack_arc(attack_origin: Vector3, attack_forward: Vector3) -> Array[Node3D]:
var hit_monsters: Array[Node3D] = []
var min_dot: float = cos(deg_to_rad(manual_attack_angle_degrees * 0.5))
for monster: Node in get_tree().get_nodes_in_group("monsters"):
if not is_instance_valid(monster) or not monster is Node3D:
continue
if monster.get("is_dead") == true:
continue
var monster_3d: Node3D = monster as Node3D
var monster_position: Vector3 = monster_3d.global_position
var to_monster: Vector3 = monster_position - attack_origin
to_monster.y = 0.0
var distance: float = to_monster.length()
if distance > manual_attack_distance:
continue
if distance <= 0.01:
hit_monsters.append(monster_3d)
continue
var direction_to_monster: Vector3 = to_monster.normalized()
if attack_forward.dot(direction_to_monster) >= min_dot:
hit_monsters.append(monster_3d)
hit_monsters.sort_custom(func(a: Node3D, b: Node3D) -> bool:
return attack_origin.distance_squared_to(a.global_position) < attack_origin.distance_squared_to(b.global_position)
)
return hit_monsters
## 수동 공격에 적중한 몬스터들에게 실제 피해를 적용합니다.
func _apply_manual_attack_damage(hit_monsters: Array[Node3D], damage_amount: float) -> void:
for monster: Node3D in hit_monsters:
if is_instance_valid(monster) and not monster.get("is_dead") and monster.has_method("take_damage"):
monster.call("take_damage", damage_amount)
## 근접 영웅의 실제 공격 판정 범위를 바닥에 잠깐 표시합니다.
## 판정 함수와 같은 radius/angle 값을 사용해 플레이어가 "어디까지 맞는지" 시각적으로 확인할 수 있게 합니다.
func _show_manual_attack_range_indicator(attack_origin: Vector3, attack_forward: Vector3) -> void:
var indicator: MeshInstance3D = MeshInstance3D.new()
indicator.name = "ManualAttackRangeIndicator"
indicator.mesh = _create_manual_attack_indicator_mesh(attack_forward)
indicator.material_override = _get_manual_attack_indicator_material()
indicator.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
var parent_node: Node = get_tree().current_scene if get_tree().current_scene else self
parent_node.add_child(indicator)
indicator.global_position = attack_origin + Vector3(0.0, 0.045, 0.0)
var tween: Tween = create_tween()
tween.tween_interval(manual_attack_indicator_duration)
tween.tween_property(indicator, "transparency", 1.0, 0.12)
tween.finished.connect(func() -> void:
if is_instance_valid(indicator):
indicator.queue_free()
)
## 전방 부채꼴 공격 범위 Mesh를 생성합니다.
## attack_forward 기준으로 월드 방향 버텍스를 직접 만들기 때문에 표시와 피해 판정이 같은 방향을 공유합니다.
func _create_manual_attack_indicator_mesh(attack_forward: Vector3) -> ArrayMesh:
var mesh: ArrayMesh = ArrayMesh.new()
var vertices: PackedVector3Array = PackedVector3Array()
var indices: PackedInt32Array = PackedInt32Array()
var segment_count: int = max(8, int(manual_attack_angle_degrees / 6.0))
var half_angle: float = deg_to_rad(manual_attack_angle_degrees * 0.5)
var forward_2d: Vector2 = Vector2(attack_forward.x, attack_forward.z).normalized()
vertices.append(Vector3.ZERO)
for index: int in range(segment_count + 1):
var ratio: float = float(index) / float(segment_count)
var angle: float = lerpf(-half_angle, half_angle, ratio)
var rotated_direction: Vector2 = forward_2d.rotated(angle)
var point: Vector3 = Vector3(
rotated_direction.x * manual_attack_distance,
0.0,
rotated_direction.y * manual_attack_distance
)
vertices.append(point)
for index: int in range(1, segment_count + 1):
indices.append(0)
indices.append(index)
indices.append(index + 1)
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_INDEX] = indices
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
## 공격 범위 표시용 머티리얼을 반환합니다.
## 매 공격마다 머티리얼을 새로 만들지 않도록 최초 1회 생성 후 재사용합니다.
func _get_manual_attack_indicator_material() -> StandardMaterial3D:
if _manual_attack_indicator_material:
return _manual_attack_indicator_material
_manual_attack_indicator_material = StandardMaterial3D.new()
_manual_attack_indicator_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_manual_attack_indicator_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_manual_attack_indicator_material.albedo_color = manual_attack_indicator_color
_manual_attack_indicator_material.cull_mode = BaseMaterial3D.CULL_DISABLED
_manual_attack_indicator_material.no_depth_test = true
return _manual_attack_indicator_material
## 키보드 스킬 단축키 입력 수신 처리 ## 키보드 스킬 단축키 입력 수신 처리
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if GameManager.is_game_over or not is_selected: if GameManager.is_game_over or not is_selected:
......
...@@ -21,6 +21,15 @@ const DWARF_ATTACK_DAMAGE_RATIO: float = 0.48 ...@@ -21,6 +21,15 @@ const DWARF_ATTACK_DAMAGE_RATIO: float = 0.48
const DWARF_MIN_ATTACK_LOCK: float = 0.65 const DWARF_MIN_ATTACK_LOCK: float = 0.65
const DWARF_MIN_ATTACK_PLAY_SPEED: float = 1.0 const DWARF_MIN_ATTACK_PLAY_SPEED: float = 1.0
const DWARF_MAX_ATTACK_PLAY_SPEED: float = 4.0 const DWARF_MAX_ATTACK_PLAY_SPEED: float = 4.0
const DWARF_MANUAL_ATTACK_DISTANCE: float = 5.2
const DWARF_MANUAL_ATTACK_ANGLE_DEGREES: float = 115.0
func _ready() -> void:
# 드워프는 도끼를 크게 휘두르는 근접 영웅이므로, Attack 버튼 입력 시 넓은 전방 부채꼴을 공격합니다.
# BaseHero의 기본값을 여기에서 덮어써 영웅별 수동 공격 반경을 분리합니다.
manual_attack_distance = DWARF_MANUAL_ATTACK_DISTANCE
manual_attack_angle_degrees = DWARF_MANUAL_ATTACK_ANGLE_DEGREES
super._ready()
func _setup_unit_visuals() -> void: func _setup_unit_visuals() -> void:
# 자식 노드 중 드워프 3D 모델 노드 이름을 명시하여 인스턴스를 확보합니다. # 자식 노드 중 드워프 3D 모델 노드 이름을 명시하여 인스턴스를 확보합니다.
......
...@@ -59,7 +59,7 @@ void fragment() { ...@@ -59,7 +59,7 @@ void fragment() {
} }
" "
[sub_resource type="PlaceholderTexture2DArray" id="PlaceholderTexture2DArray_b0gws"] [sub_resource type="PlaceholderTexture2DArray" id="PlaceholderTexture2DArray_lkjvc"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_lkjvc"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_lkjvc"]
render_priority = 0 render_priority = 0
...@@ -69,7 +69,7 @@ shader_parameter/OffsetPosition = Vector3(0, 0, 0) ...@@ -69,7 +69,7 @@ shader_parameter/OffsetPosition = Vector3(0, 0, 0)
shader_parameter/ZonesSize = 0.0 shader_parameter/ZonesSize = 0.0
shader_parameter/Resolution = 0.0 shader_parameter/Resolution = 0.0
shader_parameter/NumberOfZones = 0.0 shader_parameter/NumberOfZones = 0.0
shader_parameter/WaterTextures = SubResource("PlaceholderTexture2DArray_b0gws") shader_parameter/WaterTextures = SubResource("PlaceholderTexture2DArray_lkjvc")
shader_parameter/WaterFactor = 0.0 shader_parameter/WaterFactor = 0.0
shader_parameter/WaterInnerOffset = 0.3 shader_parameter/WaterInnerOffset = 0.3
shader_parameter/WaterColor = Color(0, 0, 0, 1) shader_parameter/WaterColor = Color(0, 0, 0, 1)
...@@ -99,20 +99,20 @@ waterColorDeep = Color(0.79508156, 0.8459231, 1, 1) ...@@ -99,20 +99,20 @@ waterColorDeep = Color(0.79508156, 0.8459231, 1, 1)
waterColorShallow = Color(0.14232409, 5.7756904e-07, 0.853347, 1) waterColorShallow = Color(0.14232409, 5.7756904e-07, 0.853347, 1)
customShader = SubResource("ShaderMaterial_lkjvc") customShader = SubResource("ShaderMaterial_lkjvc")
[sub_resource type="ZoneResource" id="ZoneResource_vhovy"] [sub_resource type="ZoneResource" id="ZoneResource_tdk5o"]
heightMapImage = ExtResource("1_6h2jq") heightMapImage = ExtResource("1_6h2jq")
splatmapsImage = Array[Image]([ExtResource("2_8a6xj")]) splatmapsImage = Array[Image]([ExtResource("2_8a6xj")])
waterImage = ExtResource("9_lkjvc") waterImage = ExtResource("9_lkjvc")
[sub_resource type="ZonesResource" id="ZonesResource_aj3fk"] [sub_resource type="ZonesResource" id="ZonesResource_b0gws"]
zones = Array[ZoneResource]([SubResource("ZoneResource_vhovy")]) zones = Array[ZoneResource]([SubResource("ZoneResource_tdk5o")])
[node name="DesertBackground" type="TerraBrush" unique_id=568763585] [node name="DesertBackground" type="TerraBrush" unique_id=568763585]
dataPath = "res://assets/map/desert" dataPath = "res://assets/map/desert"
zonesSize = 64 zonesSize = 64
textureSets = SubResource("TextureSetsResource_207lx") textureSets = SubResource("TextureSetsResource_207lx")
waterDefinition = SubResource("WaterResource_gbjga") waterDefinition = SubResource("WaterResource_gbjga")
terrainZones = SubResource("ZonesResource_aj3fk") terrainZones = SubResource("ZonesResource_b0gws")
physics_interpolation_mode = 2 physics_interpolation_mode = 2
top_level = true top_level = true
metadata/_edit_lock_ = false metadata/_edit_lock_ = false
...@@ -12,6 +12,7 @@ const MOVE_COMMAND_INTERVAL: float = 0.08 ...@@ -12,6 +12,7 @@ const MOVE_COMMAND_INTERVAL: float = 0.08
var _hero: Node3D = null var _hero: Node3D = null
var _move_vector: Vector2 = Vector2.ZERO var _move_vector: Vector2 = Vector2.ZERO
var _keyboard_move_vector: Vector2 = Vector2.ZERO
var _is_dragging_move_stick: bool = false var _is_dragging_move_stick: bool = false
var _move_command_timer: float = 0.0 var _move_command_timer: float = 0.0
...@@ -33,12 +34,28 @@ func _ready() -> void: ...@@ -33,12 +34,28 @@ func _ready() -> void:
visible = false visible = false
set_process(false) set_process(false)
## 키보드 공격 입력을 처리합니다.
## A 키는 화면의 ATK 버튼과 완전히 같은 _on_attack_pressed() 경로를 사용해 입력별 동작 차이를 없앱니다.
func _unhandled_input(event: InputEvent) -> void:
if not visible or not is_instance_valid(_hero) or _is_game_over():
return
if event is InputEventKey:
var key_event: InputEventKey = event as InputEventKey
if key_event.pressed and not key_event.echo and key_event.keycode == KEY_A:
_on_attack_pressed()
get_viewport().set_input_as_handled()
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not is_instance_valid(_hero) or _is_game_over(): if not is_instance_valid(_hero) or _is_game_over():
visible = false visible = false
set_process(false) set_process(false)
return return
_update_keyboard_move_vector()
if not _is_dragging_move_stick:
_apply_keyboard_vector_to_stick()
_move_command_timer -= delta _move_command_timer -= delta
if _move_vector.length() > JOYSTICK_DEADZONE and _move_command_timer <= 0.0: if _move_vector.length() > JOYSTICK_DEADZONE and _move_command_timer <= 0.0:
_move_command_timer = MOVE_COMMAND_INTERVAL _move_command_timer = MOVE_COMMAND_INTERVAL
...@@ -153,7 +170,10 @@ func _on_move_area_gui_input(event: InputEvent) -> void: ...@@ -153,7 +170,10 @@ func _on_move_area_gui_input(event: InputEvent) -> void:
_is_dragging_move_stick = mouse_event.pressed _is_dragging_move_stick = mouse_event.pressed
_update_move_stick(mouse_event.position if mouse_event.pressed else _move_area.size * 0.5) _update_move_stick(mouse_event.position if mouse_event.pressed else _move_area.size * 0.5)
if not mouse_event.pressed: if not mouse_event.pressed:
_reset_move_stick() if _keyboard_move_vector.length() > JOYSTICK_DEADZONE:
_apply_keyboard_vector_to_stick()
else:
_reset_move_stick()
elif event is InputEventMouseMotion and _is_dragging_move_stick: elif event is InputEventMouseMotion and _is_dragging_move_stick:
var motion_event: InputEventMouseMotion = event as InputEventMouseMotion var motion_event: InputEventMouseMotion = event as InputEventMouseMotion
_update_move_stick(motion_event.position) _update_move_stick(motion_event.position)
...@@ -169,14 +189,14 @@ func _apply_hero_move_vector() -> void: ...@@ -169,14 +189,14 @@ func _apply_hero_move_vector() -> void:
if _hero.has_method("move_to"): if _hero.has_method("move_to"):
_hero.call("move_to", target_position) _hero.call("move_to", target_position)
## 공격 버튼을 누르면 가장 가까운 살아있는 몬스터를 찾아 영웅의 공격 대상으로 지정합니다. ## 공격 버튼을 누르면 영웅 전용 수동 공격을 실행합니다.
## 근접 영웅은 BaseHero.manual_attack()에서 전방 범위 공격을 하고, 원거리 타입은 기존 자동 타겟 공격 흐름을 사용합니다.
func _on_attack_pressed() -> void: func _on_attack_pressed() -> void:
if not is_instance_valid(_hero): if not is_instance_valid(_hero):
return return
var target: Node3D = _find_nearest_monster() if _hero.has_method("manual_attack"):
if target and _hero.has_method("attack_target"): _hero.call("manual_attack")
_hero.call("attack_target", target)
## 스킬 버튼을 누르면 BaseHero.use_skill()에 그대로 위임합니다. ## 스킬 버튼을 누르면 BaseHero.use_skill()에 그대로 위임합니다.
func _on_skill_pressed(slot: String) -> void: func _on_skill_pressed(slot: String) -> void:
...@@ -199,6 +219,35 @@ func _reset_move_stick() -> void: ...@@ -199,6 +219,35 @@ func _reset_move_stick() -> void:
if _move_area and _move_knob: if _move_area and _move_knob:
_move_knob.position = _move_area.size * 0.5 - _move_knob.size * 0.5 _move_knob.position = _move_area.size * 0.5 - _move_knob.size * 0.5
## 키보드 화살표 입력을 매 프레임 읽어 조이스틱 방향 벡터로 변환합니다.
## 왼쪽/오른쪽은 x축, 위/아래는 조이스틱 y축과 동일하게 대응하며 대각선은 normalized 처리합니다.
func _update_keyboard_move_vector() -> void:
var new_vector: Vector2 = Vector2.ZERO
if Input.is_key_pressed(KEY_LEFT):
new_vector.x -= 1.0
if Input.is_key_pressed(KEY_RIGHT):
new_vector.x += 1.0
if Input.is_key_pressed(KEY_UP):
new_vector.y -= 1.0
if Input.is_key_pressed(KEY_DOWN):
new_vector.y += 1.0
_keyboard_move_vector = new_vector.normalized() if new_vector.length() > 1.0 else new_vector
## 방향키 입력을 실제 조이스틱 상태에 반영합니다.
## knob 위치와 _move_vector를 같은 함수 경로로 갱신해, 키보드와 터치 조이스틱이 완전히 같은 이동 로직을 탑니다.
func _apply_keyboard_vector_to_stick() -> void:
if not _move_area:
return
if _keyboard_move_vector.length() <= JOYSTICK_DEADZONE:
_reset_move_stick()
return
var center: Vector2 = _move_area.size * 0.5
var stick_position: Vector2 = center + _keyboard_move_vector * JOYSTICK_RADIUS
_update_move_stick(stick_position)
## 현재 필드에서 영웅과 가장 가까운 살아있는 몬스터를 찾습니다. ## 현재 필드에서 영웅과 가장 가까운 살아있는 몬스터를 찾습니다.
func _find_nearest_monster() -> Node3D: func _find_nearest_monster() -> Node3D:
var nearest_monster: Node3D = null var nearest_monster: Node3D = null
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment