diff --git a/Assets/Characters/warrior/super_attack.png b/Assets/Characters/warrior/super_attack.png new file mode 100644 index 0000000..5c7b851 Binary files /dev/null and b/Assets/Characters/warrior/super_attack.png differ diff --git a/Assets/Characters/warrior/super_attack.png.import b/Assets/Characters/warrior/super_attack.png.import new file mode 100644 index 0000000..92b29f1 --- /dev/null +++ b/Assets/Characters/warrior/super_attack.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/super_attack.png-633190e19ed593ab2a2ddf9b68781b2d.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Characters/warrior/super_attack.png" +dest_files=[ "res://.import/super_attack.png-633190e19ed593ab2a2ddf9b68781b2d.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/arrow_multi.png b/Assets/SkillAnimation/arrow_multi.png new file mode 100644 index 0000000..a6d4d80 Binary files /dev/null and b/Assets/SkillAnimation/arrow_multi.png differ diff --git a/Assets/SkillAnimation/arrow_multi.png.import b/Assets/SkillAnimation/arrow_multi.png.import new file mode 100644 index 0000000..6c78789 --- /dev/null +++ b/Assets/SkillAnimation/arrow_multi.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/arrow_multi.png-e566f9557bb7c8a12ed527f7bd9cf3b9.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/arrow_multi.png" +dest_files=[ "res://.import/arrow_multi.png-e566f9557bb7c8a12ed527f7bd9cf3b9.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/arrow_ricochet.png b/Assets/SkillAnimation/arrow_ricochet.png new file mode 100644 index 0000000..651237f Binary files /dev/null and b/Assets/SkillAnimation/arrow_ricochet.png differ diff --git a/Assets/SkillAnimation/arrow_ricochet.png.import b/Assets/SkillAnimation/arrow_ricochet.png.import new file mode 100644 index 0000000..a22058a --- /dev/null +++ b/Assets/SkillAnimation/arrow_ricochet.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/arrow_ricochet.png-3ddf9c10400670aafbba52b76ec07756.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/arrow_ricochet.png" +dest_files=[ "res://.import/arrow_ricochet.png-3ddf9c10400670aafbba52b76ec07756.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/digit_arrow.png b/Assets/SkillAnimation/digit_arrow.png new file mode 100644 index 0000000..9c8c3dc Binary files /dev/null and b/Assets/SkillAnimation/digit_arrow.png differ diff --git a/Assets/SkillAnimation/digit_arrow.png.import b/Assets/SkillAnimation/digit_arrow.png.import new file mode 100644 index 0000000..b450a95 --- /dev/null +++ b/Assets/SkillAnimation/digit_arrow.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/digit_arrow.png-ab848d6fe8f3279f18f1517bb8377629.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/digit_arrow.png" +dest_files=[ "res://.import/digit_arrow.png-ab848d6fe8f3279f18f1517bb8377629.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/fireball_multi.png b/Assets/SkillAnimation/fireball_multi.png new file mode 100644 index 0000000..d88ea49 Binary files /dev/null and b/Assets/SkillAnimation/fireball_multi.png differ diff --git a/Assets/SkillAnimation/fireball_multi.png.import b/Assets/SkillAnimation/fireball_multi.png.import new file mode 100644 index 0000000..b8fdfad --- /dev/null +++ b/Assets/SkillAnimation/fireball_multi.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/fireball_multi.png-e38cb0ce02f3fbe5dea14a9f58cb6f01.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/fireball_multi.png" +dest_files=[ "res://.import/fireball_multi.png-e38cb0ce02f3fbe5dea14a9f58cb6f01.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/fly_sword-1.png b/Assets/SkillAnimation/fly_sword-1.png new file mode 100644 index 0000000..bdd193f Binary files /dev/null and b/Assets/SkillAnimation/fly_sword-1.png differ diff --git a/Assets/SkillAnimation/fly_sword-1.png.import b/Assets/SkillAnimation/fly_sword-1.png.import new file mode 100644 index 0000000..99086ee --- /dev/null +++ b/Assets/SkillAnimation/fly_sword-1.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/fly_sword-1.png-a7442a514264b0f0f1d43aaaa6e12a4c.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/fly_sword-1.png" +dest_files=[ "res://.import/fly_sword-1.png-a7442a514264b0f0f1d43aaaa6e12a4c.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/fly_sword-2.png b/Assets/SkillAnimation/fly_sword-2.png new file mode 100644 index 0000000..ee5d59b Binary files /dev/null and b/Assets/SkillAnimation/fly_sword-2.png differ diff --git a/Assets/SkillAnimation/fly_sword-2.png.import b/Assets/SkillAnimation/fly_sword-2.png.import new file mode 100644 index 0000000..8fa6a80 --- /dev/null +++ b/Assets/SkillAnimation/fly_sword-2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/fly_sword-2.png-ba5560b5f2b4f5ceab352d4b9dd136b5.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/fly_sword-2.png" +dest_files=[ "res://.import/fly_sword-2.png-ba5560b5f2b4f5ceab352d4b9dd136b5.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/fly_sword.png b/Assets/SkillAnimation/fly_sword.png new file mode 100644 index 0000000..c07efcf Binary files /dev/null and b/Assets/SkillAnimation/fly_sword.png differ diff --git a/Assets/SkillAnimation/fly_sword.png.import b/Assets/SkillAnimation/fly_sword.png.import new file mode 100644 index 0000000..b0a8ec9 --- /dev/null +++ b/Assets/SkillAnimation/fly_sword.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/fly_sword.png-1f0c4760f72d2f7f95268d8f3775b3da.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/fly_sword.png" +dest_files=[ "res://.import/fly_sword.png-1f0c4760f72d2f7f95268d8f3775b3da.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/ice_spear-1.png b/Assets/SkillAnimation/ice_spear-1.png new file mode 100644 index 0000000..17f5b68 Binary files /dev/null and b/Assets/SkillAnimation/ice_spear-1.png differ diff --git a/Assets/SkillAnimation/ice_spear-1.png.import b/Assets/SkillAnimation/ice_spear-1.png.import new file mode 100644 index 0000000..490f2c7 --- /dev/null +++ b/Assets/SkillAnimation/ice_spear-1.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ice_spear-1.png-f15385af1dff55dac62c53c93bee3f68.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/ice_spear-1.png" +dest_files=[ "res://.import/ice_spear-1.png-f15385af1dff55dac62c53c93bee3f68.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/ice_spear-2.png b/Assets/SkillAnimation/ice_spear-2.png new file mode 100644 index 0000000..0b90730 Binary files /dev/null and b/Assets/SkillAnimation/ice_spear-2.png differ diff --git a/Assets/SkillAnimation/ice_spear-2.png.import b/Assets/SkillAnimation/ice_spear-2.png.import new file mode 100644 index 0000000..3c1483a --- /dev/null +++ b/Assets/SkillAnimation/ice_spear-2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ice_spear-2.png-2ac56cb1417d84f4934144826ebd5e68.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/ice_spear-2.png" +dest_files=[ "res://.import/ice_spear-2.png-2ac56cb1417d84f4934144826ebd5e68.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/ice_spear-3.png b/Assets/SkillAnimation/ice_spear-3.png new file mode 100644 index 0000000..d41d0ce Binary files /dev/null and b/Assets/SkillAnimation/ice_spear-3.png differ diff --git a/Assets/SkillAnimation/ice_spear-3.png.import b/Assets/SkillAnimation/ice_spear-3.png.import new file mode 100644 index 0000000..a7748b3 --- /dev/null +++ b/Assets/SkillAnimation/ice_spear-3.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ice_spear-3.png-f05297fe0b1015db0c423258b10854d8.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/ice_spear-3.png" +dest_files=[ "res://.import/ice_spear-3.png-f05297fe0b1015db0c423258b10854d8.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/ice_spear-4.png b/Assets/SkillAnimation/ice_spear-4.png new file mode 100644 index 0000000..7f75f55 Binary files /dev/null and b/Assets/SkillAnimation/ice_spear-4.png differ diff --git a/Assets/SkillAnimation/ice_spear-4.png.import b/Assets/SkillAnimation/ice_spear-4.png.import new file mode 100644 index 0000000..eda60be --- /dev/null +++ b/Assets/SkillAnimation/ice_spear-4.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ice_spear-4.png-1e5e6059b5a2f182cc61a26afe3156d6.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/ice_spear-4.png" +dest_files=[ "res://.import/ice_spear-4.png-1e5e6059b5a2f182cc61a26afe3156d6.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/ice_spear-hit.png b/Assets/SkillAnimation/ice_spear-hit.png new file mode 100644 index 0000000..9dcd042 Binary files /dev/null and b/Assets/SkillAnimation/ice_spear-hit.png differ diff --git a/Assets/SkillAnimation/ice_spear-hit.png.import b/Assets/SkillAnimation/ice_spear-hit.png.import new file mode 100644 index 0000000..96b3c99 --- /dev/null +++ b/Assets/SkillAnimation/ice_spear-hit.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ice_spear-hit.png-fa4c9ba5fcc39a568b39b3544fe0b06d.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/ice_spear-hit.png" +dest_files=[ "res://.import/ice_spear-hit.png-fa4c9ba5fcc39a568b39b3544fe0b06d.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/knight_explose.png b/Assets/SkillAnimation/knight_explose.png new file mode 100644 index 0000000..d5301a3 Binary files /dev/null and b/Assets/SkillAnimation/knight_explose.png differ diff --git a/Assets/SkillAnimation/knight_explose.png.import b/Assets/SkillAnimation/knight_explose.png.import new file mode 100644 index 0000000..7e45ba3 --- /dev/null +++ b/Assets/SkillAnimation/knight_explose.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/knight_explose.png-e78dccd81f3064883d18ab167deb7824.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/knight_explose.png" +dest_files=[ "res://.import/knight_explose.png-e78dccd81f3064883d18ab167deb7824.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/magic_slash-2.png b/Assets/SkillAnimation/magic_slash-2.png new file mode 100644 index 0000000..fae9e19 Binary files /dev/null and b/Assets/SkillAnimation/magic_slash-2.png differ diff --git a/Assets/SkillAnimation/magic_slash-2.png.import b/Assets/SkillAnimation/magic_slash-2.png.import new file mode 100644 index 0000000..e104d9e --- /dev/null +++ b/Assets/SkillAnimation/magic_slash-2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/magic_slash-2.png-9de23a3207cfb975fd9d216b38cd9fd2.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/magic_slash-2.png" +dest_files=[ "res://.import/magic_slash-2.png-9de23a3207cfb975fd9d216b38cd9fd2.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/roundhouse-slash.png b/Assets/SkillAnimation/roundhouse-slash.png new file mode 100644 index 0000000..ecb4ad8 Binary files /dev/null and b/Assets/SkillAnimation/roundhouse-slash.png differ diff --git a/Assets/SkillAnimation/roundhouse-slash.png.import b/Assets/SkillAnimation/roundhouse-slash.png.import new file mode 100644 index 0000000..c45204f --- /dev/null +++ b/Assets/SkillAnimation/roundhouse-slash.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/roundhouse-slash.png-8194ab7ba1d330f17df8f9dfd3e6468a.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/roundhouse-slash.png" +dest_files=[ "res://.import/roundhouse-slash.png-8194ab7ba1d330f17df8f9dfd3e6468a.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/slash-effect.png b/Assets/SkillAnimation/slash-effect.png new file mode 100644 index 0000000..229dca7 Binary files /dev/null and b/Assets/SkillAnimation/slash-effect.png differ diff --git a/Assets/SkillAnimation/slash-effect.png.import b/Assets/SkillAnimation/slash-effect.png.import new file mode 100644 index 0000000..0e57cc9 --- /dev/null +++ b/Assets/SkillAnimation/slash-effect.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/slash-effect.png-a39f77a5b4b6117d9c824bfa5b5046cb.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/slash-effect.png" +dest_files=[ "res://.import/slash-effect.png-a39f77a5b4b6117d9c824bfa5b5046cb.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Assets/SkillAnimation/super_arrow.png b/Assets/SkillAnimation/super_arrow.png new file mode 100644 index 0000000..d615818 Binary files /dev/null and b/Assets/SkillAnimation/super_arrow.png differ diff --git a/Assets/SkillAnimation/super_arrow.png.import b/Assets/SkillAnimation/super_arrow.png.import new file mode 100644 index 0000000..ca983de --- /dev/null +++ b/Assets/SkillAnimation/super_arrow.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/super_arrow.png-c8853dbab1a97b880a75245e7cae8153.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/SkillAnimation/super_arrow.png" +dest_files=[ "res://.import/super_arrow.png-c8853dbab1a97b880a75245e7cae8153.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Scripts/Auth/PlayerProfileSnapshot.cs b/Scripts/Auth/PlayerProfileSnapshot.cs index 1230a45..15bf248 100644 --- a/Scripts/Auth/PlayerProfileSnapshot.cs +++ b/Scripts/Auth/PlayerProfileSnapshot.cs @@ -410,7 +410,7 @@ public class PlayerProfileSnapshot public Godot.Collections.Array DiscardedItems { get; set; } = new Godot.Collections.Array(); public Godot.Collections.Dictionary EquippedItemsPayload { get; set; } = new Godot.Collections.Dictionary(); - /// Serialised class name, e.g. "adventurer", "mage", "archer", "warrior". + /// Serialised class name, e.g. "adventurer", "mage", "archer", "knight". public string ClassName { get; set; } = "adventurer"; public List Skills { get; set; } = new List(); @@ -467,6 +467,9 @@ public static PlayerProfileSnapshot FromDictionary(Godot.Collections.Dictionary DisplayOrder = ReadInt(skillDict, "display_order", i, min: 0), }); } + + // Sort skills in-place by DisplayOrder to guarantee correct sequence on frontend load + snapshot.Skills.Sort((a, b) => a.DisplayOrder.CompareTo(b.DisplayOrder)); } if (snapshot.Skills.Count == 0) @@ -477,6 +480,34 @@ public static PlayerProfileSnapshot FromDictionary(Godot.Collections.Dictionary return snapshot; } + private static Godot.Collections.Array EncodeSkills(List skills) + { + var array = new Godot.Collections.Array(); + if (skills == null) + { + return array; + } + + foreach (var skill in skills) + { + if (skill == null) + { + continue; + } + + var dict = new Godot.Collections.Dictionary + { + ["skill_id"] = skill.SkillId ?? string.Empty, + ["name"] = skill.Name ?? string.Empty, + ["cooldown_seconds"] = skill.CooldownSeconds, + ["display_order"] = skill.DisplayOrder, + }; + array.Add(dict); + } + + return array; + } + public Godot.Collections.Dictionary ToUpdatePayload(string sessionId, int sequence) { return new Godot.Collections.Dictionary @@ -492,6 +523,7 @@ public Godot.Collections.Dictionary ToUpdatePayload(string sessionId, int sequen ["inventory_items"] = InventoryItems ?? new Godot.Collections.Array(), ["discarded_items"] = DiscardedItems ?? new Godot.Collections.Array(), ["equipped_items"] = EquippedItemsPayload ?? new Godot.Collections.Dictionary(), + ["skills"] = EncodeSkills(Skills), }; } diff --git a/Scripts/Characters/Character.cs b/Scripts/Characters/Character.cs index 7178603..902c48e 100644 --- a/Scripts/Characters/Character.cs +++ b/Scripts/Characters/Character.cs @@ -93,7 +93,7 @@ public virtual void UpdateAttributes() /// Apply damage to this character. /// Blocked entirely when is true. /// - public virtual void TakeDamage(int damage) + public virtual void TakeDamage(int damage, Character source = null) { // Invincibility guard — effect-based immunity if (IsInvincible) return; diff --git a/Scripts/Characters/Monster.cs b/Scripts/Characters/Monster.cs index 1a0cbd8..49e14a1 100644 --- a/Scripts/Characters/Monster.cs +++ b/Scripts/Characters/Monster.cs @@ -211,9 +211,9 @@ public override void UpdateAttributes() Attributes.TotalDef = 0; } - public override void TakeDamage(int damage) + public override void TakeDamage(int damage, Character source = null) { - base.TakeDamage(damage); + base.TakeDamage(damage, source); if (!_isDead && Attributes?.HP != null && Attributes.HP.IsAlive) { _isHit = true; @@ -815,7 +815,7 @@ public override void Attack() { // EffectiveAtk respects Burn debuff (ATK reduction while burning) int damage = Attributes?.EffectiveAtk ?? 1; - _player.TakeDamage(damage); + _player.TakeDamage(damage, this); GD.Print($"[COMBAT] {EntityName} attacks Player for {damage} damage! Player HP: {_player.Attributes.HP.CurrentHP}/{_player.Attributes.HP.MaxHP}"); } } diff --git a/Scripts/Characters/Player.cs b/Scripts/Characters/Player.cs index 8bc5e6f..ee8b165 100644 --- a/Scripts/Characters/Player.cs +++ b/Scripts/Characters/Player.cs @@ -66,6 +66,7 @@ public class Player : Character // Invincibility state private int _damageCooldownFrames = 0; private float _respawnInvincibilityTimer = 0f; + private float _defenseStanceTimer = 0f; // Previously exposed properties now delegated to subsystems public int Experience => _inventorySystem?.Experience ?? 0; @@ -244,6 +245,13 @@ public int GetSelectedSkillIndex() public System.Collections.Generic.IReadOnlyList DiscardedItems => _inventorySystem?.Discarded?.Items.AsReadOnly() ?? new System.Collections.Generic.List().AsReadOnly(); + public void ActivateDefenseStance(float duration) + { + _defenseStanceTimer = duration; + _animationController?.PlayDefenseAnimation(); + GD.Print($"[Player] Defense Stance activated for {duration} seconds."); + } + /// /// Set the map reference and initialize room tracking /// @@ -289,14 +297,24 @@ public override void UpdateAttributes() Attributes.TotalVit = jobBonuses.Vit + equipmentBonuses.Vit; } - public override void TakeDamage(int damage) + public override void TakeDamage(int damage, Character source = null) { + if (_defenseStanceTimer > 0f) + { + if (source != null && source.Attributes?.HP?.IsAlive == true) + { + source.TakeDamage(Attributes.EffectiveAtk); + _animationController?.PlayDefenseCounterAnimation(0.2f); + } + return; + } + if (_respawnInvincibilityTimer > 0f) return; if (_damageCooldownFrames > 0) return; _damageCooldownFrames = 6; // 0.1 seconds at 60 FPS processing - base.TakeDamage(damage); + base.TakeDamage(damage, source); if (!_isDead && Attributes?.HP != null && Attributes.HP.IsAlive) { _animationController?.PlayHitAnimation(_hitTexture, 0.2f); @@ -352,8 +370,17 @@ public override void _PhysicsProcess(float delta) if (_map == null) return; + if (_defenseStanceTimer > 0f) + { + _defenseStanceTimer -= delta; + if (_defenseStanceTimer <= 0f) + { + _animationController?.StopDefenseAnimation(); + } + } + // Get current movement input - Vector2 movementInput = _inputHandler.GetMovementInput(); + Vector2 movementInput = _defenseStanceTimer > 0f ? Vector2.Zero : _inputHandler.GetMovementInput(); // 1. Handle physics and movement _physicsController.Update( @@ -475,7 +502,29 @@ public void ApplyProfile(PlayerProfileSnapshot snapshot) PlayerClass restoredClass = PlayerClassData.Deserialize(snapshot.ClassName); SetClass(restoredClass, rebuildSkills: false); - _combatSystem?.SetSkills(BuildSkillsFromSnapshot(snapshot.Skills, restoredClass)); + var newSkills = BuildSkillsFromSnapshot(snapshot.Skills, restoredClass); + + // Check if the new list of skills matches our current skills in ID and order exactly. + // If they match, keep the current live skill instances to avoid resetting their local cooldowns. + var currentSkills = _combatSystem?.CurrentSkills; + bool skillsMatch = false; + if (currentSkills != null && currentSkills.Count == newSkills.Count) + { + skillsMatch = true; + for (int i = 0; i < currentSkills.Count; i++) + { + if (ResolveSkillId(currentSkills[i]) != ResolveSkillId(newSkills[i])) + { + skillsMatch = false; + break; + } + } + } + + if (!skillsMatch) + { + _combatSystem?.SetSkills(newSkills); + } // Re-broadcast HP to refresh HUD after profile application. if (Attributes?.HP != null) @@ -560,33 +609,20 @@ private List BuildSkillsFromSnapshot(IReadOnlyList continue; } - if (string.Equals(snapshot.SkillId, "basic_attack", StringComparison.OrdinalIgnoreCase)) - { - var basicAttack = new BasicAttackSkill - { - EffectRenderer = new BasicAttackEffectRenderer(), - }; - skills.Add(basicAttack); - continue; - } - - if (string.Equals(snapshot.SkillId, "bow_attack", StringComparison.OrdinalIgnoreCase)) + var skill = CreateSkillFromId(snapshot.SkillId); + if (skill == null) { - skills.Add(new BowAttackSkill()); - continue; + // Fall back to remote skill mapping if it's a remote/external skill not recognized locally + skill = new RemoteSkill( + snapshot.SkillId, + snapshot.Name, + snapshot.CooldownSeconds); } - if (string.Equals(snapshot.SkillId, "fireball", StringComparison.OrdinalIgnoreCase)) + if (skill != null) { - skills.Add(new FireballSkill()); - continue; + skills.Add(skill); } - - var remoteSkill = new RemoteSkill( - snapshot.SkillId, - snapshot.Name, - snapshot.CooldownSeconds); - skills.Add(remoteSkill); } EnsureClassCoreSkills(skills, cls); @@ -627,6 +663,66 @@ private static string ResolveSkillId(Skills skill) return "fireball"; } + if (skill is TripleFireballSkill) + { + return "triple_fireball"; + } + + if (skill is GiantFireballSkill) + { + return "giant_fireball"; + } + + if (skill is TripleArrowSkill) + { + return "triple_arrow"; + } + + if (skill is RicochetArrowSkill) + { + return "ricochet_arrow"; + } + + if (skill is FlyingSwordSkill) + { + return "flying_sword"; + } + + if (skill is DefenseStanceSkill) + { + return "defense_stance"; + } + + if (skill is MagicSlashSkill) + { + return "magic_slash"; + } + + if (skill is IceSpearSkill) + { + return "ice_spear"; + } + + if (skill is DigitArrowSkill) + { + return "digit_arrow"; + } + + if (skill is SuperArrowSkill) + { + return "super_arrow"; + } + + if (skill is RoundhouseSlashSkill) + { + return "roundhouse_slash"; + } + + if (skill is KnightExploseSkill) + { + return "knight_explose"; + } + if (skill is RemoteSkill remoteSkill) { return remoteSkill.SkillId; @@ -697,28 +793,19 @@ private static void EnsureClassCoreSkills(List skills, PlayerClass cls) // Remove skills that are not allowed for this class skills.RemoveAll(s => !allowed.Contains(ResolveSkillId(s))); - // Ensure the class's primary skill exists - bool hasSword = skills.Any(s => s is BasicAttackSkill); - bool hasBow = skills.Any(s => s is BowAttackSkill); - bool hasFireball = skills.Any(s => s is FireballSkill); - - if (allowed.Contains(PlayerClassData.SkillIdSword) && !hasSword) + // If the player currently has no skills (newly initialized or class changed), + // populate with the class's default 3-skill loadout. + if (skills.Count == 0) { - var basicAttack = new BasicAttackSkill + var defaults = PlayerClassData.GetDefaultSkillLoadout(cls); + foreach (var skillId in defaults) { - EffectRenderer = new BasicAttackEffectRenderer(), - }; - skills.Insert(0, basicAttack); - } - - if (allowed.Contains(PlayerClassData.SkillIdBow) && !hasBow) - { - skills.Add(new BowAttackSkill()); - } - - if (allowed.Contains(PlayerClassData.SkillIdFireball) && !hasFireball) - { - skills.Add(new FireballSkill()); + var skill = CreateSkillFromId(skillId); + if (skill != null) + { + skills.Add(skill); + } + } } } @@ -753,10 +840,19 @@ private void ApplyClassSkills(PlayerClass cls) return; } - var current = _combatSystem.CurrentSkills.ToList(); - EnsureClassCoreSkills(current, cls); - _combatSystem.SetSkills(current); - GD.Print($"[Player] Skills rebuilt for class {cls}: {string.Join(", ", current.Select(ResolveSkillId))}"); + var defaultSkills = new List(); + var defaults = PlayerClassData.GetDefaultSkillLoadout(cls); + foreach (var skillId in defaults) + { + var skill = CreateSkillFromId(skillId); + if (skill != null) + { + defaultSkills.Add(skill); + } + } + + _combatSystem.SetSkills(defaultSkills); + GD.Print($"[Player] Skills rebuilt for class {cls}: {string.Join(", ", defaultSkills.Select(ResolveSkillId))}"); } /// @@ -811,10 +907,14 @@ string Resolve(string primary, string fb) => // Reload class-specific hit / dead textures (with shared fallback). string hitPath = Resolve(paths.HitFrame, fallback.HitFrame); string deadPath = Resolve(paths.DeadFrame, fallback.DeadFrame); + string defPath = Resolve(paths.DefenseFrame, fallback.DefenseFrame); + string atkPath = Resolve(paths.SkillAttackFrame, fallback.SkillAttackFrame); _hitTexture = GD.Load(hitPath); _deadTexture = GD.Load(deadPath); + _animationController?.LoadDefenseTextures(defPath, atkPath); + string bowFrame = paths.BowAttackPaths != null ? paths.BowAttackPaths[0] : "default"; GD.Print($"[Player] Sprites updated for class {cls}. Stand={stand1}, Attack={attack1}, Bow={bowFrame}, Hit={hitPath}, Dead={deadPath}"); } @@ -893,6 +993,11 @@ private void RestoreEquippedItems(Godot.Collections.Dictionary data) /// public bool IsAttacking => _animationController?.IsAttacking ?? false; + /// + /// Expose player's facing direction (1.0f for right, -1.0f for left). + /// + public float FacingDirection => _animationController?.GetFacingDirection() ?? 1f; + /// /// Update skill cooldowns (called by PhysicsController) /// @@ -920,6 +1025,133 @@ public void LearnSkill(Skills skill) _combatSystem?.LearnSkill(skill); } + /// + /// Replace the player's equipped skill list with the given ordered list of skill IDs, + /// enforcing class restrictions. Called from the skill-equip UI after the player + /// saves their loadout. Invalid / disallowed IDs are silently skipped. + /// + public void SetEquippedSkills(System.Collections.Generic.List orderedSkillIds) + { + if (_combatSystem == null) return; + + var builtSkills = BuildSkillsFromIds(orderedSkillIds, PlayerClass); + _combatSystem.SetSkills(builtSkills); + GD.Print($"[Player] Skill loadout updated: {string.Join(", ", orderedSkillIds)}"); + } + + /// + /// Returns the ordered list of skill IDs currently equipped by the player. + /// + public System.Collections.Generic.List GetEquippedSkillIds() + { + var ids = new System.Collections.Generic.List(); + var skills = _combatSystem?.CurrentSkills; + if (skills == null) return ids; + foreach (var s in skills) + { + if (s != null) ids.Add(ResolveSkillId(s)); + } + return ids; + } + + /// + /// Creates a Skills instance based on the unique skill ID string. + /// + private static Skills CreateSkillFromId(string key) + { + string cleanKey = (key ?? string.Empty).ToLowerInvariant(); + if (cleanKey == "basic_attack") + { + return new BasicAttackSkill { EffectRenderer = new BasicAttackEffectRenderer() }; + } + if (cleanKey == "bow_attack") + { + return new BowAttackSkill(); + } + if (cleanKey == "triple_arrow") + { + return new TripleArrowSkill(); + } + if (cleanKey == "ricochet_arrow") + { + return new RicochetArrowSkill(); + } + if (cleanKey == "fireball") + { + return new FireballSkill(); + } + if (cleanKey == "triple_fireball") + { + return new TripleFireballSkill(); + } + if (cleanKey == "giant_fireball") + { + return new GiantFireballSkill(); + } + if (cleanKey == "flying_sword") + { + return new FlyingSwordSkill(); + } + if (cleanKey == "defense_stance") + { + return new DefenseStanceSkill(); + } + if (cleanKey == "magic_slash") + { + return new MagicSlashSkill(); + } + if (cleanKey == "ice_spear") + { + return new IceSpearSkill(); + } + if (cleanKey == "digit_arrow") + { + return new DigitArrowSkill(); + } + if (cleanKey == "super_arrow") + { + return new SuperArrowSkill(); + } + if (cleanKey == "roundhouse_slash") + { + return new RoundhouseSlashSkill(); + } + if (cleanKey == "knight_explose") + { + return new KnightExploseSkill(); + } + return null; + } + + /// + /// Translates an ordered list of skill IDs into live skill instances, + /// filtering out any IDs not allowed for the given class. + /// + private static System.Collections.Generic.List BuildSkillsFromIds( + System.Collections.Generic.List orderedIds, + PlayerClass cls) + { + var allowed = PlayerClassData.GetAllowedSkillIds(cls); + var skills = new System.Collections.Generic.List(); + + if (orderedIds != null) + { + foreach (var id in orderedIds) + { + string key = (id ?? string.Empty).ToLowerInvariant(); + if (!allowed.Contains(key)) continue; + + var skill = CreateSkillFromId(key); + if (skill != null) + { + skills.Add(skill); + } + } + } + + return skills; + } + // ==================== Inventory System ==================== /// /// Gain experience points diff --git a/Scripts/Characters/PlayerSystems/PlayerAnimationController.cs b/Scripts/Characters/PlayerSystems/PlayerAnimationController.cs index bc8fa1d..45604ee 100644 --- a/Scripts/Characters/PlayerSystems/PlayerAnimationController.cs +++ b/Scripts/Characters/PlayerSystems/PlayerAnimationController.cs @@ -7,6 +7,8 @@ public enum AttackAnimationStyle Sword, Bow, Fireball, + ArcherShot, + KnightExplose, } /// @@ -27,6 +29,11 @@ public class PlayerAnimationController private Texture[] _swordAttackFrames; private Texture[] _bowAttackFrames; private Texture[] _fireballAttackFrames; + private readonly Texture[] _archerShotFrames; + private readonly Texture[] _knightExploseFrames; + + private Texture _defenseTexture; + private Texture _counterTexture; // Default (Adventurer) frame paths for each skill style. private static readonly string[] DefaultSwordPaths = new[] @@ -65,6 +72,16 @@ public PlayerAnimationController( _bowAttackFrames = BuildFrames(DefaultBowPaths[0], DefaultBowPaths[1], DefaultBowPaths[2]); _fireballAttackFrames = BuildFrames(DefaultFireballPaths[0], DefaultFireballPaths[1], DefaultFireballPaths[2]); + + _archerShotFrames = new[] + { + GD.Load("res://Assets/Characters/archer/shot.png"), + GD.Load("res://Assets/Characters/archer/shot1.png") + }; + _knightExploseFrames = new[] + { + GD.Load("res://Assets/Characters/warrior/super_attack.png") + }; } /// @@ -135,6 +152,14 @@ public void PlayAttackAnimation(AttackAnimationStyle style = AttackAnimationStyl { selectedFrames = _fireballAttackFrames; } + else if (style == AttackAnimationStyle.ArcherShot) + { + selectedFrames = _archerShotFrames; + } + else if (style == AttackAnimationStyle.KnightExplose) + { + selectedFrames = _knightExploseFrames; + } _animationSystem.SetAttackFrames(selectedFrames); _animationSystem.PlayAttackAnimation(); @@ -172,6 +197,29 @@ public void UpdateClassFrames(string[] swordPaths, string[] bowPaths, string[] f GD.Print("[PlayerAnimationController] Attack frames updated for class."); } + public void LoadDefenseTextures(string defensePath, string counterPath) + { + _defenseTexture = !string.IsNullOrEmpty(defensePath) ? GD.Load(defensePath) : null; + _counterTexture = !string.IsNullOrEmpty(counterPath) ? GD.Load(counterPath) : null; + } + + public void PlayDefenseAnimation() + { + if (_defenseTexture != null) + _animationSystem.PlayDefenseAnimation(_defenseTexture); + } + + public void PlayDefenseCounterAnimation(float duration = 0.2f) + { + if (_counterTexture != null) + _animationSystem.PlayDefenseCounterAnimation(_counterTexture, duration); + } + + public void StopDefenseAnimation() + { + _animationSystem.StopDefenseAnimation(); + } + /// /// Trigger dead animation playback /// diff --git a/Scripts/Characters/PlayerSystems/PlayerAnimationSystem.cs b/Scripts/Characters/PlayerSystems/PlayerAnimationSystem.cs index 40697e5..e2ddd22 100644 --- a/Scripts/Characters/PlayerSystems/PlayerAnimationSystem.cs +++ b/Scripts/Characters/PlayerSystems/PlayerAnimationSystem.cs @@ -11,7 +11,8 @@ public enum AnimationState Walking, // Moving around Attacking, // Performing basic attack Dead, // Character is dead - Hit // Character took damage + Hit, // Character took damage + Defending // Character is in defense stance } public class PlayerAnimationSystem @@ -37,6 +38,11 @@ public class PlayerAnimationSystem private Texture _hitTexture; private float _hitTimer = 0f; + // Defense animation + private Texture _defenseTexture; + private Texture _counterTexture; + private float _counterTimer = 0f; + private AnimationState _currentState = AnimationState.Idle; private int _frameIndex = 0; private float _animationTimer = 0f; @@ -190,6 +196,16 @@ public void UpdateAnimation(bool isMoving, float delta, float walkAnimationFps, } } + if (_currentState == AnimationState.Defending) + { + if (_counterTimer > 0f) + { + _counterTimer -= delta; + } + ApplyCurrentFrame(facingX); + return; + } + // Determine target state AnimationState targetState = isMoving ? AnimationState.Walking : AnimationState.Idle; @@ -311,6 +327,34 @@ public void Revive() } } + public void PlayDefenseAnimation(Texture defenseTexture) + { + if (_currentState == AnimationState.Dead) return; + + _currentState = AnimationState.Defending; + _defenseTexture = defenseTexture; + _counterTimer = 0f; + ApplyCurrentFrame(1f); + } + + public void PlayDefenseCounterAnimation(Texture counterTexture, float duration) + { + if (_currentState != AnimationState.Defending) return; + + _counterTexture = counterTexture; + _counterTimer = duration; + ApplyCurrentFrame(1f); + } + + public void StopDefenseAnimation() + { + if (_currentState == AnimationState.Defending) + { + _currentState = AnimationState.Idle; + ApplyCurrentFrame(1f); + } + } + /// /// Update attack animation and return true when finished /// @@ -380,6 +424,16 @@ private void ApplyCurrentFrame(float facingX) frameToUse = _hitTexture; } break; + case AnimationState.Defending: + if (_counterTimer > 0f && _counterTexture != null) + { + frameToUse = _counterTexture; + } + else if (_defenseTexture != null) + { + frameToUse = _defenseTexture; + } + break; } if (frameToUse != null) diff --git a/Scripts/Characters/PlayerSystems/PlayerCombatController.cs b/Scripts/Characters/PlayerSystems/PlayerCombatController.cs index 6d6bca0..7fb5eea 100644 --- a/Scripts/Characters/PlayerSystems/PlayerCombatController.cs +++ b/Scripts/Characters/PlayerSystems/PlayerCombatController.cs @@ -125,16 +125,26 @@ private bool ExecuteSkill(Player player, Map map, int skillIndex) private static AttackAnimationStyle GetAttackAnimationStyle(Skills skill) { - if (skill is BowAttackSkill) + if (skill is BowAttackSkill || skill is TripleArrowSkill || skill is RicochetArrowSkill) { return AttackAnimationStyle.Bow; } - if (skill is FireballSkill) + if (skill is DigitArrowSkill || skill is SuperArrowSkill) + { + return AttackAnimationStyle.ArcherShot; + } + + if (skill is FireballSkill || skill is TripleFireballSkill || skill is GiantFireballSkill || skill is IceSpearSkill || skill is MagicSlashSkill) { return AttackAnimationStyle.Fireball; } + if (skill is KnightExploseSkill) + { + return AttackAnimationStyle.KnightExplose; + } + return AttackAnimationStyle.Sword; } diff --git a/Scripts/Core/Data/PlayerClass.cs b/Scripts/Core/Data/PlayerClass.cs index e525491..b25182c 100644 --- a/Scripts/Core/Data/PlayerClass.cs +++ b/Scripts/Core/Data/PlayerClass.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; namespace QuestFantasy.Core.Data { @@ -11,7 +12,7 @@ public enum PlayerClass Adventurer, Mage, Archer, - Warrior + Knight } /// @@ -46,6 +47,9 @@ public class ClassSpritePaths // Death / knocked-down frame (optional) public string DeadFrame { get; set; } + public string DefenseFrame { get; set; } + public string SkillAttackFrame { get; set; } + /// /// Uniform visual scale multiplier applied on top of the body-size calculation. /// 1.0 = normal size, 0.8 = 20 % smaller, etc. @@ -54,6 +58,19 @@ public class ClassSpritePaths public float ScaleMultiplier { get; set; } = 1.0f; } + /// + /// Immutable descriptor for a single learnable skill. + /// Used by the skill-equip UI to build cards without coupling to concrete skill classes. + /// + public class SkillDefinition + { + public string Id { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public string Emoji { get; set; } + public float CooldownSec { get; set; } + } + /// /// Static registry of per-class data: /// - Which skill IDs are allowed @@ -86,6 +103,18 @@ public static class PlayerClassData public const string SkillIdSword = "basic_attack"; public const string SkillIdBow = "bow_attack"; public const string SkillIdFireball = "fireball"; + public const string SkillIdTripleFireball = "triple_fireball"; + public const string SkillIdGiantFireball = "giant_fireball"; + public const string SkillIdTripleArrow = "triple_arrow"; + public const string SkillIdRicochetArrow = "ricochet_arrow"; + public const string SkillIdFlyingSword = "flying_sword"; + public const string SkillIdDefenseStance = "defense_stance"; + public const string SkillIdMagicSlash = "magic_slash"; + public const string SkillIdIceSpear = "ice_spear"; + public const string SkillIdDigitArrow = "digit_arrow"; + public const string SkillIdSuperArrow = "super_arrow"; + public const string SkillIdRoundhouseSlash = "roundhouse_slash"; + public const string SkillIdKnightExplose = "knight_explose"; // ── Allowed skills per class ─────────────────────────────────────── @@ -96,17 +125,17 @@ public static class PlayerClassData private static readonly HashSet MageSkills = new HashSet { - SkillIdFireball + SkillIdFireball, SkillIdTripleFireball, SkillIdGiantFireball, SkillIdMagicSlash, SkillIdIceSpear }; private static readonly HashSet ArcherSkills = new HashSet { - SkillIdBow + SkillIdBow, SkillIdTripleArrow, SkillIdRicochetArrow, SkillIdDigitArrow, SkillIdSuperArrow }; - private static readonly HashSet WarriorSkills = new HashSet + private static readonly HashSet KnightSkills = new HashSet { - SkillIdSword + SkillIdSword, SkillIdFlyingSword, SkillIdDefenseStance, SkillIdRoundhouseSlash, SkillIdKnightExplose }; /// @@ -118,7 +147,7 @@ public static HashSet GetAllowedSkillIds(PlayerClass cls) { case PlayerClass.Mage: return MageSkills; case PlayerClass.Archer: return ArcherSkills; - case PlayerClass.Warrior: return WarriorSkills; + case PlayerClass.Knight: return KnightSkills; default: return AdventurerSkills; } } @@ -130,7 +159,7 @@ public static string GetDisplayName(PlayerClass cls) { case PlayerClass.Mage: return "Mage"; case PlayerClass.Archer: return "Archer"; - case PlayerClass.Warrior: return "Warrior"; + case PlayerClass.Knight: return "Knight"; default: return "Adventurer"; } } @@ -144,7 +173,7 @@ public static string GetDescription(PlayerClass cls) return "Wields the arcane arts.\nMasters the art of Fireball magic."; case PlayerClass.Archer: return "Swift and precise.\nStrikes enemies from a distance with arrows."; - case PlayerClass.Warrior: + case PlayerClass.Knight: return "Unyielding and fierce.\nCleaves foes with a powerful sword slash."; default: return "The all-rounder.\nCommands sword, bow, and magic freely."; @@ -158,11 +187,182 @@ public static string GetSkillListText(PlayerClass cls) { case PlayerClass.Mage: return "Fireball"; case PlayerClass.Archer: return "Arrow Shot"; - case PlayerClass.Warrior: return "Sword Slash"; + case PlayerClass.Knight: return "Sword Slash"; default: return "Sword Slash, Arrow Shot, Fireball"; } } + // ── Skill definition catalogue ───────────────────────────────────── + + private static readonly SkillDefinition DefSword = new SkillDefinition + { + Id = SkillIdSword, + DisplayName = "Sword Slash", + Description = "A powerful melee strike that hits nearby enemies.", + Emoji = "⚔️", + CooldownSec = 0.3f, + }; + + private static readonly SkillDefinition DefBow = new SkillDefinition + { + Id = SkillIdBow, + DisplayName = "Arrow Shot", + Description = "Fire an arrow that pierces enemies at range.", + Emoji = "🏹", + CooldownSec = 0.8f, + }; + + private static readonly SkillDefinition DefTripleArrow = new SkillDefinition + { + Id = SkillIdTripleArrow, + DisplayName = "Triple Arrow", + Description = "Fire 3 parallel arrows at once that pierce enemies.", + Emoji = "🏹", + CooldownSec = 1.5f, + }; + + private static readonly SkillDefinition DefRicochetArrow = new SkillDefinition + { + Id = SkillIdRicochetArrow, + DisplayName = "Ricochet Arrow", + Description = "Fire an arrow that bounces between enemies and walls.", + Emoji = "↪️", + CooldownSec = 2.5f, + }; + + private static readonly SkillDefinition DefFireball = new SkillDefinition + { + Id = SkillIdFireball, + DisplayName = "Fireball", + Description = "Launch a fireball that explodes on impact and may Burn.", + Emoji = "🔥", + CooldownSec = 1.2f, + }; + + private static readonly SkillDefinition DefTripleFireball = new SkillDefinition + { + Id = SkillIdTripleFireball, + DisplayName = "Triple Fireball", + Description = "Launch 3 fireballs in a spread that explode on impact.", + Emoji = "☄️", + CooldownSec = 2.0f, + }; + + private static readonly SkillDefinition DefGiantFireball = new SkillDefinition + { + Id = SkillIdGiantFireball, + DisplayName = "Giant Fireball", + Description = "Launch a massive fireball that explodes on impact.", + Emoji = "🔥", + CooldownSec = 3.0f, + }; + + private static readonly SkillDefinition DefFlyingSword = new SkillDefinition + { + Id = SkillIdFlyingSword, + DisplayName = "Flying Sword", + Description = "Throw a sword that pierces enemies and returns to you.", + Emoji = "🗡️", + CooldownSec = 1.5f, + }; + + private static readonly SkillDefinition DefDefenseStance = new SkillDefinition + { + Id = SkillIdDefenseStance, + DisplayName = "Defense Stance", + Description = "Block all damage and counter-attack enemies when hit.", + Emoji = "🛡️", + CooldownSec = 8.0f, + }; + + private static readonly SkillDefinition DefMagicSlash = new SkillDefinition + { + Id = SkillIdMagicSlash, + DisplayName = "Magic Slash", + Description = "Strike with arcane energy, cutting through magic.", + Emoji = "✨", + CooldownSec = 1.0f, + }; + + private static readonly SkillDefinition DefIceSpear = new SkillDefinition + { + Id = SkillIdIceSpear, + DisplayName = "Ice Spear", + Description = "Launch a spear of ice that pierces and freezes enemies.", + Emoji = "❄️", + CooldownSec = 1.8f, + }; + + private static readonly SkillDefinition DefDigitArrow = new SkillDefinition + { + Id = SkillIdDigitArrow, + DisplayName = "Digit Arrow", + Description = "Fire an arrow that passes through obstacles and enemies, traveling a set distance.", + Emoji = "🏹", + CooldownSec = 1.2f, + }; + + private static readonly SkillDefinition DefSuperArrow = new SkillDefinition + { + Id = SkillIdSuperArrow, + DisplayName = "Super Arrow", + Description = "Fire a powerful arrow that paralyzes enemies for 5 seconds.", + Emoji = "💫", + CooldownSec = 10.0f, + }; + + private static readonly SkillDefinition DefRoundhouseSlash = new SkillDefinition + { + Id = SkillIdRoundhouseSlash, + DisplayName = "Roundhouse Slash", + Description = "Spin and strike all nearby enemies with a powerful slash.", + Emoji = "⚡", + CooldownSec = 2.0f, + }; + + private static readonly SkillDefinition DefKnightExplose = new SkillDefinition + { + Id = SkillIdKnightExplose, + DisplayName = "Knight Explosion", + Description = "Create an expanding explosion at your location that damages enemies inside it repeatedly.", + Emoji = "💥", + CooldownSec = 30.0f, + }; + + /// + /// Returns the ordered list of all s that + /// the given class is permitted to equip. Used to build the skill-equip UI. + /// + public static ReadOnlyCollection GetAllSkillDefinitions(PlayerClass cls) + { + switch (cls) + { + case PlayerClass.Mage: + return new ReadOnlyCollection(new[] { DefFireball, DefTripleFireball, DefGiantFireball, DefMagicSlash, DefIceSpear }); + case PlayerClass.Archer: + return new ReadOnlyCollection(new[] { DefBow, DefTripleArrow, DefRicochetArrow, DefDigitArrow, DefSuperArrow }); + case PlayerClass.Knight: + return new ReadOnlyCollection(new[] { DefSword, DefFlyingSword, DefDefenseStance, DefRoundhouseSlash, DefKnightExplose }); + default: // Adventurer + return new ReadOnlyCollection(new[] { DefSword, DefBow, DefFireball }); + } + } + + /// + /// Returns the default ordered skill-ID loadout for . + /// Used to reset the loadout when the player changes class. + /// + public static ReadOnlyCollection GetDefaultSkillLoadout(PlayerClass cls) + { + switch (cls) + { + case PlayerClass.Mage: return new ReadOnlyCollection(new[] { SkillIdFireball, SkillIdMagicSlash, SkillIdIceSpear }); + case PlayerClass.Archer: return new ReadOnlyCollection(new[] { SkillIdBow, SkillIdDigitArrow, SkillIdSuperArrow }); + case PlayerClass.Knight: return new ReadOnlyCollection(new[] { SkillIdSword, SkillIdRoundhouseSlash, SkillIdKnightExplose }); + default: return new ReadOnlyCollection(new[] { SkillIdSword, SkillIdBow, SkillIdFireball }); + } + } + /// /// Returns the explicit sprite paths for each class. /// Paths that don't exist on disk are handled gracefully in @@ -215,7 +415,7 @@ public static ClassSpritePaths GetSpritePaths(PlayerClass cls) }; } - case PlayerClass.Warrior: + case PlayerClass.Knight: { const string W = "res://Assets/Characters/warrior/"; return new ClassSpritePaths @@ -233,6 +433,8 @@ public static ClassSpritePaths GetSpritePaths(PlayerClass cls) FireballAttackPaths = null, HitFrame = W + "hit.png", DeadFrame = W + "skill_down.png", // knocked-down sprite + DefenseFrame = W + "defense.png", + SkillAttackFrame = W + "skill_attack.png", }; } @@ -271,7 +473,7 @@ public static string Serialize(PlayerClass cls) { case PlayerClass.Mage: return "mage"; case PlayerClass.Archer: return "archer"; - case PlayerClass.Warrior: return "warrior"; + case PlayerClass.Knight: return "knight"; default: return "adventurer"; } } @@ -286,7 +488,8 @@ public static PlayerClass Deserialize(string value) { case "mage": return PlayerClass.Mage; case "archer": return PlayerClass.Archer; - case "warrior": return PlayerClass.Warrior; + case "knight": + case "warrior": return PlayerClass.Knight; default: return PlayerClass.Adventurer; } } diff --git a/Scripts/Core/Data/Skills/BasicAttackSkill.cs b/Scripts/Core/Data/Skills/BasicAttackSkill.cs index 1e2edbb..5ef6bbc 100644 --- a/Scripts/Core/Data/Skills/BasicAttackSkill.cs +++ b/Scripts/Core/Data/Skills/BasicAttackSkill.cs @@ -2,6 +2,7 @@ using QuestFantasy.Characters; using QuestFantasy.Core.Base; +using QuestFantasy.Core.Data; using QuestFantasy.Core.Data.Attributes; using QuestFantasy.Core.Systems.StatusEffects; @@ -50,6 +51,29 @@ public override void Effect(Player player, Character target) return; } + // Determine aim direction + Vector2 targetPos = target != null ? target.GlobalPosition : player.GetGlobalMousePosition(); + Vector2 towardTarget = targetPos - player.GlobalPosition; + Vector2 dir = towardTarget.LengthSquared() > 0.0001f ? towardTarget.Normalized() : Vector2.Right; + Vector2 effectPos = player.GlobalPosition + dir * 30f; + effectPos.y -= 8f; + + // Spawn standard slash effect only for Knight (Sword Slash) + if (player.PlayerClass == PlayerClass.Knight) + { + SkillProjectileSpawner.SpawnTemporaryVisualEffect( + player, + "res://Assets/SkillAnimation/slash-effect.png", + effectPos, + 0.25f, + 0.25f, + 0.3f, + 0f, + false, + dir.Angle() + ); + } + // Allow empty swing if no target if (target == null) { diff --git a/Scripts/Core/Data/Skills/DefenseStanceSkill.cs b/Scripts/Core/Data/Skills/DefenseStanceSkill.cs new file mode 100644 index 0000000..0c3a497 --- /dev/null +++ b/Scripts/Core/Data/Skills/DefenseStanceSkill.cs @@ -0,0 +1,27 @@ +using Godot; + +using QuestFantasy.Characters; + +namespace QuestFantasy.Core.Data.Skills +{ + public class DefenseStanceSkill : Attributes.Skills + { + public DefenseStanceSkill() + { + Name = "Defense Stance"; + Description = "Become invulnerable for 3 seconds. Any attack received triggers a counter-attack."; + } + + public override float MaxRange => 0f; + + public override float GetCooldownDuration() => 8.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) return; + + // Activate defense stance for 3 seconds + player.ActivateDefenseStance(3.0f); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/DigitArrowSkill.cs b/Scripts/Core/Data/Skills/DigitArrowSkill.cs new file mode 100644 index 0000000..dadafde --- /dev/null +++ b/Scripts/Core/Data/Skills/DigitArrowSkill.cs @@ -0,0 +1,35 @@ +using Godot; + +using QuestFantasy.Characters; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Digit arrow skill for the Archer. + /// Fires an arrow that passes through obstacles and enemies, + /// traveling a set distance without being blocked. + /// + public class DigitArrowSkill : Attributes.Skills + { + public DigitArrowSkill() + { + Name = "Digit Arrow"; + Description = "Fire an arrow that passes through obstacles and enemies, traveling a set distance."; + } + + public override float MaxRange => 400f; + + public override float GetCooldownDuration() => 1.2f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Spawn a digit arrow that passes through obstacles + SkillProjectileSpawner.SpawnDigitArrow(player, target, MaxRange); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/FlyingSwordSkill.cs b/Scripts/Core/Data/Skills/FlyingSwordSkill.cs new file mode 100644 index 0000000..2a52dd8 --- /dev/null +++ b/Scripts/Core/Data/Skills/FlyingSwordSkill.cs @@ -0,0 +1,38 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// A returning projectile skill for the Warrior. + /// Throws a sword that pierces through enemies, reaches max range, + /// and then flies back to the player, damaging enemies again. + /// + public class FlyingSwordSkill : Attributes.Skills + { + public FlyingSwordSkill() + { + Name = "Flying Sword"; + Description = "Throw a sword that pierces enemies and returns to you."; + } + + public override float MaxRange => 250f; + + public override float GetCooldownDuration() => 1.5f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + SkillProjectileSpawner.SpawnFlyingSword( + player, + target, + MaxRange); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/GiantFireballSkill.cs b/Scripts/Core/Data/Skills/GiantFireballSkill.cs new file mode 100644 index 0000000..0199ac0 --- /dev/null +++ b/Scripts/Core/Data/Skills/GiantFireballSkill.cs @@ -0,0 +1,42 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// A massive fireball projectile exclusive to the Mage. + /// Explodes on impact with a large AOE and high damage. + /// Has a higher chance to apply Burn on each enemy hit. + /// + public class GiantFireballSkill : Attributes.Skills + { + public GiantFireballSkill() + { + Name = "Giant Fireball"; + Description = "Launch a massive fireball that explodes on impact."; + } + + public override float MaxRange => 400f; + + public override float GetCooldownDuration() => 3.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + SkillProjectileSpawner.SpawnGiantFireball( + player, + target, + MaxRange, + onHitEffect: () => new BurnEffect( + GameConstants.FIREBALL_BURN_DURATION * 1.5f, + GameConstants.FIREBALL_BURN_DPS * 2), + onHitChance: GameConstants.FIREBALL_BURN_CHANCE * 1.5f); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/IceSpearSkill.cs b/Scripts/Core/Data/Skills/IceSpearSkill.cs new file mode 100644 index 0000000..0d9bf7a --- /dev/null +++ b/Scripts/Core/Data/Skills/IceSpearSkill.cs @@ -0,0 +1,40 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Ice spear skill for the Mage. + /// Launches a spear of ice that pierces and freezes enemies. + /// + public class IceSpearSkill : Attributes.Skills + { + public IceSpearSkill() + { + Name = "Ice Spear"; + Description = "Launch a spear of ice that pierces and freezes enemies."; + } + + public override float MaxRange => 280f; + + public override float GetCooldownDuration() => 1.8f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Spawn an ice spear projectile + SkillProjectileSpawner.SpawnIceSpear( + player, + target, + MaxRange, + onHitEffect: () => new FreezeEffect(1.0f), // 1 second freeze + onHitChance: 1.0f); // Always freeze + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/KnightExploseSkill.cs b/Scripts/Core/Data/Skills/KnightExploseSkill.cs new file mode 100644 index 0000000..78c6b10 --- /dev/null +++ b/Scripts/Core/Data/Skills/KnightExploseSkill.cs @@ -0,0 +1,35 @@ +using Godot; + +using QuestFantasy.Characters; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Knight explosion skill for the Warrior. + /// Creates an expanding explosion at the player's location that continuously + /// damages enemies within its expanding radius. + /// + public class KnightExploseSkill : Attributes.Skills + { + public KnightExploseSkill() + { + Name = "Knight Explosion"; + Description = "Create an expanding explosion at your location that damages enemies inside it repeatedly."; + } + + public override float MaxRange => 0f; // Centered on player + + public override float GetCooldownDuration() => 30.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Spawn knight explosion at player location + SkillProjectileSpawner.SpawnKnightExplosion(player); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/MagicSlashSkill.cs b/Scripts/Core/Data/Skills/MagicSlashSkill.cs new file mode 100644 index 0000000..fed8967 --- /dev/null +++ b/Scripts/Core/Data/Skills/MagicSlashSkill.cs @@ -0,0 +1,83 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Base; +using QuestFantasy.Core.Data.Attributes; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Magic slash skill for the Mage. + /// Strikes with arcane energy, hitting nearby enemies. + /// + public class MagicSlashSkill : Attributes.Skills + { + private const float DAMAGE_VARIANCE_MIN = 0.9f; + private const float DAMAGE_VARIANCE_MAX = 1.1f; + private const float DAMAGE_REDUCTION_FACTOR = 0.5f; + + public MagicSlashSkill() + { + Name = "Magic Slash"; + Description = "Strike with arcane energy, cutting through magic."; + } + + public override float MaxRange => 100f; + + public override float GetCooldownDuration() => 1.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Determine aim direction (mouse position or target position) + Vector2 targetPos = target != null ? target.GlobalPosition : player.GetGlobalMousePosition(); + Vector2 towardTarget = targetPos - player.GlobalPosition; + Vector2 dir = towardTarget.LengthSquared() > 0.0001f ? towardTarget.Normalized() : Vector2.Right; + + // Spawn visual slash effect 36 pixels in front of the player (offset vertically slightly) + Vector2 effectPos = player.GlobalPosition + dir * 36f; + effectPos.y -= 8f; + + // Spawn visual slash effect, rotated to face aim direction + SkillProjectileSpawner.SpawnTemporaryVisualEffect( + player, + "res://Assets/SkillAnimation/magic_slash-2.png", + effectPos, + 0.3f, + 0.3f, + 0.35f, + 0f, + false, + dir.Angle() + ); + + // Get attacker stats + int attackerAtk = player.Attributes?.EffectiveAtk ?? 1; + int defenderDef = 0; + + // Damage to each enemy + float variance = GD.Randf() * (DAMAGE_VARIANCE_MAX - DAMAGE_VARIANCE_MIN) + DAMAGE_VARIANCE_MIN; + + // If we have a specific target, damage them more + if (target != null) + { + defenderDef = target.Attributes?.EffectiveDef ?? 0; + int baseDamage = Mathf.Max(1, attackerAtk - Mathf.FloorToInt(defenderDef * DAMAGE_REDUCTION_FACTOR)); + int finalDamage = Mathf.RoundToInt(baseDamage * variance); + + target.TakeDamage(finalDamage); + GD.Print($"[COMBAT] {player.EntityName} casts Magic Slash on {target.EntityName} for {finalDamage} damage!"); + } + else + { + // Empty cast + GD.Print("[MagicSlashSkill] Empty cast - no targets in range"); + } + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/RicochetArrowSkill.cs b/Scripts/Core/Data/Skills/RicochetArrowSkill.cs new file mode 100644 index 0000000..e7a16e8 --- /dev/null +++ b/Scripts/Core/Data/Skills/RicochetArrowSkill.cs @@ -0,0 +1,38 @@ +using Godot; + +using QuestFantasy.Characters; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Advanced ricochet arrow projectile for the Archer. + /// Fires a single arrow that bounces between walls and enemies up to 4 times. + /// + public class RicochetArrowSkill : Attributes.Skills + { + public RicochetArrowSkill() + { + Name = "Ricochet Arrow"; + Description = "Fire an arrow that bounces between walls and enemies."; + } + + public override float MaxRange => 300f; + + public override float GetCooldownDuration() => 2.5f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Spawn an arrow with 4 bounces + SkillProjectileSpawner.SpawnRicochetArrow( + player, + target, + MaxRange, + bounces: 4); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/RoundhouseSlashSkill.cs b/Scripts/Core/Data/Skills/RoundhouseSlashSkill.cs new file mode 100644 index 0000000..5f10fdc --- /dev/null +++ b/Scripts/Core/Data/Skills/RoundhouseSlashSkill.cs @@ -0,0 +1,92 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Base; +using QuestFantasy.Core.Data.Attributes; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Roundhouse slash skill for the Warrior. + /// A spinning attack that hits all nearby enemies. + /// + public class RoundhouseSlashSkill : Attributes.Skills + { + private const float DAMAGE_VARIANCE_MIN = 0.85f; + private const float DAMAGE_VARIANCE_MAX = 1.15f; + private const float DAMAGE_REDUCTION_FACTOR = 0.5f; + + public RoundhouseSlashSkill() + { + Name = "Roundhouse Slash"; + Description = "Spin and strike all nearby enemies with a powerful slash."; + } + + public override float MaxRange => 80f; + + public override float GetCooldownDuration() => 2.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + int attackerAtk = player.Attributes?.EffectiveAtk ?? 1; + + // Spawn spinning roundhouse slash visual effect + SkillProjectileSpawner.SpawnTemporaryVisualEffect( + player, + "res://Assets/SkillAnimation/roundhouse-slash.png", + player.GlobalPosition, + 0.3f, + 0.4f, + 0.5f, + 15f + ); + + // Get all Character objects in the scene tree and damage those in range + var root = player.GetTree()?.Root; + if (root != null) + { + var characters = GetAllNodesOfType(root); + foreach (var enemy in characters) + { + if (enemy == null || enemy == player || enemy.Attributes?.HP?.IsAlive != true) + { + continue; + } + + float distance = player.GlobalPosition.DistanceTo(enemy.GlobalPosition); + if (distance <= MaxRange) + { + int defenderDef = enemy.Attributes?.EffectiveDef ?? 0; + int baseDamage = Mathf.Max(1, attackerAtk - Mathf.FloorToInt(defenderDef * DAMAGE_REDUCTION_FACTOR)); + float variance = GD.Randf() * (DAMAGE_VARIANCE_MAX - DAMAGE_VARIANCE_MIN) + DAMAGE_VARIANCE_MIN; + int finalDamage = Mathf.RoundToInt(baseDamage * variance); + + enemy.TakeDamage(finalDamage); + GD.Print($"[COMBAT] {player.EntityName} performs Roundhouse Slash on {enemy.EntityName} for {finalDamage} damage!"); + } + } + } + } + + private System.Collections.Generic.List GetAllNodesOfType(Node root) where T : Node + { + var result = new System.Collections.Generic.List(); + if (root is T tNode) + { + result.Add(tNode); + } + + foreach (Node child in root.GetChildren()) + { + result.AddRange(GetAllNodesOfType(child)); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/SkillProjectileSpawner.cs b/Scripts/Core/Data/Skills/SkillProjectileSpawner.cs index 311c33d..5fc155d 100644 --- a/Scripts/Core/Data/Skills/SkillProjectileSpawner.cs +++ b/Scripts/Core/Data/Skills/SkillProjectileSpawner.cs @@ -18,10 +18,35 @@ public static void SpawnArrow( float onHitChance = 0f) { Vector2 direction = ResolveDirection(player, target); - var node = SkillProjectileNode.CreateArrow(player, direction, maxRange, onHitEffect, onHitChance); + var node = SkillProjectileNode.CreateArrow(player, direction, maxRange, 0, onHitEffect, onHitChance); AttachToScene(player, node); } + public static void SpawnTripleArrow( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + + // Calculate a perpendicular offset vector (10 pixels wide) to separate the arrows + Vector2 perpOffset = new Vector2(-direction.y, direction.x) * 12f; + + var nodeCenter = SkillProjectileNode.CreateArrow(player, direction, maxRange, 0, onHitEffect, onHitChance); + var nodeLeft = SkillProjectileNode.CreateArrow(player, direction, maxRange, 0, onHitEffect, onHitChance); + var nodeRight = SkillProjectileNode.CreateArrow(player, direction, maxRange, 0, onHitEffect, onHitChance); + + AttachToScene(player, nodeCenter); + AttachToScene(player, nodeLeft); + AttachToScene(player, nodeRight); + + // Apply the visual offsets + nodeLeft.GlobalPosition += perpOffset; + nodeRight.GlobalPosition -= perpOffset; + } + public static void SpawnFireball( Player player, Character target, @@ -34,6 +59,134 @@ public static void SpawnFireball( AttachToScene(player, node); } + public static void SpawnTripleFireball( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + + // Calculate spread directions (±15 degrees = ±0.261799 radians) + float spreadAngle = 15f * Mathf.Pi / 180f; + Vector2 dirLeft = direction.Rotated(-spreadAngle); + Vector2 dirRight = direction.Rotated(spreadAngle); + + var nodeCenter = SkillProjectileNode.CreateFireball(player, direction, maxRange, onHitEffect, onHitChance); + var nodeLeft = SkillProjectileNode.CreateFireball(player, dirLeft, maxRange, onHitEffect, onHitChance); + var nodeRight = SkillProjectileNode.CreateFireball(player, dirRight, maxRange, onHitEffect, onHitChance); + + AttachToScene(player, nodeCenter); + AttachToScene(player, nodeLeft); + AttachToScene(player, nodeRight); + } + + public static void SpawnGiantFireball( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateGiantFireball(player, direction, maxRange, onHitEffect, onHitChance); + AttachToScene(player, node); + } + + public static void SpawnFlyingSword( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateFlyingSword(player, direction, maxRange, onHitEffect, onHitChance); + AttachToScene(player, node); + } + + public static void SpawnRicochetArrow( + Player player, + Character target, + float maxRange, + int bounces, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateArrow(player, direction, maxRange, bounces, onHitEffect, onHitChance); + AttachToScene(player, node); + } + + public static void SpawnKnightExplosion(Player player) + { + if (player == null) + { + return; + } + + var node = new KnightExplosionNode(player); + AttachToScene(player, node); + node.GlobalPosition = player.GlobalPosition; + + GD.Print($"[KnightExplosion] Spawned at {player.GlobalPosition}"); + } + + public static void SpawnDigitArrow(Player player, Character target, float maxRange) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateDigitArrow(player, direction, maxRange); + AttachToScene(player, node); + } + + public static void SpawnSuperArrow( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateSuperArrow(player, direction, maxRange, onHitEffect, onHitChance); + AttachToScene(player, node); + } + + public static void SpawnIceSpear( + Player player, + Character target, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateIceSpear(player, direction, maxRange, onHitEffect, onHitChance); + AttachToScene(player, node); + } + + public static void SpawnMagicSlash(Player player, Character target, float maxRange) + { + Vector2 direction = ResolveDirection(player, target); + var node = SkillProjectileNode.CreateMagicSlash(player, direction, maxRange); + AttachToScene(player, node); + } + + public static void SpawnTemporaryVisualEffect( + Player player, + string texturePath, + Vector2 position, + float duration, + float initialScale, + float targetScale = -1f, + float rotationSpeed = 0f, + bool flipH = false, + float initialRotation = 0f) + { + var node = new TemporaryVisualEffectNode(texturePath, duration, initialScale, targetScale, rotationSpeed, flipH, initialRotation); + AttachToScene(player, node); + node.GlobalPosition = position; + } + private static Vector2 ResolveDirection(Player player, Character target) { if (target != null && Godot.Object.IsInstanceValid(target)) @@ -54,7 +207,7 @@ private static Vector2 ResolveDirection(Player player, Character target) return Vector2.Right; } - private static void AttachToScene(Player player, SkillProjectileNode node) + private static void AttachToScene(Player player, Node2D node) { if (player == null || !Godot.Object.IsInstanceValid(player)) { @@ -115,14 +268,27 @@ internal class SkillProjectileNode : Node2D private int _flightFrameIndex = -1; private float _flightFrameDuration = 0f; private float _impactFrameDuration = ArrowImpactFrameDuration; + private int _bouncesRemaining = 0; + private readonly float _bounceRange = 150f; + private bool _isPiercing = false; + private bool _isReturning = false; + private bool _returningPhase = false; + private bool _ignoreWalls = false; + private bool _loopFlightAnimation = true; + private Character _lastHitTarget = null; public static SkillProjectileNode CreateArrow( Player owner, Vector2 direction, float maxRange, + int bounces = 0, Func onHitEffect = null, float onHitChance = 0f) { + string arrowTexturePath = bounces > 0 + ? "res://Assets/SkillAnimation/arrow_ricochet.png" + : "res://Assets/SkillAnimation/arrow.png"; + return new SkillProjectileNode { _owner = owner, @@ -135,18 +301,19 @@ public static SkillProjectileNode CreateArrow( _damageMax = 4, _isAoe = false, _aoeRadius = 0f, + _bouncesRemaining = bounces, _onHitEffect = onHitEffect, _onHitChance = onHitChance, - _projectileTexture = GD.Load("res://Assets/SkillAnimation/arrow.png"), + _projectileTexture = GD.Load(arrowTexturePath), _projectileScale = ArrowProjectileScale, _impactScale = ArrowImpactScale, _flightFrames = new[] { - GD.Load("res://Assets/SkillAnimation/arrow.png"), + GD.Load(arrowTexturePath), }, _impactFrames = new[] { - GD.Load("res://Assets/SkillAnimation/arrow.png"), + GD.Load(arrowTexturePath), } }; } @@ -193,6 +360,231 @@ public static SkillProjectileNode CreateFireball( }; } + public static SkillProjectileNode CreateGiantFireball( + Player owner, + Vector2 direction, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = FireballSpeed * 0.8f, // Slightly slower because it's massive + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 24f, + _damageMin = 10, + _damageMax = 18, + _isAoe = true, + _aoeRadius = 80f, + _onHitEffect = onHitEffect, + _onHitChance = onHitChance, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/fireball.png"), + _projectileScale = FireballProjectileScale * 2f, + _impactScale = FireballImpactScale * 2f, + _flightFrameDuration = FireballFlightFrameDuration, + _impactFrameDuration = FireballImpactFrameDuration, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/fireball1.png"), + GD.Load("res://Assets/SkillAnimation/fireball2.png"), + GD.Load("res://Assets/SkillAnimation/fireball3.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/fireball_hit.png"), + GD.Load("res://Assets/SkillAnimation/fireball_hit1.png"), + GD.Load("res://Assets/SkillAnimation/fireball_hit2.png"), + GD.Load("res://Assets/SkillAnimation/fireball_hit3.png"), + } + }; + } + + public static SkillProjectileNode CreateFlyingSword( + Player owner, + Vector2 direction, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = ArrowSpeed * 0.9f, + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 16f, + _damageMin = 6, + _damageMax = 12, + _isAoe = false, + _aoeRadius = 0f, + _isPiercing = true, + _isReturning = true, + _onHitEffect = onHitEffect, + _onHitChance = onHitChance, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/fly_sword.png"), + _projectileScale = 0.3f, + _impactScale = 0.3f, + _flightFrameDuration = 0.08f, + _impactFrameDuration = ArrowImpactFrameDuration, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/fly_sword.png"), + GD.Load("res://Assets/SkillAnimation/fly_sword-1.png"), + GD.Load("res://Assets/SkillAnimation/fly_sword-2.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/fly_sword.png"), + } + }; + } + + public static SkillProjectileNode CreateDigitArrow( + Player owner, + Vector2 direction, + float maxRange) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = ArrowSpeed * 1.3f, + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 12f, + _damageMin = 4, + _damageMax = 7, + _isAoe = false, + _isPiercing = true, + _ignoreWalls = true, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/digit_arrow.png"), + _projectileScale = 0.25f, + _impactScale = 0.25f, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/digit_arrow.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/digit_arrow.png"), + } + }; + } + + public static SkillProjectileNode CreateSuperArrow( + Player owner, + Vector2 direction, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = ArrowSpeed * 0.75f, + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 28f, + _damageMin = 8, + _damageMax = 15, + _isAoe = false, + _ignoreWalls = true, + _isPiercing = true, + _onHitEffect = onHitEffect, + _onHitChance = onHitChance, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/super_arrow.png"), + _projectileScale = 0.55f, + _impactScale = 0.55f, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/super_arrow.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/super_arrow.png"), + } + }; + } + + public static SkillProjectileNode CreateIceSpear( + Player owner, + Vector2 direction, + float maxRange, + Func onHitEffect = null, + float onHitChance = 0f) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = ArrowSpeed * 0.6f, + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 15f, + _damageMin = 5, + _damageMax = 9, + _isAoe = false, + _onHitEffect = onHitEffect, + _onHitChance = onHitChance, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/ice_spear-1.png"), + _projectileScale = 0.3f, + _impactScale = 0.35f, + _flightFrameDuration = 0.1f, + _impactFrameDuration = 0.08f, + _loopFlightAnimation = false, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/ice_spear-1.png"), + GD.Load("res://Assets/SkillAnimation/ice_spear-2.png"), + GD.Load("res://Assets/SkillAnimation/ice_spear-3.png"), + GD.Load("res://Assets/SkillAnimation/ice_spear-4.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/ice_spear-hit.png"), + } + }; + } + + public static SkillProjectileNode CreateMagicSlash( + Player owner, + Vector2 direction, + float maxRange) + { + return new SkillProjectileNode + { + _owner = owner, + _map = FindMap(owner), + _direction = direction, + _speed = ArrowSpeed * 1.2f, + _maxDistance = Mathf.Max(10f, maxRange), + _hitRadius = 20f, + _damageMin = 4, + _damageMax = 8, + _isAoe = false, + _onHitEffect = null, + _onHitChance = 0f, + _projectileTexture = GD.Load("res://Assets/SkillAnimation/magic_slash-2.png"), + _projectileScale = 0.3f, + _impactScale = 0.3f, + _flightFrameDuration = 0f, + _impactFrameDuration = ArrowImpactFrameDuration, + _flightFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/magic_slash-2.png"), + }, + _impactFrames = new[] + { + GD.Load("res://Assets/SkillAnimation/magic_slash-2.png"), + } + }; + } + public override void _Ready() { Texture firstFlightTexture = _projectileTexture; @@ -234,30 +626,116 @@ public override void _Process(float delta) UpdateFlightAnimation(delta); + if (_returningPhase) + { + Vector2 toOwner = _owner.GlobalPosition - GlobalPosition; + if (toOwner.LengthSquared() < 400f) // approx 20 units + { + QueueFree(); + return; + } + _direction = toOwner.Normalized(); + Rotation = _direction.Angle(); + } + Vector2 step = _direction * _speed * delta; Vector2 nextPosition = GlobalPosition + step; - if (IsBlockedByWall(nextPosition)) + if (IsBlockedByWall(nextPosition) && !_returningPhase) { - BeginImpact(nextPosition, null); - return; + if (_ignoreWalls) + { + // Do not block - pierce the wall/obstacle! + } + else if (_bouncesRemaining > 0) + { + _bouncesRemaining--; + + // Simple reflection based on axis + bool xBlocked = IsBlockedByWall(GlobalPosition + new Vector2(step.x, 0)); + bool yBlocked = IsBlockedByWall(GlobalPosition + new Vector2(0, step.y)); + + if (xBlocked) _direction.x *= -1; + if (yBlocked) _direction.y *= -1; + + // Corner case: if both or neither (diagonal corner), just flip both + if (!xBlocked && !yBlocked) { _direction = -_direction; } + + _direction = _direction.Normalized(); + Rotation = _direction.Angle(); + return; // continue next frame + } + else + { + if (_isReturning) + { + StartReturningPhase(); + return; + } + BeginImpact(nextPosition, null); + return; + } } Character hitTarget = FindHitTarget(nextPosition); if (hitTarget != null) { - BeginImpact(hitTarget.GlobalPosition, hitTarget); - return; + if (_bouncesRemaining > 0) + { + ApplyDamage(hitTarget); + _bouncesRemaining--; + + // Find next target + Character nextTarget = FindNextBounceTarget(hitTarget.GlobalPosition); + if (nextTarget != null) + { + _direction = (nextTarget.GlobalPosition - GlobalPosition).Normalized(); + Rotation = _direction.Angle(); + return; // continue next frame + } + else + { + // No target to bounce to, just continue flying in current direction so it can bounce off walls later! + return; + } + } + else if (_isPiercing) + { + ApplyDamage(hitTarget); + } + else + { + BeginImpact(hitTarget.GlobalPosition, hitTarget); + return; + } } GlobalPosition = nextPosition; - _traveled += step.Length(); - if (_traveled >= _maxDistance) + + if (!_returningPhase) { - QueueFree(); + _traveled += step.Length(); + if (_traveled >= _maxDistance) + { + if (_isReturning) + { + StartReturningPhase(); + } + else + { + QueueFree(); + } + } } } + private void StartReturningPhase() + { + _returningPhase = true; + _damagedTargets.Clear(); + _isPiercing = true; + } + private bool IsBlockedByWall(Vector2 position) { if (_map == null || !Godot.Object.IsInstanceValid(_map)) @@ -283,6 +761,22 @@ private Character FindHitTarget(Vector2 position) continue; } + // For bouncing projectiles (ricochet arrow), we only prevent hitting the immediate last hit target consecutively + if (_bouncesRemaining > 0) + { + if (enemy == _lastHitTarget) + { + continue; + } + } + else + { + if (_damagedTargets.Contains(enemy)) + { + continue; + } + } + if (enemy.GlobalPosition.DistanceTo(position) <= _hitRadius) { return enemy; @@ -292,6 +786,40 @@ private Character FindHitTarget(Vector2 position) return null; } + private Character FindNextBounceTarget(Vector2 currentPosition) + { + if (!IsOwnerAlive()) + { + return null; + } + + Character bestTarget = null; + float closestDistSq = _bounceRange * _bounceRange; + + foreach (Character enemy in EnumerateEnemyCharacters()) + { + if (enemy?.Attributes?.HP == null || !enemy.Attributes.HP.IsAlive) + { + continue; + } + + // Do not bounce back to the immediate last hit target consecutively + if (enemy == _lastHitTarget) + { + continue; + } + + float distSq = enemy.GlobalPosition.DistanceSquaredTo(currentPosition); + if (distSq <= closestDistSq) + { + closestDistSq = distSq; + bestTarget = enemy; + } + } + + return bestTarget; + } + private void BeginImpact(Vector2 impactPosition, Character directHitTarget) { GlobalPosition = impactPosition; @@ -331,7 +859,22 @@ private void UpdateFlightAnimation(float delta) } _flightFrameTimer = 0f; - _flightFrameIndex = (_flightFrameIndex + 1) % _flightFrames.Length; + int nextIndex = _flightFrameIndex + 1; + if (nextIndex >= _flightFrames.Length) + { + if (_loopFlightAnimation) + { + _flightFrameIndex = 0; + } + else + { + _flightFrameIndex = _flightFrames.Length - 1; // Hold on the last frame + } + } + else + { + _flightFrameIndex = nextIndex; + } _sprite.Texture = _flightFrames[_flightFrameIndex] ?? _sprite.Texture; } @@ -386,9 +929,20 @@ private void ApplyDamage(Character target) return; } - if (_damagedTargets.Contains(target)) + // For bouncing projectiles, allow re-damaging targets on subsequent bounces, but not consecutively + if (_bouncesRemaining > 0) { - return; + if (target == _lastHitTarget) + { + return; + } + } + else + { + if (_damagedTargets.Contains(target)) + { + return; + } } // Use effective stats so Burn (ATK debuff) and Bleed (DEF debuff) are reflected @@ -399,6 +953,7 @@ private void ApplyDamage(Character target) target.TakeDamage(damage); _damagedTargets.Add(target); + _lastHitTarget = target; GD.Print($"[COMBAT] {_owner.EntityName} hit {target.EntityName} with projectile for {damage}. HP={target.Attributes.HP.CurrentHP}/{target.Attributes.HP.MaxHP}"); @@ -478,4 +1033,196 @@ private bool IsOwnerAlive() return _owner != null && Godot.Object.IsInstanceValid(_owner); } } + + internal class TemporaryVisualEffectNode : Node2D + { + private readonly string _texturePath; + private readonly float _duration; + private readonly float _initialScale; + private readonly float _targetScale; + private readonly float _rotationSpeed; + private readonly bool _flipH; + private readonly float _initialRotation; + + private Sprite _sprite; + private float _elapsed = 0f; + + public TemporaryVisualEffectNode(string texturePath, float duration, float initialScale, float targetScale = -1f, float rotationSpeed = 0f, bool flipH = false, float initialRotation = 0f) + { + _texturePath = texturePath; + _duration = duration; + _initialScale = initialScale; + _targetScale = targetScale < 0f ? initialScale : targetScale; + _rotationSpeed = rotationSpeed; + _flipH = flipH; + _initialRotation = initialRotation; + } + + public override void _Ready() + { + Texture tex = GD.Load(_texturePath); + _sprite = new Sprite + { + Texture = tex, + Centered = true, + Scale = new Vector2(_initialScale, _initialScale), + FlipH = _flipH + }; + AddChild(_sprite); + Rotation = _initialRotation; + SetProcess(true); + } + + public override void _Process(float delta) + { + _elapsed += delta; + if (_elapsed >= _duration) + { + QueueFree(); + return; + } + + // Lerp scale + float t = _elapsed / _duration; + float currentScale = Mathf.Lerp(_initialScale, _targetScale, t); + _sprite.Scale = new Vector2(currentScale, currentScale); + + // Lerp opacity (fade out) + float opacity = Mathf.Lerp(1f, 0f, t); + _sprite.Modulate = new Color(1f, 1f, 1f, opacity); + + // Rotate + if (_rotationSpeed != 0f) + { + Rotation += _rotationSpeed * delta; + } + } + } + + internal class KnightExplosionNode : Node2D + { + private const float DURATION = 3.0f; // 3 seconds duration + private const float INITIAL_SCALE = 0.25f; + private const float TARGET_SCALE = 0.5f; // growing to 2x initial scale (0.25 * 2 = 0.5) + private const float DAMAGE_TICK_INTERVAL = 0.5f; // damage tick every 0.5s + private const float BASE_RADIUS = 160f; // base radius of explosion at scale 1.0 + + private readonly Player _owner; + private Sprite _sprite; + private float _elapsed = 0f; + private float _tickTimer = 0f; + + public KnightExplosionNode(Player owner) + { + _owner = owner; + } + + public override void _Ready() + { + Texture tex = GD.Load("res://Assets/SkillAnimation/knight_explose.png"); + _sprite = new Sprite + { + Texture = tex, + Centered = true, + Scale = new Vector2(INITIAL_SCALE, INITIAL_SCALE) + }; + AddChild(_sprite); + SetProcess(true); + + // Deal first tick immediately + DealExplosionDamage(); + } + + public override void _Process(float delta) + { + if (_owner == null || !Godot.Object.IsInstanceValid(_owner)) + { + QueueFree(); + return; + } + + _elapsed += delta; + if (_elapsed >= DURATION) + { + QueueFree(); + return; + } + + // Lerp scale + float t = _elapsed / DURATION; + float currentScale = Mathf.Lerp(INITIAL_SCALE, TARGET_SCALE, t); + _sprite.Scale = new Vector2(currentScale, currentScale); + + // Lerp opacity (fade out towards the end) + float opacity = t > 0.8f ? Mathf.Lerp(1f, 0f, (t - 0.8f) / 0.2f) : 1f; + _sprite.Modulate = new Color(1f, 1f, 1f, opacity); + + // Tick damage + _tickTimer += delta; + if (_tickTimer >= DAMAGE_TICK_INTERVAL) + { + _tickTimer -= DAMAGE_TICK_INTERVAL; + DealExplosionDamage(); + } + } + + private void DealExplosionDamage() + { + if (_owner == null || !Godot.Object.IsInstanceValid(_owner)) + { + return; + } + + float currentScale = _sprite.Scale.x; + float currentRadius = BASE_RADIUS * currentScale; + + int attackerAtk = _owner.Attributes?.EffectiveAtk ?? 1; + + foreach (Character enemy in EnumerateEnemyCharacters()) + { + if (enemy?.Attributes?.HP == null || !enemy.Attributes.HP.IsAlive) + { + continue; + } + + if (enemy.GlobalPosition.DistanceTo(GlobalPosition) <= currentRadius) + { + int defenderDef = enemy.Attributes?.EffectiveDef ?? 0; + int baseDamage = Mathf.Max(1, attackerAtk - Mathf.FloorToInt(defenderDef * 0.4f)); + // Roll smaller random damage per tick + int rolled = Mathf.RoundToInt((float)GD.RandRange(1, 3)); + int damage = Mathf.Max(1, baseDamage + rolled); + + enemy.TakeDamage(damage); + GD.Print($"[COMBAT] Knight Explosion ticked on {enemy.EntityName} for {damage} damage!"); + } + } + } + + private IEnumerable EnumerateEnemyCharacters() + { + Node root = _owner?.GetTree()?.Root; + if (root == null) + { + yield break; + } + + var stack = new Stack(); + stack.Push(root); + + while (stack.Count > 0) + { + Node node = stack.Pop(); + if (node is Character character && character != _owner) + { + yield return character; + } + + foreach (Node child in node.GetChildren()) + { + stack.Push(child); + } + } + } + } } \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/SuperArrowSkill.cs b/Scripts/Core/Data/Skills/SuperArrowSkill.cs new file mode 100644 index 0000000..a1d3f50 --- /dev/null +++ b/Scripts/Core/Data/Skills/SuperArrowSkill.cs @@ -0,0 +1,41 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Super arrow skill for the Archer. + /// A powerful arrow that paralyzes enemies for 5 seconds on hit. + /// Long cooldown skill. + /// + public class SuperArrowSkill : Attributes.Skills + { + public SuperArrowSkill() + { + Name = "Super Arrow"; + Description = "Fire a powerful arrow that paralyzes enemies for 5 seconds."; + } + + public override float MaxRange => 350f; + + public override float GetCooldownDuration() => 10.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + // Spawn an arrow that applies stun effect on hit + SkillProjectileSpawner.SpawnSuperArrow( + player, + target, + MaxRange, + onHitEffect: () => new StunEffect(5.0f), // 5 seconds stun (paralysis) + onHitChance: 1.0f); // Always apply + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/TripleArrowSkill.cs b/Scripts/Core/Data/Skills/TripleArrowSkill.cs new file mode 100644 index 0000000..29e267e --- /dev/null +++ b/Scripts/Core/Data/Skills/TripleArrowSkill.cs @@ -0,0 +1,36 @@ +using Godot; + +using QuestFantasy.Characters; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Advanced triple arrow projectile for the Archer. + /// Fires 3 arrows simultaneously in the same direction, slightly offset side-by-side. + /// + public class TripleArrowSkill : Attributes.Skills + { + public TripleArrowSkill() + { + Name = "Triple Arrow"; + Description = "Fire 3 arrows at once that pierce enemies at range."; + } + + public override float MaxRange => 300f; + + public override float GetCooldownDuration() => 1.5f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + SkillProjectileSpawner.SpawnTripleArrow( + player, + target, + MaxRange); + } + } +} \ No newline at end of file diff --git a/Scripts/Core/Data/Skills/TripleFireballSkill.cs b/Scripts/Core/Data/Skills/TripleFireballSkill.cs new file mode 100644 index 0000000..6b7a346 --- /dev/null +++ b/Scripts/Core/Data/Skills/TripleFireballSkill.cs @@ -0,0 +1,43 @@ +using Godot; + +using QuestFantasy.Characters; +using QuestFantasy.Core.Systems.StatusEffects; + +namespace QuestFantasy.Core.Data.Skills +{ + /// + /// Advanced triple fireball projectile for the Mage. + /// Fires 3 fireballs simultaneously in a spread pattern. + /// Explodes on wall or monster hit and damages nearby enemies. + /// Has a 25% chance to apply Burn on each enemy hit (configurable via GameConstants). + /// + public class TripleFireballSkill : Attributes.Skills + { + public TripleFireballSkill() + { + Name = "Triple Fireball"; + Description = "Launch 3 fireballs in a spread that explode on impact."; + } + + public override float MaxRange => 300f; + + public override float GetCooldownDuration() => 2.0f; + + public override void Effect(Player player, Character target) + { + if (player == null) + { + return; + } + + SkillProjectileSpawner.SpawnTripleFireball( + player, + target, + MaxRange, + onHitEffect: () => new BurnEffect( + GameConstants.FIREBALL_BURN_DURATION, + GameConstants.FIREBALL_BURN_DPS), + onHitChance: GameConstants.FIREBALL_BURN_CHANCE); + } + } +} \ No newline at end of file diff --git a/Scripts/Prototype/LobbyManager.cs b/Scripts/Prototype/LobbyManager.cs index 97aebfd..e85d180 100644 --- a/Scripts/Prototype/LobbyManager.cs +++ b/Scripts/Prototype/LobbyManager.cs @@ -313,6 +313,20 @@ private void SetupClassSelectUI() _classSelectUI = new ClassSelectUI(); AddChild(_classSelectUI); _classSelectUI.ClassSelected += OnClassSelected; + _classSelectUI.SkillLoadoutChanged += OnSkillLoadoutChanged; + } + + private void OnSkillLoadoutChanged(List orderedSkillIds) + { + Player target = _player; + if (target == null) + { + return; + } + + target.SetEquippedSkills(orderedSkillIds); + GD.Print($"[Lobby] Applied skill loadout to {target.Name}"); + SyncRequested?.Invoke(); } private void OnNpcClassChangeRequested(NPC npc, Player player) @@ -325,8 +339,9 @@ private void OnNpcClassChangeRequested(NPC npc, Player player) Player target = player ?? _player; PlayerClass current = target?.PlayerClass ?? PlayerClass.Adventurer; int level = (int)(target?.Level ?? 1); + var equippedSkills = target?.GetEquippedSkillIds(); - _classSelectUI.Show(current, level); + _classSelectUI.Show(current, level, equippedSkills); GD.Print($"[Lobby] Class selector opened by {npc.EntityName}. Current class: {current}, Player Level: {level}"); } diff --git a/Scripts/UI/ClassSelectUI.cs b/Scripts/UI/ClassSelectUI.cs index a4a0dea..3eb87e0 100644 --- a/Scripts/UI/ClassSelectUI.cs +++ b/Scripts/UI/ClassSelectUI.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using Godot; @@ -6,12 +8,6 @@ namespace QuestFantasy.UI { - /// - /// Fullscreen overlay panel presented when the player interacts with the "Previous Hero" NPC. - /// Extends CanvasLayer (same as DifficultySelectionUI) so it is always fixed to screen-space, - /// independent of the game camera position. - /// Fires when the player confirms a choice. - /// public class ClassSelectUI : CanvasLayer { // ── Layout constants ────────────────────────────────────────────── @@ -41,26 +37,41 @@ public class ClassSelectUI : CanvasLayer private static readonly Color BtnCloseBg = new Color(0.20f, 0.22f, 0.28f, 1f); private static readonly Color BtnCloseHover = new Color(0.30f, 0.33f, 0.42f, 1f); + // Tab colors + private static readonly Color TabNormal = new Color(0.15f, 0.18f, 0.25f, 1f); + private static readonly Color TabSelected = new Color(0.25f, 0.45f, 0.85f, 1f); + // ── State ───────────────────────────────────────────────────────── private PlayerClass _currentClass = PlayerClass.Adventurer; private PlayerClass _selectedClass = PlayerClass.Adventurer; private int _playerLevel = 1; - // Per-card panel references kept for highlight refresh (no node-path lookup needed) - private Panel[] _cardPanels; + // Skill state + private readonly List _equippedSkillIds = new List(); + private ReadOnlyCollection _availableSkills; - // Root container — shown/hidden as a whole + // UI References private Control _root; private Label _subtitleLabel; + private Control _classTabContent; + private Control _skillTabContent; + private Button _tabClassBtn; + private Button _tabSkillsBtn; + private Panel[] _cardPanels; + + // Skill UI References + private Label[] _skillSlotLabels; + private Panel[] _skillCardPanels; public event Action ClassSelected; + public event Action> SkillLoadoutChanged; private static readonly PlayerClass[] AllClasses = { PlayerClass.Adventurer, PlayerClass.Mage, PlayerClass.Archer, - PlayerClass.Warrior + PlayerClass.Knight }; // ── Class accent colours ────────────────────────────────────────── @@ -70,7 +81,7 @@ private static Color GetClassAccent(PlayerClass cls) { case PlayerClass.Mage: return new Color(0.75f, 0.40f, 1.00f, 1f); case PlayerClass.Archer: return new Color(0.40f, 0.90f, 0.50f, 1f); - case PlayerClass.Warrior: return new Color(1.00f, 0.50f, 0.25f, 1f); + case PlayerClass.Knight: return new Color(1.00f, 0.50f, 0.25f, 1f); default: return new Color(0.40f, 0.70f, 1.00f, 1f); } } @@ -81,7 +92,7 @@ private static string GetClassEmoji(PlayerClass cls) { case PlayerClass.Mage: return "🔮"; case PlayerClass.Archer: return "🏹"; - case PlayerClass.Warrior: return "⚔️"; + case PlayerClass.Knight: return "⚔️"; default: return "🗺️"; } } @@ -90,23 +101,35 @@ private static string GetClassEmoji(PlayerClass cls) public override void _Ready() { - // Keep receiving input even when game tree is paused PauseMode = PauseModeEnum.Process; BuildLayout(); _root.Visible = false; } - /// Opens the panel, pre-selecting the player's current class. - public void Show(PlayerClass currentClass, int playerLevel) + public void Show(PlayerClass currentClass, int playerLevel, IReadOnlyList currentEquippedSkills = null) { _currentClass = currentClass; _selectedClass = currentClass; _playerLevel = playerLevel; + + _equippedSkillIds.Clear(); + if (currentEquippedSkills != null) + { + _equippedSkillIds.AddRange(currentEquippedSkills); + } + else + { + _equippedSkillIds.AddRange(PlayerClassData.GetDefaultSkillLoadout(_currentClass)); + } + + _availableSkills = PlayerClassData.GetAllSkillDefinitions(_currentClass); + if (_subtitleLabel != null) { _subtitleLabel.Text = $"Different classes unlock different skills. You can change class again any time. (Level Requirement: Lv.{playerLevel}/{GameConstants.CLASS_CHANGE_MIN_LEVEL})"; } RefreshCardHighlights(); + SwitchTab(0); _root.Visible = true; } @@ -122,13 +145,11 @@ public void Show(PlayerClass currentClass, int playerLevel) private void BuildLayout() { - // Full-screen Control root — anchored to entire viewport via preset _root = new Control(); _root.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); _root.MouseFilter = Control.MouseFilterEnum.Stop; AddChild(_root); - // Dim overlay var overlay = new ColorRect { Color = BgOverlay, @@ -137,10 +158,8 @@ private void BuildLayout() overlay.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); _root.AddChild(overlay); - // Centred panel container var panel = new Panel(); panel.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Center); - // Offset by half the panel size so it is perfectly centred panel.MarginLeft = -PanelWidth / 2f; panel.MarginTop = -PanelHeight / 2f; panel.MarginRight = PanelWidth / 2f; @@ -148,24 +167,49 @@ private void BuildLayout() panel.AddStyleboxOverride("panel", MakePanelStyle()); _root.AddChild(panel); - // ── Header ────────────────────────────────────────────────── + // Tabs + var tabsBox = new HBoxContainer(); + tabsBox.SetAnchorsAndMarginsPreset(Control.LayoutPreset.TopWide); + tabsBox.MarginLeft = 24f; + tabsBox.MarginRight = -24f; + tabsBox.MarginTop = 16f; + tabsBox.MarginBottom = 50f; + panel.AddChild(tabsBox); + + _tabClassBtn = CreateStyledButton("⚡ Change Class", TabSelected, TabSelected); + _tabClassBtn.SizeFlagsHorizontal = (int)Control.SizeFlags.ExpandFill; + _tabClassBtn.Connect("pressed", this, nameof(OnTabPressed), new Godot.Collections.Array { 0 }); + tabsBox.AddChild(_tabClassBtn); + + _tabSkillsBtn = CreateStyledButton("🗡 Skills", TabNormal, TabSelected); + _tabSkillsBtn.SizeFlagsHorizontal = (int)Control.SizeFlags.ExpandFill; + _tabSkillsBtn.Connect("pressed", this, nameof(OnTabPressed), new Godot.Collections.Array { 1 }); + tabsBox.AddChild(_tabSkillsBtn); + + _classTabContent = new Control(); + _classTabContent.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); + _classTabContent.MarginTop = 50f; + panel.AddChild(_classTabContent); + + _skillTabContent = new Control(); + _skillTabContent.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); + _skillTabContent.MarginTop = 50f; + _skillTabContent.Visible = false; + panel.AddChild(_skillTabContent); + + BuildClassTabContent(); + BuildSkillTabContent(); + } + + private void BuildClassTabContent() + { var header = new VBoxContainer(); header.SetAnchorsAndMarginsPreset(Control.LayoutPreset.TopWide); header.MarginLeft = 24f; header.MarginRight = -24f; header.MarginTop = 16f; header.MarginBottom = HeaderHeight; - panel.AddChild(header); - - var title = new Label - { - Text = "⚡ Choose Your Class", - Align = Label.AlignEnum.Center, - RectMinSize = new Vector2(0f, 34f), - MouseFilter = Control.MouseFilterEnum.Ignore, - }; - title.AddColorOverride("font_color", HeaderColor); - header.AddChild(title); + _classTabContent.AddChild(header); _subtitleLabel = new Label { @@ -178,7 +222,6 @@ private void BuildLayout() _subtitleLabel.AddColorOverride("font_color", SubHeaderColor); header.AddChild(_subtitleLabel); - // ── Cards row ─────────────────────────────────────────────── float totalCardsW = AllClasses.Length * CardWidth + (AllClasses.Length - 1) * CardSpacing; float cardsLeft = (PanelWidth - totalCardsW) / 2f; float cardsTop = HeaderHeight + 8f; @@ -190,23 +233,85 @@ private void BuildLayout() PlayerClass cls = AllClasses[i]; float cardX = cardsLeft + i * (CardWidth + CardSpacing); var card = BuildClassCard(cls, cardX, cardsTop, i); - panel.AddChild(card); + _classTabContent.AddChild(card); } - // ── Footer buttons ─────────────────────────────────────────── - float footerY = PanelHeight - FooterHeight + 4f; + float footerY = PanelHeight - FooterHeight - 50f; - var confirmBtn = CreateStyledButton("✔ Confirm Class", BtnConfirmBg, BtnConfirmHover); + var confirmBtn = CreateStyledButton("✔ Confirm Class", BtnConfirmBg, BtnConfirmHover); confirmBtn.RectPosition = new Vector2(PanelWidth / 2f - 152f, footerY); confirmBtn.RectMinSize = new Vector2(142f, 42f); confirmBtn.Connect("pressed", this, nameof(OnConfirmPressed)); - panel.AddChild(confirmBtn); + _classTabContent.AddChild(confirmBtn); - var closeBtn = CreateStyledButton("✖ Cancel", BtnCloseBg, BtnCloseHover); + var closeBtn = CreateStyledButton("✖ Cancel", BtnCloseBg, BtnCloseHover); closeBtn.RectPosition = new Vector2(PanelWidth / 2f + 14f, footerY); closeBtn.RectMinSize = new Vector2(112f, 42f); closeBtn.Connect("pressed", this, nameof(OnClosePressed)); - panel.AddChild(closeBtn); + _classTabContent.AddChild(closeBtn); + } + + private void BuildSkillTabContent() + { + // Skill slots (Top) + var slotsBox = new HBoxContainer(); + slotsBox.SetAnchorsAndMarginsPreset(Control.LayoutPreset.TopWide); + slotsBox.MarginLeft = 50f; + slotsBox.MarginRight = -50f; + slotsBox.MarginTop = 16f; + slotsBox.MarginBottom = 76f; + slotsBox.Alignment = BoxContainer.AlignMode.Center; + slotsBox.AddConstantOverride("separation", 20); + _skillTabContent.AddChild(slotsBox); + + _skillSlotLabels = new Label[3]; + for (int i = 0; i < 3; i++) + { + var slotPanel = new Panel(); + slotPanel.RectMinSize = new Vector2(200f, 60f); + slotPanel.AddStyleboxOverride("panel", MakeCardStyle(new Color(0.12f, 0.15f, 0.22f, 1f), new Color(0.3f, 0.4f, 0.6f, 1f), 2)); + + var lbl = new Label + { + Text = $"Slot {i + 1}: Empty", + Align = Label.AlignEnum.Center, + Valign = Label.VAlign.Center, + RectMinSize = new Vector2(200f, 60f) + }; + slotPanel.AddChild(lbl); + _skillSlotLabels[i] = lbl; + slotsBox.AddChild(slotPanel); + } + + // Scroll container for skills (Middle) + var scroll = new ScrollContainer(); + scroll.Name = "ScrollContainer"; + scroll.SetAnchorsAndMarginsPreset(Control.LayoutPreset.TopWide); + scroll.MarginLeft = 24f; + scroll.MarginRight = -24f; + scroll.MarginTop = 100f; + scroll.MarginBottom = 385f; + scroll.RectMinSize = new Vector2(0f, 285f); + scroll.ScrollVerticalEnabled = false; + scroll.ScrollHorizontalEnabled = true; + _skillTabContent.AddChild(scroll); + + // Skill grid inside scroll container + var gridBox = new HBoxContainer(); + gridBox.Name = "SkillGrid"; + gridBox.SizeFlagsHorizontal = (int)Control.SizeFlags.ExpandFill; + gridBox.SizeFlagsVertical = (int)Control.SizeFlags.ExpandFill; + gridBox.Alignment = BoxContainer.AlignMode.Center; + gridBox.AddConstantOverride("separation", (int)CardSpacing); + scroll.AddChild(gridBox); + + // Footer (Bottom) + float footerY = PanelHeight - FooterHeight - 50f; + var saveBtn = CreateStyledButton("✔ Save Loadout", BtnConfirmBg, BtnConfirmHover); + saveBtn.RectPosition = new Vector2(PanelWidth / 2f - 71f, footerY); + saveBtn.RectMinSize = new Vector2(142f, 42f); + saveBtn.Connect("pressed", this, nameof(OnSaveLoadoutPressed)); + _skillTabContent.AddChild(saveBtn); } // ── Card builder ────────────────────────────────────────────────── @@ -219,30 +324,38 @@ private Panel BuildClassCard(PlayerClass cls, float x, float y, int index) card.RectPosition = new Vector2(x, y); card.RectMinSize = new Vector2(CardWidth, CardHeight); card.AddStyleboxOverride("panel", MakeCardStyle(CardBgNormal, CardBorderNormal)); + card.MouseFilter = Control.MouseFilterEnum.Stop; - // Accent stripe at the very top var stripe = new ColorRect { Color = accent, MouseFilter = Control.MouseFilterEnum.Ignore }; stripe.SetAnchorsAndMarginsPreset(Control.LayoutPreset.TopWide); stripe.MarginBottom = -(CardHeight - 6f); card.AddChild(stripe); - // Content VBox var vbox = new VBoxContainer(); vbox.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); vbox.MarginLeft = 8f; vbox.MarginRight = -8f; vbox.MarginTop = 14f; vbox.MarginBottom = -8f; + vbox.SizeFlagsHorizontal = (int)Control.SizeFlags.ExpandFill; + vbox.SizeFlagsVertical = (int)Control.SizeFlags.ExpandFill; card.AddChild(vbox); vbox.AddChild(MakeLabel(GetClassEmoji(cls), 32f, CardTitleColor, center: true)); vbox.AddChild(MakeLabel(PlayerClassData.GetDisplayName(cls), 22f, CardTitleColor, center: true)); vbox.AddChild(new HSeparator { RectMinSize = new Vector2(0f, 4f) }); vbox.AddChild(MakeLabel(PlayerClassData.GetDescription(cls), 72f, CardDescColor, center: true, wrap: true)); - vbox.AddChild(MakeLabel("Skills:", 16f, SubHeaderColor, center: true)); - vbox.AddChild(MakeLabel(PlayerClassData.GetSkillListText(cls), 30f, SkillLabelColor, center: true, wrap: true)); - // Transparent click-receiver over the whole card + // Display all available skills for this class + vbox.AddChild(MakeLabel("Available Skills:", 16f, SubHeaderColor, center: true)); + + var skillDefs = PlayerClassData.GetAllSkillDefinitions(cls); + foreach (var skillDef in skillDefs) + { + var skillLbl = MakeLabel($"{skillDef.Emoji} {skillDef.DisplayName}", 12f, SkillLabelColor, center: false, wrap: true); + vbox.AddChild(skillLbl); + } + var btn = new Button { Text = string.Empty, Flat = true }; btn.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); foreach (var s in new[] { "normal", "hover", "pressed", "focus" }) @@ -256,6 +369,37 @@ private Panel BuildClassCard(PlayerClass cls, float x, float y, int index) return card; } + private Panel BuildSkillCard(SkillDefinition def) + { + var card = new Panel(); + card.RectMinSize = new Vector2(CardWidth, CardHeight); + + var vbox = new VBoxContainer(); + vbox.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); + vbox.MarginLeft = 8f; + vbox.MarginRight = -8f; + vbox.MarginTop = 14f; + vbox.MarginBottom = -8f; + card.AddChild(vbox); + + vbox.AddChild(MakeLabel(def.Emoji, 32f, CardTitleColor, center: true)); + vbox.AddChild(MakeLabel(def.DisplayName, 22f, CardTitleColor, center: true)); + vbox.AddChild(new HSeparator { RectMinSize = new Vector2(0f, 4f) }); + vbox.AddChild(MakeLabel(def.Description, 72f, CardDescColor, center: true, wrap: true)); + vbox.AddChild(MakeLabel($"CD: {def.CooldownSec}s", 16f, SubHeaderColor, center: true)); + + var btn = new Button { Text = string.Empty, Flat = true }; + btn.SetAnchorsAndMarginsPreset(Control.LayoutPreset.Wide); + foreach (var s in new[] { "normal", "hover", "pressed", "focus" }) + { + btn.AddStyleboxOverride(s, new StyleBoxEmpty()); + } + btn.Connect("pressed", this, nameof(OnSkillCardPressed), new Godot.Collections.Array { def.Id }); + card.AddChild(btn); + + return card; + } + // ── Style / Label helpers ───────────────────────────────────────── private static StyleBoxFlat MakePanelStyle() @@ -335,6 +479,25 @@ private static StyleBoxFlat MakeButtonStyle(Color bg) // ── Signal handlers ─────────────────────────────────────────────── + private void SwitchTab(int tabIndex) + { + _classTabContent.Visible = (tabIndex == 0); + _skillTabContent.Visible = (tabIndex == 1); + + _tabClassBtn.AddStyleboxOverride("normal", MakeButtonStyle(tabIndex == 0 ? TabSelected : TabNormal)); + _tabSkillsBtn.AddStyleboxOverride("normal", MakeButtonStyle(tabIndex == 1 ? TabSelected : TabNormal)); + + if (tabIndex == 1) + { + RefreshSkillTab(); + } + } + + private void OnTabPressed(int tabIndex) + { + SwitchTab(tabIndex); + } + private void OnCardPressed(int index) { if (index >= 0 && index < AllClasses.Length) @@ -342,7 +505,6 @@ private void OnCardPressed(int index) PlayerClass targetClass = AllClasses[index]; if (targetClass != PlayerClass.Adventurer && _playerLevel < GameConstants.CLASS_CHANGE_MIN_LEVEL) { - // Block selection of locked advanced classes (only Adventurer is selectable) return; } @@ -352,9 +514,27 @@ private void OnCardPressed(int index) } private void OnConfirmPressed() + { + if (_selectedClass != _currentClass) + { + // Class changed -> Reset skills to new class defaults + _currentClass = _selectedClass; + _availableSkills = PlayerClassData.GetAllSkillDefinitions(_currentClass); + _equippedSkillIds.Clear(); + _equippedSkillIds.AddRange(PlayerClassData.GetDefaultSkillLoadout(_currentClass)); + ClassSelected?.Invoke(_selectedClass); + SwitchTab(1); // Auto jump to skills tab + } + else + { + Hide(); + } + } + + private void OnSaveLoadoutPressed() { Hide(); - ClassSelected?.Invoke(_selectedClass); + SkillLoadoutChanged?.Invoke(_equippedSkillIds); } private void OnClosePressed() @@ -362,6 +542,30 @@ private void OnClosePressed() Hide(); } + private void OnSkillCardPressed(string skillId) + { + // If class only has 1 skill, it's always equipped + if (_availableSkills.Count <= 1) return; + + if (_equippedSkillIds.Contains(skillId)) + { + _equippedSkillIds.Remove(skillId); + } + else + { + if (_equippedSkillIds.Count >= 3) + { + // Replace the last slot if full + _equippedSkillIds[2] = skillId; + } + else + { + _equippedSkillIds.Add(skillId); + } + } + RefreshSkillTab(); + } + // ── Highlight refresh ───────────────────────────────────────────── private void RefreshCardHighlights() @@ -378,12 +582,10 @@ private void RefreshCardHighlights() bool isSelected = cls == _selectedClass; bool isCurrent = cls == _currentClass; - // Adventurer is always unlocked. Advanced classes are locked if level requirement is not met. bool isLocked = levelLocked && cls != PlayerClass.Adventurer; if (isLocked) { - // Gray out and make semi-transparent _cardPanels[i].Modulate = new Color(0.4f, 0.4f, 0.4f, 0.7f); _cardPanels[i].AddStyleboxOverride("panel", MakeCardStyle(CardBgNormal, CardBorderNormal)); } @@ -404,5 +606,71 @@ private void RefreshCardHighlights() } } } + + private void RefreshSkillTab() + { + // Update slots + for (int i = 0; i < 3; i++) + { + if (i < _equippedSkillIds.Count) + { + string skillId = _equippedSkillIds[i]; + var def = GetSkillDef(skillId); + if (def != null) + { + _skillSlotLabels[i].Text = $"Slot {i + 1}: {def.Emoji} {def.DisplayName}"; + } + else + { + _skillSlotLabels[i].Text = $"Slot {i + 1}: Empty"; + } + } + else + { + _skillSlotLabels[i].Text = $"Slot {i + 1}: Empty"; + } + } + + // Rebuild grid + var gridBox = _skillTabContent.GetNodeOrNull("ScrollContainer/SkillGrid"); + if (gridBox != null) + { + foreach (Node child in gridBox.GetChildren()) + { + child.QueueFree(); + } + + _skillCardPanels = new Panel[_availableSkills.Count]; + + for (int i = 0; i < _availableSkills.Count; i++) + { + var def = _availableSkills[i]; + var card = BuildSkillCard(def); + + bool isEquipped = _equippedSkillIds.Contains(def.Id); + + card.AddStyleboxOverride("panel", MakeCardStyle( + isEquipped ? CardBgSelected : CardBgNormal, + isEquipped ? SkillLabelColor : CardBorderNormal, + isEquipped ? 3 : 2 + )); + + gridBox.AddChild(card); + _skillCardPanels[i] = card; + } + } + } + + private SkillDefinition GetSkillDef(string id) + { + if (_availableSkills == null) return null; + foreach (var def in _availableSkills) + { + if (def.Id == id) return def; + } + return null; + } } + + } \ No newline at end of file diff --git a/Scripts/UI/PlayerHud.cs b/Scripts/UI/PlayerHud.cs index a6da0d5..2e6046e 100644 --- a/Scripts/UI/PlayerHud.cs +++ b/Scripts/UI/PlayerHud.cs @@ -17,7 +17,19 @@ public class PlayerHud : CanvasLayer { ["basic_attack"] = "res://Assets/SkillAnimation/slash-icon.png", ["bow_attack"] = "res://Assets/SkillAnimation/arrow.png", + ["triple_arrow"] = "res://Assets/SkillAnimation/arrow_multi.png", + ["ricochet_arrow"] = "res://Assets/SkillAnimation/arrow_ricochet.png", ["fireball"] = "res://Assets/SkillAnimation/fireball.png", + ["triple_fireball"] = "res://Assets/SkillAnimation/fireball_multi.png", + ["giant_fireball"] = "res://Assets/SkillAnimation/fireball3.png", + ["flying_sword"] = "res://Assets/SkillAnimation/fly_sword.png", + ["defense_stance"] = "res://Assets/Characters/warrior/defense.png", + ["magic_slash"] = "res://Assets/SkillAnimation/magic_slash-2.png", + ["ice_spear"] = "res://Assets/SkillAnimation/ice_spear-4.png", + ["digit_arrow"] = "res://Assets/SkillAnimation/digit_arrow.png", + ["super_arrow"] = "res://Assets/SkillAnimation/super_arrow.png", + ["roundhouse_slash"] = "res://Assets/SkillAnimation/roundhouse-slash.png", + ["knight_explose"] = "res://Assets/SkillAnimation/knight_explose.png", }; private readonly List _skillSlots = new List();