Commit 9d46295f authored by Gavin An's avatar Gavin An

소환사 선택 화면

parent c35090ea
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnwwqm2dvwbsc"
valid=false
[deps]
source_file="res://assets/ui/summoner/portraits/default_summoner.png"
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://nugrj2mmrqng"
valid=false
[deps]
source_file="res://assets/ui/summoner/portraits/warlock_summoner.png"
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b426g8d4pt5fq"
valid=false
[deps]
source_file="res://assets/ui/summoner/spells/spell_haste.png"
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c6u8dh0qlfmv2"
valid=false
[deps]
source_file="res://assets/ui/summoner/spells/spell_smite.png"
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
...@@ -5,10 +5,11 @@ ...@@ -5,10 +5,11 @@
ldefense/ ldefense/
├── .godot/ ├── .godot/
├── docs/ # 개발 문서 폴더 (.md) ├── docs/ # AI 지침 문서 폴더 (.md)
│ ├── agents.md │ ├── agents.md
│ ├── game_design.md │ ├── game_design.md
│ └── architecture.md │ ├── architecture.md
│ ├──
├── src/ # 실제 게임 소스 코드 폴더 ├── src/ # 실제 게임 소스 코드 폴더
│ ├── autoload/ # 싱글톤 (AutoLoad) 스크립트 │ ├── autoload/ # 싱글톤 (AutoLoad) 스크립트
│ ├── core/ # 메인 게임 루프 및 관리자 스크립트 │ ├── core/ # 메인 게임 루프 및 관리자 스크립트
......
...@@ -44,7 +44,11 @@ func _ready() -> void: ...@@ -44,7 +44,11 @@ func _ready() -> void:
# GameManager 시그널 구독 (스테이지 변경 및 몬스터 수 체크) # GameManager 시그널 구독 (스테이지 변경 및 몬스터 수 체크)
GameManager.active_monster_count_changed.connect(_on_active_monster_count_changed) GameManager.active_monster_count_changed.connect(_on_active_monster_count_changed)
# 3초 뒤에 첫 번째 스테이지 대기 시간 가동 # 대기 시간(쿨다운) 시작을 대기 (소환사 선택 완료 시 start_game()으로 기동)
## 소환사 선택 완료 후 게임을 정식으로 가동하는 수동 엔트리
func start_game() -> void:
print("StageManager: Game started manually after summoner selection.")
_start_next_stage_cooldown(3.0) _start_next_stage_cooldown(3.0)
## 다음 스테이지 준비 대기 시간 시작 ## 다음 스테이지 준비 대기 시간 시작
......
[gd_scene format=3 uid="uid://dimlohck6ebg7"] [gd_scene format=3 uid="uid://ygm6ry4fu5x0"]
[ext_resource type="Script" uid="uid://dk2xux8f0hp7m" path="res://src/entities/monsters/noble_man/noble_man_monster.gd" id="1_base_monster"] [ext_resource type="Script" uid="uid://dk2xux8f0hp7m" path="res://src/entities/monsters/noble_man/noble_man_monster.gd" id="1_base_monster"]
[ext_resource type="PackedScene" uid="uid://dy3qfnuybgjg6" path="res://src/entities/monsters/noble_man/noble_man.tscn" id="2_noble_man"] [ext_resource type="PackedScene" uid="uid://dy3qfnuybgjg6" path="res://src/entities/monsters/noble_man/noble_man.tscn" id="2_noble_man"]
......
class_name Summoner
extends Node
## 소환사 비즈니스 로직 및 주문(Spell) 제어 핵심 클래스
signal mana_changed(current: float, max: float)
signal spell_cooldown_changed(spell_name: String, remaining: float, duration: float)
@export var summoner_data: SummonerData
var current_mana: float = 0.0
var max_mana: float = 100.0
var mana_regen: float = 2.5
# 각 스펠의 쿨다운 현황 및 기본 쿨다운 설정
# spells_cooldown = { "haste": 0.0, "smite": 0.0 }
var spell_cooldowns: Dictionary = {}
var spell_max_cooldowns: Dictionary = {
"haste": 15.0,
"smite": 10.0
}
var spell_mana_costs: Dictionary = {
"haste": 40.0,
"smite": 50.0
}
var is_initialized: bool = false
func _ready() -> void:
# setup_summoner()를 통해 외부에서 데이터를 직접 바인딩할 때까지 대기
pass
## 소환사 데이터를 주입받아 초기 셋업을 완료하는 메서드
func setup_summoner(data: SummonerData) -> void:
summoner_data = data
max_mana = data.max_mana
current_mana = max_mana
mana_regen = data.mana_regen
# 쿨다운 딕셔너리 초기화
spell_cooldowns.clear()
for spell in data.spells:
spell_cooldowns[spell] = 0.0
is_initialized = true
# 초기 마나 신호 발신
mana_changed.emit(current_mana, max_mana)
func _physics_process(delta: float) -> void:
if not is_initialized or GameManager.is_game_over:
return
# 1. 실시간 마나 리젠
if current_mana < max_mana:
current_mana = minf(max_mana, current_mana + mana_regen * delta)
mana_changed.emit(current_mana, max_mana)
# 2. 실시간 스펠 쿨다운 갱신
for spell in spell_cooldowns.keys():
if spell_cooldowns[spell] > 0.0:
spell_cooldowns[spell] = maxf(0.0, spell_cooldowns[spell] - delta)
var max_cd = spell_max_cooldowns.get(spell, 1.0)
spell_cooldown_changed.emit(spell, spell_cooldowns[spell], max_cd)
## 주문 시전 핸들러
func cast_spell(spell_name: String) -> bool:
if not is_initialized or GameManager.is_game_over:
return false
if not spell_cooldowns.has(spell_name):
print("Summoner: Spell '", spell_name, "' is not equipped.")
return false
# 쿨다운 중인지 체크
if spell_cooldowns[spell_name] > 0.0:
print("Summoner: Spell '", spell_name, "' is on cooldown (", spell_cooldowns[spell_name], "s remaining).")
return false
# 마나 비용 체크
var cost = spell_mana_costs.get(spell_name, 0.0)
if current_mana < cost:
print("Summoner: Insufficient mana for '", spell_name, "'. Need: ", cost, ", Current: ", current_mana)
return false
# 주문 실행 분기
var cast_success = false
if spell_name == "haste":
cast_success = _execute_haste()
elif spell_name == "smite":
cast_success = _execute_smite()
if cast_success:
# 마나 소모 및 쿨다운 가동
current_mana -= cost
spell_cooldowns[spell_name] = spell_max_cooldowns[spell_name]
mana_changed.emit(current_mana, max_mana)
spell_cooldown_changed.emit(spell_name, spell_cooldowns[spell_name], spell_max_cooldowns[spell_name])
print("Summoner: Casted spell '", spell_name, "' successfully.")
return true
return false
## 1. 가속 주문 (Haste)
func _execute_haste() -> bool:
var units = get_tree().get_nodes_in_group("units")
if units.is_empty():
print("Summoner: No units on field to apply Haste.")
return false
# 5초간 모든 유닛의 공격 속도 배율을 2배로 상승
for unit in units:
if is_instance_valid(unit) and "attack_speed_mult" in unit:
unit.set("attack_speed_mult", 2.0)
# 비주얼 피드백을 위해 임시 파란색 광원 생성
_attach_temporary_light(unit, Color(0.9, 0.8, 0.1), 5.0)
# 5초 후 원복
get_tree().create_timer(5.0).timeout.connect(func():
var current_units = get_tree().get_nodes_in_group("units")
for unit in current_units:
if is_instance_valid(unit) and "attack_speed_mult" in unit:
unit.set("attack_speed_mult", 1.0)
print("Summoner: Haste spell expired. Unit attack speeds reverted to normal.")
)
print("Summoner: Applied Haste to ", units.size(), " units for 5 seconds.")
return true
## 2. 강타 주문 (Smite)
func _execute_smite() -> bool:
var monsters = get_tree().get_nodes_in_group("monsters")
if monsters.is_empty():
print("Summoner: No active monsters for Smite.")
return false
# 살아있는 유효한 몬스터 필터링
var active_monsters: Array[Node] = []
for m in monsters:
if is_instance_valid(m) and not m.get("is_dead"):
active_monsters.append(m)
if active_monsters.is_empty():
print("Summoner: No alive monsters found.")
return false
# 출구와 가까운 몬스터 (PathFollow3D의 progress가 큰 순서) 기준으로 정렬
active_monsters.sort_custom(func(a: Node, b: Node) -> bool:
var progress_a = a.get("progress") if "progress" in a else 0.0
var progress_b = b.get("progress") if "progress" in b else 0.0
return progress_a > progress_b
)
# 최전방 최대 5마리 타겟팅
var targets_count = min(5, active_monsters.size())
print("Summoner: Smite targeting ", targets_count, " monsters.")
for i in range(targets_count):
var monster = active_monsters[i]
if is_instance_valid(monster):
# 60의 즉시 데미지
if monster.has_method("take_damage"):
monster.call("take_damage", 60.0)
# 낙뢰 이펙트 및 조명 시각화 연동
_create_lightning_strike_effect(monster.global_position)
return true
## 헬퍼: 유닛에게 일정 시간 붙어있는 버프 조명 연출
func _attach_temporary_light(parent_node: Node3D, light_color: Color, duration: float) -> void:
var light = OmniLight3D.new()
light.light_color = light_color
light.light_energy = 3.0
light.omni_range = 3.0
light.position = Vector3(0, 1.2, 0)
parent_node.add_child(light)
get_tree().create_timer(duration).timeout.connect(func():
if is_instance_valid(light):
light.queue_free()
)
## 헬퍼: 3D 월드 상에 번개 기둥 시각화 연출
func _create_lightning_strike_effect(strike_pos: Vector3) -> void:
var root = get_tree().current_scene
if not root:
return
# 1. 번개 기둥 역할을 할 실린더 메쉬 인스턴스 생성
var mesh_inst = MeshInstance3D.new()
var cyl_mesh = CylinderMesh.new()
cyl_mesh.top_radius = 0.05
cyl_mesh.bottom_radius = 0.15
cyl_mesh.height = 10.0
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.1, 0.6, 1.0) # 강렬한 하늘색
mat.emission_enabled = true
mat.emission = Color(0.2, 0.7, 1.0)
mat.emission_energy_multiplier = 4.0
mesh_inst.mesh = cyl_mesh
mesh_inst.material_override = mat
root.add_child(mesh_inst)
# 번개 기둥 위치 정렬: 하늘에서 내리치는 연출이므로 y 오프셋
mesh_inst.global_position = strike_pos + Vector3(0.0, 5.0, 0.0)
# 2. 번개 순간 발광 조명 생성
var flash_light = OmniLight3D.new()
flash_light.light_color = Color(0.3, 0.7, 1.0)
flash_light.light_energy = 8.0
flash_light.omni_range = 6.0
root.add_child(flash_light)
flash_light.global_position = strike_pos + Vector3(0.0, 1.0, 0.0)
# 3. 0.2초 뒤 이펙트 삭제
get_tree().create_timer(0.2).timeout.connect(func():
if is_instance_valid(mesh_inst):
mesh_inst.queue_free()
if is_instance_valid(flash_light):
flash_light.queue_free()
)
class_name SummonerData
extends Resource
## 소환사의 이름, 초상화 에셋 및 주문 목록을 관리하는 데이터 리소스
@export var summoner_name: String = "Default Summoner"
@export var portrait_texture: Texture2D = null
@export var max_mana: float = 100.0
@export var mana_regen: float = 2.5 # 초당 회복량
# 소환사가 보유한 기본 스펠 목록 (예: ["haste", "smite"])
@export var spells: Array[String] = ["haste", "smite"]
...@@ -32,6 +32,7 @@ var _model_anim_player: AnimationPlayer = null ...@@ -32,6 +32,7 @@ var _model_anim_player: AnimationPlayer = null
var _last_anim_state: String = "" var _last_anim_state: String = ""
var _is_currently_attacking: bool = false var _is_currently_attacking: bool = false
var _attack_lock_timer: float = 0.0 # 공격 모션 완주 및 물리 이동 차단을 위한 타이머 var _attack_lock_timer: float = 0.0 # 공격 모션 완주 및 물리 이동 차단을 위한 타이머
var attack_speed_mult: float = 1.0 # 소환사 주문(가속) 등으로 변경 가능한 공속 배율
func _ready() -> void: func _ready() -> void:
# 유닛 그룹에 등록 # 유닛 그룹에 등록
...@@ -58,11 +59,11 @@ func _physics_process(delta: float) -> void: ...@@ -58,11 +59,11 @@ func _physics_process(delta: float) -> void:
velocity = Vector3.ZERO velocity = Vector3.ZERO
return return
# 공격 타이머 및 자동 타겟팅 쿨타임 차감 # 공격 타이머 및 자동 타겟팅 쿨타임 차감 (공속 배율 반영)
if _attack_timer > 0.0: if _attack_timer > 0.0:
_attack_timer -= delta _attack_timer -= delta * attack_speed_mult
if _attack_lock_timer > 0.0: if _attack_lock_timer > 0.0:
_attack_lock_timer -= delta _attack_lock_timer -= delta * attack_speed_mult
if _auto_target_cooldown > 0.0: if _auto_target_cooldown > 0.0:
_auto_target_cooldown -= delta _auto_target_cooldown -= delta
......
...@@ -17,6 +17,7 @@ extends Control ...@@ -17,6 +17,7 @@ extends Control
# 몬스터 일시정지 버튼 런타임 저장 변수 # 몬스터 일시정지 버튼 런타임 저장 변수
var _pause_button: Button = null var _pause_button: Button = null
var _summoner: Summoner = null
...@@ -38,6 +39,15 @@ func _ready() -> void: ...@@ -38,6 +39,15 @@ func _ready() -> void:
_on_stage_changed(GameManager.current_stage) _on_stage_changed(GameManager.current_stage)
_on_monster_changed(GameManager.active_monster_count) _on_monster_changed(GameManager.active_monster_count)
# 소환사 노드 동적 생성 및 바인딩 (선택 전 데이터 주입 보류)
var summoner_script: GDScript = preload("res://src/entities/summoner/summoner.gd")
_summoner = summoner_script.new() as Summoner
_summoner.name = "ActiveSummoner"
add_child(_summoner)
# 소환사 선택 오버레이 화면 띄우기
_show_summoner_selection_screen()
# 몬스터 생성 일시 정지, 게임 재시작 및 모든 유닛 삭제 버튼 런타임 생성 # 몬스터 생성 일시 정지, 게임 재시작 및 모든 유닛 삭제 버튼 런타임 생성
var bottom_bar: Control = get_node_or_null("BottomBar") as Control var bottom_bar: Control = get_node_or_null("BottomBar") as Control
if bottom_bar: if bottom_bar:
...@@ -151,3 +161,240 @@ func _on_pause_spawning_pressed() -> void: ...@@ -151,3 +161,240 @@ func _on_pause_spawning_pressed() -> void:
_pause_button.text = "Resume Spawning" _pause_button.text = "Resume Spawning"
else: else:
_pause_button.text = "Pause Spawning" _pause_button.text = "Pause Spawning"
## 런타임에 소환사 선택 패널을 오버레이로 생성하여 현시
func _show_summoner_selection_screen() -> void:
var selection_overlay: ColorRect = ColorRect.new()
selection_overlay.name = "SummonerSelectionOverlay"
selection_overlay.color = Color(0.08, 0.08, 0.12, 0.95) # 어두운 판타지 테마의 반투명 배경
selection_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(selection_overlay)
var vbox: VBoxContainer = VBoxContainer.new()
vbox.name = "SelectionVBox"
vbox.add_theme_constant_override("separation", 30)
vbox.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
vbox.grow_horizontal = Control.GROW_DIRECTION_BOTH
vbox.grow_vertical = Control.GROW_DIRECTION_BOTH
selection_overlay.add_child(vbox)
# 타이틀 라벨
var title_lbl: Label = Label.new()
title_lbl.name = "TitleLabel"
title_lbl.text = "SELECT YOUR SUMMONER"
title_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_lbl.add_theme_font_size_override("font_size", 24)
vbox.add_child(title_lbl)
# 소환사 카드 그룹
var hbox: HBoxContainer = HBoxContainer.new()
hbox.name = "CardsHBox"
hbox.add_theme_constant_override("separation", 40)
vbox.add_child(hbox)
# --- 1. Archmage (대마법사) 카드 ---
var card_arch: VBoxContainer = VBoxContainer.new()
card_arch.name = "ArchmageCard"
card_arch.add_theme_constant_override("separation", 10)
hbox.add_child(card_arch)
var port_arch: TextureRect = TextureRect.new()
port_arch.custom_minimum_size = Vector2(150, 150)
port_arch.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
port_arch.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
var arch_path: String = "res://assets/ui/summoner/portraits/default_summoner.png"
if ResourceLoader.exists(arch_path):
port_arch.texture = load(arch_path) as Texture2D
card_arch.add_child(port_arch)
var name_arch: Label = Label.new()
name_arch.text = "Archmage"
name_arch.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_arch.add_theme_font_size_override("font_size", 16)
card_arch.add_child(name_arch)
var desc_arch: Label = Label.new()
desc_arch.text = "Max Mana: 100\nMana Regen: 3.0 / s\nSpells: Haste, Smite"
desc_arch.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc_arch.add_theme_font_size_override("font_size", 11)
card_arch.add_child(desc_arch)
var btn_arch: Button = Button.new()
btn_arch.name = "SelectArchmage"
btn_arch.text = "Select Archmage"
btn_arch.custom_minimum_size = Vector2(120, 35)
btn_arch.pressed.connect(func() -> void:
var data: SummonerData = SummonerData.new()
data.summoner_name = "Archmage"
data.max_mana = 100.0
data.mana_regen = 3.0
if ResourceLoader.exists(arch_path):
data.portrait_texture = load(arch_path) as Texture2D
selection_overlay.queue_free()
_on_summoner_selected(data)
)
card_arch.add_child(btn_arch)
# --- 2. Warlock (흑마법사) 카드 ---
var card_war: VBoxContainer = VBoxContainer.new()
card_war.name = "WarlockCard"
card_war.add_theme_constant_override("separation", 10)
hbox.add_child(card_war)
var port_war: TextureRect = TextureRect.new()
port_war.custom_minimum_size = Vector2(150, 150)
port_war.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
port_war.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
var war_path: String = "res://assets/ui/summoner/portraits/warlock_summoner.png"
if ResourceLoader.exists(war_path):
port_war.texture = load(war_path) as Texture2D
card_war.add_child(port_war)
var name_war: Label = Label.new()
name_war.text = "Warlock"
name_war.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_war.add_theme_font_size_override("font_size", 16)
card_war.add_child(name_war)
var desc_war: Label = Label.new()
desc_war.text = "Max Mana: 130\nMana Regen: 1.8 / s\nSpells: Haste, Smite"
desc_war.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc_war.add_theme_font_size_override("font_size", 11)
card_war.add_child(desc_war)
var btn_war: Button = Button.new()
btn_war.name = "SelectWarlock"
btn_war.text = "Select Warlock"
btn_war.custom_minimum_size = Vector2(120, 35)
btn_war.pressed.connect(func() -> void:
var data: SummonerData = SummonerData.new()
data.summoner_name = "Warlock"
data.max_mana = 130.0
data.mana_regen = 1.8
if ResourceLoader.exists(war_path):
data.portrait_texture = load(war_path) as Texture2D
selection_overlay.queue_free()
_on_summoner_selected(data)
)
card_war.add_child(btn_war)
## 소환사 선택 완료 후 UI 구성 및 스테이지 타이머 시작
func _on_summoner_selected(data: SummonerData) -> void:
# 1. 소환사 데이터 실 셋업 가동
_summoner.setup_summoner(data)
# 2. 인게임 소환사 UI 런타임 조립하여 BottomBar에 배치
var bottom_bar: Control = get_node_or_null("BottomBar") as Control
if bottom_bar:
var summoner_ui: HBoxContainer = HBoxContainer.new()
summoner_ui.name = "SummonerUI"
summoner_ui.add_theme_constant_override("separation", 10)
# 소환사 초상화
var portrait: TextureRect = TextureRect.new()
portrait.name = "SummonerPortrait"
portrait.custom_minimum_size = Vector2(48, 48)
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
if data.portrait_texture:
portrait.texture = data.portrait_texture
summoner_ui.add_child(portrait)
# 마나 정보 그룹
var info_group: VBoxContainer = VBoxContainer.new()
info_group.name = "InfoGroup"
info_group.custom_minimum_size = Vector2(110, 48)
var mana_bar: ProgressBar = ProgressBar.new()
mana_bar.name = "ManaBar"
mana_bar.show_percentage = false
mana_bar.custom_minimum_size = Vector2(110, 14)
var sb_bg: StyleBoxFlat = StyleBoxFlat.new()
sb_bg.bg_color = Color(0.12, 0.12, 0.12, 0.8)
sb_bg.set_corner_radius_all(2)
var sb_fg: StyleBoxFlat = StyleBoxFlat.new()
sb_fg.bg_color = Color(0.15, 0.45, 0.85, 1.0)
sb_fg.set_corner_radius_all(2)
mana_bar.add_theme_stylebox_override("background", sb_bg)
mana_bar.add_theme_stylebox_override("fill", sb_fg)
info_group.add_child(mana_bar)
var mana_label: Label = Label.new()
mana_label.name = "ManaLabel"
mana_label.text = "MP: %.1f / %.1f" % [data.max_mana, data.max_mana]
mana_label.add_theme_font_size_override("font_size", 10)
info_group.add_child(mana_label)
summoner_ui.add_child(info_group)
# 스펠 주문 버튼 그룹
var spell_group: HBoxContainer = HBoxContainer.new()
spell_group.name = "SpellGroup"
spell_group.add_theme_constant_override("separation", 5)
var btn_haste: Button = Button.new()
btn_haste.name = "SpellHaste"
btn_haste.text = "Haste (40 MP)"
var haste_icon_path: String = "res://assets/ui/summoner/spells/spell_haste.png"
if ResourceLoader.exists(haste_icon_path):
btn_haste.icon = load(haste_icon_path) as Texture2D
btn_haste.expand_icon = true
btn_haste.custom_minimum_size = Vector2(90, 45)
else:
btn_haste.custom_minimum_size = Vector2(90, 30)
btn_haste.pressed.connect(func() -> void: _summoner.cast_spell("haste"))
spell_group.add_child(btn_haste)
var btn_smite: Button = Button.new()
btn_smite.name = "SpellSmite"
btn_smite.text = "Smite (50 MP)"
var smite_icon_path: String = "res://assets/ui/summoner/spells/spell_smite.png"
if ResourceLoader.exists(smite_icon_path):
btn_smite.icon = load(smite_icon_path) as Texture2D
btn_smite.expand_icon = true
btn_smite.custom_minimum_size = Vector2(90, 45)
else:
btn_smite.custom_minimum_size = Vector2(90, 30)
btn_smite.pressed.connect(func() -> void: _summoner.cast_spell("smite"))
spell_group.add_child(btn_smite)
summoner_ui.add_child(spell_group)
# UI 조립
bottom_bar.add_child(summoner_ui)
bottom_bar.move_child(summoner_ui, 0)
# 신호 연결
_summoner.mana_changed.connect(func(current: float, max_v: float) -> void:
mana_bar.max_value = max_v
mana_bar.value = current
mana_label.text = "MP: %.1f / %.1f" % [current, max_v]
btn_haste.disabled = (current < 40.0 or _summoner.spell_cooldowns.get("haste", 0.0) > 0.0)
btn_smite.disabled = (current < 50.0 or _summoner.spell_cooldowns.get("smite", 0.0) > 0.0)
)
_summoner.spell_cooldown_changed.connect(func(spell_name: String, remaining: float, duration: float) -> void:
var btn: Button = btn_haste if spell_name == "haste" else btn_smite
var label_text: String = "Haste" if spell_name == "haste" else "Smite"
var cost: float = 40.0 if spell_name == "haste" else 50.0
if remaining > 0.0:
btn.text = "%s\n(%.1fs)" % [label_text, remaining]
btn.disabled = true
else:
btn.text = "%s (%d MP)" % [label_text, cost]
if _summoner.current_mana >= cost:
btn.disabled = false
)
# 강제 GUI 갱신용으로 신호 1회 수동 트리거
_summoner.mana_changed.emit(_summoner.current_mana, _summoner.max_mana)
# 3. StageManager를 찾아 수동 게임 시작 기동
var stage_mgr: Node = get_node_or_null("../StageManager")
if stage_mgr and stage_mgr.has_method("start_game"):
stage_mgr.call("start_game")
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