├── docs ├── battlehud_docs.txt ├── formulas_docs.txt ├── database_docs.txt ├── battler_docs.txt └── troops.txt ├── BattlerStats.gd.uid ├── database ├── states │ ├── counter.gd.uid │ ├── states_template.gd.uid │ ├── poison_state.tres │ ├── states_template.gd │ ├── counter.tres │ └── counter.gd ├── items │ ├── items_template.gd.uid │ ├── healingspandhealth.tres │ ├── throwabledamageitem.tres │ └── items_template.gd ├── troops │ ├── enemy_group.gd.uid │ ├── troops_template.gd.uid │ ├── enemy_group.gd │ ├── test_troop.tres │ └── troops_template.gd └── skills │ ├── skills_template.gd.uid │ ├── normal_attack.tres │ ├── simple-heal.tres │ ├── fireball.tres │ ├── counter_skill.tres │ └── skills_template.gd ├── assets ├── globals │ ├── signal_bus.gd.uid │ ├── save-system │ │ ├── saved_game.gd.uid │ │ ├── battler_data.gd.uid │ │ ├── save_loader.gd.uid │ │ ├── saved_game.gd │ │ ├── battler_data.gd │ │ └── save_loader.gd │ ├── battle-settings │ │ ├── GlobalBattleSettings.gd.uid │ │ └── GlobalBattleSettings.gd │ └── signal_bus.gd ├── shaders │ ├── battler_select_shader.gdshader.uid │ └── battler_select_shader.gdshader ├── sounds │ ├── misssound1.ogg │ ├── defeatsound1.wav │ ├── misssound1.ogg.import │ └── defeatsound1.wav.import ├── images │ ├── icons │ │ ├── Asset 11.png │ │ ├── Asset 12.png │ │ ├── Asset 87.png │ │ ├── Asset 11.png.import │ │ ├── Asset 12.png.import │ │ └── Asset 87.png.import │ ├── turnbasedcombatthumbnail.png │ └── turnbasedcombatthumbnail.png.import ├── abilities │ ├── heal_ability.tres │ └── damage_ability.tres └── turnbasedcombatthumbnail.png.import ├── battle-manager ├── ai_manager.gd.uid ├── battlemanager.gd.uid ├── scripts │ ├── experience.gd.uid │ ├── formulas.gd.uid │ ├── damage_number.gd.uid │ ├── battler_scripts │ │ ├── battler.gd.uid │ │ └── battler.gd │ ├── damage_number.gd │ ├── experience.gd │ └── formulas.gd ├── skills │ ├── skill_list.gd.uid │ ├── character_abilities.gd.uid │ ├── skill_list.gd │ └── character_abilities.gd ├── battlehud │ ├── battlehud.gd.uid │ ├── battler_info.gd.uid │ ├── item_button.gd.uid │ ├── skill_button.gd.uid │ ├── battler_info.gd │ ├── item_button.gd │ ├── item-button-preset.tscn │ ├── skill-button-preset.tscn │ ├── skill_button.gd │ ├── battlehud.gd │ ├── battleresults.tscn │ └── battlehud.tscn ├── inventory │ ├── inventory.gd.uid │ ├── item_handler.gd.uid │ ├── item_handler.gd │ └── inventory.gd ├── allies │ ├── YBot.glb │ ├── default_material.tres │ ├── Ybot_Mixamo.tres │ ├── YBot.glb.import │ └── battle_tree_node.tres ├── BattleManager.tscn ├── damage_number.tscn ├── enemy.tscn ├── ai_manager.gd └── battlemanager.gd ├── .gitattributes ├── Placeholder.png ├── animations └── library │ └── Locomotion-Library.res ├── .gitignore ├── replace ├── theme_test.tres └── regular_map │ ├── backtogame.tscn │ └── playableplayer │ └── playableplayer.tscn ├── PlayerData.tres ├── BattlerStats.gd ├── Placeholder.png.import ├── icon.svg.import ├── Placeholder.svg.import ├── LICENSE ├── gettingstarted.md ├── readme.md ├── icon.svg ├── maps └── battle_scenes │ └── test.tscn └── project.godot /docs/battlehud_docs.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BattlerStats.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d2mintfjxyqei 2 | -------------------------------------------------------------------------------- /database/states/counter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://tlfmm6knj1y4 2 | -------------------------------------------------------------------------------- /assets/globals/signal_bus.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bjukgkpmhw4gr 2 | -------------------------------------------------------------------------------- /battle-manager/ai_manager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cmpbtv044tv4d 2 | -------------------------------------------------------------------------------- /database/items/items_template.gd.uid: -------------------------------------------------------------------------------- 1 | uid://m02b4cy78or 2 | -------------------------------------------------------------------------------- /database/troops/enemy_group.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bqvsfh10bn27f 2 | -------------------------------------------------------------------------------- /battle-manager/battlemanager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c4knt0u45bqyx 2 | -------------------------------------------------------------------------------- /battle-manager/scripts/experience.gd.uid: -------------------------------------------------------------------------------- 1 | uid://wn06g6ffxnqr 2 | -------------------------------------------------------------------------------- /battle-manager/scripts/formulas.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bfvdki0u37ayi 2 | -------------------------------------------------------------------------------- /battle-manager/skills/skill_list.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ck8aeete0o2pe 2 | -------------------------------------------------------------------------------- /database/skills/skills_template.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8svh8mpm0hp5 2 | -------------------------------------------------------------------------------- /database/states/states_template.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ck454uorrpoxa 2 | -------------------------------------------------------------------------------- /database/troops/troops_template.gd.uid: -------------------------------------------------------------------------------- 1 | uid://rub0awaryv0r 2 | -------------------------------------------------------------------------------- /assets/globals/save-system/saved_game.gd.uid: -------------------------------------------------------------------------------- 1 | uid://4dprbluji8jg 2 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battlehud.gd.uid: -------------------------------------------------------------------------------- 1 | uid://835wmi7qej15 2 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battler_info.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dbbsrqrispnmb 2 | -------------------------------------------------------------------------------- /battle-manager/battlehud/item_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ec0y25we1j16 2 | -------------------------------------------------------------------------------- /battle-manager/battlehud/skill_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bp8uc3di8qowc 2 | -------------------------------------------------------------------------------- /battle-manager/inventory/inventory.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c2j4t3rigq4gk 2 | -------------------------------------------------------------------------------- /battle-manager/inventory/item_handler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dn464tj32mmmx 2 | -------------------------------------------------------------------------------- /battle-manager/scripts/damage_number.gd.uid: -------------------------------------------------------------------------------- 1 | uid://beodjfctvpq4q 2 | -------------------------------------------------------------------------------- /assets/globals/save-system/battler_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c1qn6g8h7qys8 2 | -------------------------------------------------------------------------------- /assets/globals/save-system/save_loader.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cxgqfg26rrf0e 2 | -------------------------------------------------------------------------------- /assets/shaders/battler_select_shader.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://dgpm5x0y35t1x 2 | -------------------------------------------------------------------------------- /battle-manager/skills/character_abilities.gd.uid: -------------------------------------------------------------------------------- 1 | uid://452h5duekyet 2 | -------------------------------------------------------------------------------- /battle-manager/scripts/battler_scripts/battler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://grs8x8ujsmun 2 | -------------------------------------------------------------------------------- /assets/globals/battle-settings/GlobalBattleSettings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://mequ67nmifu2 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/Placeholder.png -------------------------------------------------------------------------------- /docs/formulas_docs.txt: -------------------------------------------------------------------------------- 1 | (Formula's Example/Explanation) 2 | '(Offense - Defense) * ElementalMultiplier' 3 | -------------------------------------------------------------------------------- /assets/sounds/misssound1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/sounds/misssound1.ogg -------------------------------------------------------------------------------- /assets/sounds/defeatsound1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/sounds/defeatsound1.wav -------------------------------------------------------------------------------- /battle-manager/allies/YBot.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/battle-manager/allies/YBot.glb -------------------------------------------------------------------------------- /docs/database_docs.txt: -------------------------------------------------------------------------------- 1 | (Skills) 2 | res://database/skills/ 3 | 4 | (States, Not implemented.) 5 | res://database/skills/states/ 6 | -------------------------------------------------------------------------------- /assets/images/icons/Asset 11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/images/icons/Asset 11.png -------------------------------------------------------------------------------- /assets/images/icons/Asset 12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/images/icons/Asset 12.png -------------------------------------------------------------------------------- /assets/images/icons/Asset 87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/images/icons/Asset 87.png -------------------------------------------------------------------------------- /animations/library/Locomotion-Library.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/animations/library/Locomotion-Library.res -------------------------------------------------------------------------------- /assets/images/turnbasedcombatthumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cute-Fame-Studio/3D-TurnBasedCombat/HEAD/assets/images/turnbasedcombatthumbnail.png -------------------------------------------------------------------------------- /battle-manager/inventory/item_handler.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func use_item(item:Item, target:Battler) -> void: 4 | print("Use item ", item.item_name, " on target: ", target.character_name) 5 | -------------------------------------------------------------------------------- /assets/globals/save-system/saved_game.gd: -------------------------------------------------------------------------------- 1 | class_name SavedGame extends Resource 2 | 3 | var key # character name or id - Some unique identifier 4 | var value: BattlerData 5 | 6 | @export var char_data = {key: value} 7 | -------------------------------------------------------------------------------- /docs/battler_docs.txt: -------------------------------------------------------------------------------- 1 | (Script used for functional turn based players.) 2 | res://battle-manager/players/battler_ally.gd 3 | 4 | (Script used for functional turn based enemys.) 5 | res://battle-manager/players/battler_enemy.gd 6 | -------------------------------------------------------------------------------- /assets/globals/save-system/battler_data.gd: -------------------------------------------------------------------------------- 1 | class_name BattlerData extends Resource 2 | 3 | var current_health: float # Changed from current_hp to match battler_ally.gd 4 | var current_sp: float # Added SP variable 5 | var current_exp: float 6 | var current_level: int 7 | var skill_list: Array[Resource] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | *.tmp 17 | -------------------------------------------------------------------------------- /battle-manager/allies/default_material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://bxmsjeam54lba"] 2 | 3 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_11x3r"] 4 | 5 | [resource] 6 | next_pass = SubResource("ShaderMaterial_11x3r") 7 | albedo_color = Color(0.661078, 0.740406, 0.987524, 1) 8 | -------------------------------------------------------------------------------- /database/troops/enemy_group.gd: -------------------------------------------------------------------------------- 1 | class_name EnemyGroup 2 | extends Resource 3 | 4 | ## Resource class for storing a group/set of enemy scenes 5 | ## Used by the Troops resource to organize enemies into logical groups 6 | 7 | @export var group_name: String = "" 8 | @export var enemy_scenes: Array[PackedScene] = [] # Array of enemy scenes (PackedScene objects) 9 | -------------------------------------------------------------------------------- /battle-manager/skills/skill_list.gd: -------------------------------------------------------------------------------- 1 | class_name SkillList 2 | extends Node 3 | 4 | @export var character_skills: Array[Skill] = [] 5 | @export var hidden_skill_names: Array[String] = ["Attack"] # Skills to hide from UI 6 | 7 | func get_visible_skills() -> Array[Skill]: 8 | return character_skills.filter(func(skill): return not hidden_skill_names.has(skill.skill_name)) 9 | 10 | func get_skills() -> Array[Skill]: 11 | return character_skills 12 | -------------------------------------------------------------------------------- /database/skills/normal_attack.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Skill" load_steps=2 format=3 uid="uid://w5n4kf04c1fp"] 2 | 3 | [ext_resource type="Script" uid="uid://c8svh8mpm0hp5" path="res://database/skills/skills_template.gd" id="1_pveml"] 4 | 5 | [resource] 6 | script = ExtResource("1_pveml") 7 | skill_name = "Attack" 8 | description = "Basic Attack" 9 | base_power = 10 10 | critical_rate = 5 11 | animation_name = "attack" 12 | -------------------------------------------------------------------------------- /replace/theme_test.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Theme" load_steps=2 format=3 uid="uid://crdk7x2bxdhob"] 2 | 3 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h0lgr"] 4 | bg_color = Color(0, 0, 0, 1) 5 | corner_radius_top_left = 5 6 | corner_radius_top_right = 5 7 | corner_radius_bottom_right = 5 8 | corner_radius_bottom_left = 5 9 | 10 | [resource] 11 | Base/base_type = &"Button" 12 | Button/styles/normal = SubResource("StyleBoxFlat_h0lgr") 13 | -------------------------------------------------------------------------------- /database/states/poison_state.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="State" load_steps=2 format=3 uid="uid://cerdxhbt7nhhk"] 2 | 3 | [ext_resource type="Script" uid="uid://ck454uorrpoxa" path="res://database/states/states_template.gd" id="1_wscyq"] 4 | 5 | [resource] 6 | script = ExtResource("1_wscyq") 7 | state_name = "Poison" 8 | state_description = "Takes damage each turn" 9 | damage_per_turn = 10 10 | turns_active = 3 11 | can_be_cured = true 12 | -------------------------------------------------------------------------------- /PlayerData.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="BattlerStats" load_steps=2 format=3 uid="uid://ciia7nijtierl"] 2 | 3 | [ext_resource type="Script" uid="uid://d2mintfjxyqei" path="res://BattlerStats.gd" id="1_curse"] 4 | 5 | [resource] 6 | resource_local_to_scene = true 7 | script = ExtResource("1_curse") 8 | character_name = "Player" 9 | max_health = 100 10 | max_sp = 100 11 | attack = 10 12 | defense = 5 13 | speed = 5 14 | sp_regen = 5 15 | element = 0 16 | -------------------------------------------------------------------------------- /battle-manager/skills/character_abilities.gd: -------------------------------------------------------------------------------- 1 | class_name CharacterAbilities extends Resource 2 | 3 | @export var ability_name: String 4 | @export var ability_description: String 5 | 6 | @export var target_type: String # Ally or Enemy 7 | @export var target_area: String # Single or Group 8 | 9 | @export var number_value: float # damage or healing value 10 | @export var damage_type: String # Physical, Magical, Healing, etc. 11 | @export var anim_tree_name: String # name of animation to use 12 | -------------------------------------------------------------------------------- /assets/sounds/misssound1.ogg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="oggvorbisstr" 4 | type="AudioStreamOggVorbis" 5 | uid="uid://blhpqv47iiut2" 6 | path="res://.godot/imported/misssound1.ogg-c0e5bcddce7e881bf1f233bb7554c021.oggvorbisstr" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/misssound1.ogg" 11 | dest_files=["res://.godot/imported/misssound1.ogg-c0e5bcddce7e881bf1f233bb7554c021.oggvorbisstr"] 12 | 13 | [params] 14 | 15 | loop=false 16 | loop_offset=0 17 | bpm=0 18 | beat_count=0 19 | bar_beats=4 20 | -------------------------------------------------------------------------------- /assets/abilities/heal_ability.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="CharacterAbilities" load_steps=2 format=3 uid="uid://bhp64tgfkw5xm"] 2 | 3 | [ext_resource type="Script" uid="uid://452h5duekyet" path="res://battle-manager/skills/character_abilities.gd" id="1_xf3od"] 4 | 5 | [resource] 6 | script = ExtResource("1_xf3od") 7 | ability_name = "Heal" 8 | ability_description = "Heals damage through ability use." 9 | target_type = "Ally" 10 | target_area = "Single" 11 | number_value = 20.0 12 | damage_type = "Healing" 13 | anim_tree_name = "jump" 14 | -------------------------------------------------------------------------------- /assets/abilities/damage_ability.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="CharacterAbilities" load_steps=2 format=3 uid="uid://bkn0p2p5vxwv3"] 2 | 3 | [ext_resource type="Script" uid="uid://452h5duekyet" path="res://battle-manager/skills/character_abilities.gd" id="1_nc1uw"] 4 | 5 | [resource] 6 | script = ExtResource("1_nc1uw") 7 | ability_name = "Damage" 8 | ability_description = "Deals damage through ability use." 9 | target_type = "Enemy" 10 | target_area = "Single" 11 | number_value = 25.0 12 | damage_type = "Physical" 13 | anim_tree_name = "kick" 14 | -------------------------------------------------------------------------------- /database/items/healingspandhealth.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Item" load_steps=2 format=3 uid="uid://dff8h0io4jqux"] 2 | 3 | [ext_resource type="Script" uid="uid://m02b4cy78or" path="res://database/items/items_template.gd" id="1_lakn7"] 4 | 5 | [resource] 6 | script = ExtResource("1_lakn7") 7 | item_name = "Basic Heal" 8 | description = "" 9 | item_tags = "" 10 | base_power = 0 11 | critical_rate = 0 12 | effectiveness = 100 13 | element = 0 14 | target_type = 1 15 | skill_type = 0 16 | hp_delta = 0 17 | sp_delta = 0 18 | effect_type = "Damage" 19 | animation_name = "" 20 | -------------------------------------------------------------------------------- /database/items/throwabledamageitem.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Item" load_steps=2 format=3 uid="uid://wucmfbwbmh7u"] 2 | 3 | [ext_resource type="Script" uid="uid://m02b4cy78or" path="res://database/items/items_template.gd" id="1_4x8p8"] 4 | 5 | [resource] 6 | script = ExtResource("1_4x8p8") 7 | item_name = "Dynamite" 8 | description = "" 9 | item_tags = "" 10 | base_power = 0 11 | critical_rate = 0 12 | effectiveness = 100 13 | element = 0 14 | target_type = 1 15 | skill_type = 0 16 | hp_delta = 0 17 | sp_delta = 0 18 | effect_type = "Damage" 19 | animation_name = "" 20 | -------------------------------------------------------------------------------- /assets/shaders/battler_select_shader.gdshader: -------------------------------------------------------------------------------- 1 | shader_type spatial; 2 | render_mode cull_front, unshaded; 3 | 4 | uniform vec3 color : source_color = vec3(0,0,0); 5 | uniform float thickness : hint_range(0.0, 1.0, 0.01) = 0.05; 6 | uniform float alpha : hint_range(0.0, 1.0, 0.01) = 1.0; 7 | 8 | void vertex() { 9 | VERTEX += thickness*NORMAL; 10 | } 11 | 12 | void fragment() { 13 | ALBEDO = color; 14 | ALPHA = alpha; 15 | } 16 | 17 | //void light() { 18 | // // Called for every pixel for every light affecting the material. 19 | // // Uncomment to replace the default light processing function with this one. 20 | //} 21 | -------------------------------------------------------------------------------- /assets/sounds/defeatsound1.wav.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="wav" 4 | type="AudioStreamWAV" 5 | uid="uid://dwsik1526gw4d" 6 | path="res://.godot/imported/defeatsound1.wav-31ff764e9c86fd6be55984bc6c26bdff.sample" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/sounds/defeatsound1.wav" 11 | dest_files=["res://.godot/imported/defeatsound1.wav-31ff764e9c86fd6be55984bc6c26bdff.sample"] 12 | 13 | [params] 14 | 15 | force/8_bit=false 16 | force/mono=false 17 | force/max_rate=false 18 | force/max_rate_hz=44100 19 | edit/trim=false 20 | edit/normalize=false 21 | edit/loop_mode=0 22 | edit/loop_begin=0 23 | edit/loop_end=-1 24 | compress/mode=0 25 | -------------------------------------------------------------------------------- /database/states/states_template.gd: -------------------------------------------------------------------------------- 1 | class_name State 2 | extends Resource 3 | 4 | enum StateType { 5 | DOT, # Damage over time (poison, burn) 6 | COUNTER, # Counter attacks 7 | BUFF, # Stat increases 8 | DEBUFF # Stat decreases 9 | } 10 | 11 | @export var state_name: String = "" 12 | @export var state_description: String = "" 13 | @export var state_type: StateType 14 | @export var damage_per_turn: int = 0 15 | @export var turns_active: int = -1 # -1 means infinite until cured 16 | @export var can_be_cured: bool = true 17 | @export var damage_reduction: float = 1.0 # 1.0 = normal damage, 0.5 = half damage, etc. 18 | -------------------------------------------------------------------------------- /database/skills/simple-heal.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Skill" load_steps=3 format=3 uid="uid://1hjacbe5v8of"] 2 | 3 | [ext_resource type="Script" uid="uid://c8svh8mpm0hp5" path="res://database/skills/skills_template.gd" id="1_1m4de"] 4 | 5 | [sub_resource type="NoiseTexture3D" id="NoiseTexture3D_udlog"] 6 | 7 | [resource] 8 | script = ExtResource("1_1m4de") 9 | skill_name = "Heal" 10 | description = "Heal Yourself" 11 | skill_color = "red" 12 | skill_tags = "fire,magic,projectile" 13 | icon = SubResource("NoiseTexture3D_udlog") 14 | sp_cost = 15 15 | element = 5 16 | target_type = 4 17 | hp_delta = 50 18 | animation_name = "attack" 19 | -------------------------------------------------------------------------------- /database/skills/fireball.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Skill" load_steps=3 format=3 uid="uid://ddoo4r86oyqyr"] 2 | 3 | [ext_resource type="Script" uid="uid://c8svh8mpm0hp5" path="res://database/skills/skills_template.gd" id="1_udlog"] 4 | 5 | [sub_resource type="NoiseTexture3D" id="NoiseTexture3D_udlog"] 6 | 7 | [resource] 8 | script = ExtResource("1_udlog") 9 | skill_name = "Fireball" 10 | description = "Fireball" 11 | skill_color = "red" 12 | skill_tags = "fire,magic,projectile" 13 | icon = SubResource("NoiseTexture3D_udlog") 14 | base_power = 65 15 | critical_rate = 10 16 | hit_chance = 95 17 | sp_cost = 15 18 | element = 3 19 | animation_name = "attack" 20 | -------------------------------------------------------------------------------- /database/states/counter.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="CounterState" load_steps=3 format=3 uid="uid://bk3jcsfo0eoev"] 2 | 3 | [ext_resource type="Script" uid="uid://tlfmm6knj1y4" path="res://database/states/counter.gd" id="1_05ang"] 4 | [ext_resource type="Resource" uid="uid://w5n4kf04c1fp" path="res://database/skills/normal_attack.tres" id="1_75s6r"] 5 | 6 | [resource] 7 | script = ExtResource("1_05ang") 8 | counter_damage_multiplier = 1.5 9 | counter_skill = ExtResource("1_75s6r") 10 | state_name = "counter" 11 | state_description = "counter" 12 | state_type = 1 13 | damage_per_turn = 0 14 | turns_active = 1 15 | can_be_cured = true 16 | damage_reduction = 1.0 17 | -------------------------------------------------------------------------------- /battle-manager/BattleManager.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bvhah4cfn4txb"] 2 | 3 | [ext_resource type="Script" uid="uid://c4knt0u45bqyx" path="res://battle-manager/battlemanager.gd" id="1_duov8"] 4 | [ext_resource type="PackedScene" uid="uid://cyjcaceen0235" path="res://battle-manager/battlehud/battlehud.tscn" id="5_se261"] 5 | 6 | [node name="Battle-Manager" type="Node3D"] 7 | script = ExtResource("1_duov8") 8 | animation_dictionary = { 9 | "attack": "attack", 10 | "defend": "idle2", 11 | "idle": "idle1", 12 | "turn_left": "turn_left", 13 | "turn_right": "turn_right", 14 | "walk": "run" 15 | } 16 | 17 | [node name="BattleHUD" parent="." instance=ExtResource("5_se261")] 18 | -------------------------------------------------------------------------------- /assets/globals/signal_bus.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | #=============================================================================== 4 | # Battler Targeting Controls 5 | #------------------------------------------------------------------------------- 6 | @warning_ignore("unused_signal") 7 | signal allow_select_target(can_target:bool) 8 | @warning_ignore("unused_signal") 9 | signal select_target(battler:Battler) 10 | @warning_ignore("unused_signal") 11 | signal hover_target(battler:Battler) 12 | @warning_ignore("unused_signal") 13 | signal clear_default_selection 14 | @warning_ignore("unused_signal") 15 | signal counter_attack_started 16 | @warning_ignore("unused_signal") 17 | signal counter_attack_finished 18 | -------------------------------------------------------------------------------- /battle-manager/damage_number.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://d1t2f3fd1qnpn"] 2 | 3 | [ext_resource type="Script" uid="uid://beodjfctvpq4q" path="res://battle-manager/scripts/damage_number.gd" id="1_0awbh"] 4 | 5 | [node name="DamageNumber" type="RichTextLabel"] 6 | anchors_preset = 15 7 | anchor_right = 1.0 8 | anchor_bottom = 1.0 9 | grow_horizontal = 2 10 | grow_vertical = 2 11 | size_flags_horizontal = 3 12 | size_flags_vertical = 3 13 | theme_override_constants/outline_size = 16 14 | theme_override_font_sizes/bold_italics_font_size = 28 15 | theme_override_font_sizes/normal_font_size = 28 16 | theme_override_font_sizes/bold_font_size = 28 17 | bbcode_enabled = true 18 | text = "[b]17[/b]" 19 | horizontal_alignment = 1 20 | vertical_alignment = 1 21 | script = ExtResource("1_0awbh") 22 | -------------------------------------------------------------------------------- /database/skills/counter_skill.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Skill" load_steps=3 format=3 uid="uid://wfkodiihxt64"] 2 | 3 | [ext_resource type="Script" uid="uid://tlfmm6knj1y4" path="res://database/states/counter.gd" id="1_77flv"] 4 | [ext_resource type="Script" uid="uid://c8svh8mpm0hp5" path="res://database/skills/skills_template.gd" id="1_wobk6"] 5 | 6 | [resource] 7 | script = ExtResource("1_wobk6") 8 | skill_name = "Counter" 9 | description = "Enter a defensive stance that counters attacks" 10 | skill_color = "" 11 | skill_tags = "" 12 | base_power = 0 13 | critical_rate = 0 14 | hit_chance = 100 15 | sp_cost = 0 16 | hp_cost = 0 17 | element = 0 18 | target_type = 4 19 | skill_type = 0 20 | hp_delta = 0 21 | sp_delta = 0 22 | effect_type = 2 23 | animation_name = "" 24 | applies_state = ExtResource("1_77flv") 25 | state_apply_chance = 100 26 | -------------------------------------------------------------------------------- /BattlerStats.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name BattlerStats 3 | extends Resource 4 | 5 | @export var character_name: String = "Player" ## Fill in this variable as soon as possible, This will be used when mentioning the character 6 | @export var max_health: int = 100 ## Health is used to make sure character's take longer to be downed. 7 | @export var max_sp: int = 100 ## SP is required for many skills, Such as healing or damaging. 8 | @export var attack: int = 10 ## Attack will increase the amount of base damage when peforming any skill. 9 | @export var defense: int = 5 ## Defense is another way to reduce damage. (Added to the damage intake) 10 | @export var agility: int = 5 ## Agility of the character (affects speed and evasion) 11 | 12 | @export var sp_regen: int = 5 ## Amount of SP recovered per turn 13 | @export var element: int = GlobalBattleSettings.Elements.Physical ## Elements has their own docs file. 14 | -------------------------------------------------------------------------------- /database/states/counter.gd: -------------------------------------------------------------------------------- 1 | class_name CounterState 2 | extends State 3 | 4 | @export var counter_damage_multiplier: float = 1.5 5 | @export var counter_skill: Skill 6 | 7 | func _init(): 8 | state_type = StateType.COUNTER 9 | damage_reduction = 0.5 10 | 11 | func perform_counter(battler: Battler, attacker: Battler) -> void: 12 | print("[COUNTER] %s counters %s's attack!" % [battler.character_name, attacker.character_name]) 13 | 14 | # Wait for attacker's animation to finish 15 | await battler.get_tree().create_timer(0.3).timeout 16 | 17 | if counter_skill: 18 | battler.use_skill(counter_skill, attacker) 19 | else: 20 | battler.state_machine.travel("attack") 21 | var counter_damage = Formulas.physical_damage( 22 | battler, 23 | attacker, 24 | battler.attack * counter_damage_multiplier 25 | ) 26 | attacker.take_damage(counter_damage, battler) # Now properly passes both parameters 27 | -------------------------------------------------------------------------------- /Placeholder.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bc07xmjlpe8y1" 6 | path="res://.godot/imported/Placeholder.png-30ac785a3c5b1e5af09dc50b4f4296ca.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Placeholder.png" 14 | dest_files=["res://.godot/imported/Placeholder.png-30ac785a3c5b1e5af09dc50b4f4296ca.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /database/troops/test_troop.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Resource" script_class="Troops" load_steps=5 format=3 uid="uid://d0noky2jlhk4u"] 2 | 3 | [ext_resource type="Script" uid="uid://bqvsfh10bn27f" path="res://database/troops/enemy_group.gd" id="1_6o0q3"] 4 | [ext_resource type="Script" uid="uid://rub0awaryv0r" path="res://database/troops/troops_template.gd" id="1_abc12"] 5 | [ext_resource type="PackedScene" uid="uid://bmu11p8vvo71p" path="res://battle-manager/enemy.tscn" id="1_tl6uc"] 6 | 7 | [sub_resource type="Resource" id="Resource_tl6uc"] 8 | script = ExtResource("1_6o0q3") 9 | group_name = "Test" 10 | enemy_scenes = Array[PackedScene]([ExtResource("1_tl6uc"), ExtResource("1_tl6uc")]) 11 | metadata/_custom_type_script = "uid://bqvsfh10bn27f" 12 | 13 | [resource] 14 | script = ExtResource("1_abc12") 15 | troop_name = "Test Troop" 16 | troop_description = "A test troop for demonstration" 17 | enemy_group = SubResource("Resource_tl6uc") 18 | -------------------------------------------------------------------------------- /assets/images/icons/Asset 11.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://desmmvfvehe3b" 6 | path="res://.godot/imported/Asset 11.png-2ca65f17b3c4c7a7dccf8c85d343786c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/images/icons/Asset 11.png" 14 | dest_files=["res://.godot/imported/Asset 11.png-2ca65f17b3c4c7a7dccf8c85d343786c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/images/icons/Asset 12.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://diw6gajb1i3np" 6 | path="res://.godot/imported/Asset 12.png-e69436aa2f6c881aba3bdc7589926211.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/images/icons/Asset 12.png" 14 | dest_files=["res://.godot/imported/Asset 12.png-e69436aa2f6c881aba3bdc7589926211.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/images/icons/Asset 87.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://eonss5j5korp" 6 | path="res://.godot/imported/Asset 87.png-b7e0d77b98101770c0c5e4d61cc3af20.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/images/icons/Asset 87.png" 14 | dest_files=["res://.godot/imported/Asset 87.png-b7e0d77b98101770c0c5e4d61cc3af20.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /battle-manager/scripts/damage_number.gd: -------------------------------------------------------------------------------- 1 | class_name DamageNumber 2 | extends RichTextLabel 3 | 4 | @export var rise_speed:float = -25.0 5 | @export_range(0.1, 10.0, 0.1) var frequency:float = 10.0 6 | @export var amplitude:float = 1.0 7 | 8 | @export var fade_time:float = 1.5 9 | var time_alive:float = 0.0 10 | var started_fade:bool = false 11 | var value:int = 10 12 | 13 | func _ready() -> void: 14 | text = "[b]" + str(value) + "[/b]" 15 | 16 | func _process(delta:float) -> void: 17 | time_alive += delta 18 | position.y += delta * rise_speed 19 | position.x += get_oscillation(time_alive) 20 | if !started_fade: 21 | _fade_out() 22 | 23 | func _fade_out() -> void: 24 | var fade_out_tween:Tween = get_tree().create_tween() 25 | fade_out_tween.tween_property(self, "modulate:a", 0.0, fade_time) 26 | await fade_out_tween.finished 27 | call_deferred("queue_free") 28 | 29 | func get_oscillation(time:float) -> float: 30 | return sin(time * frequency) * amplitude 31 | -------------------------------------------------------------------------------- /assets/turnbasedcombatthumbnail.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dj0y0778m7v63" 6 | path="res://.godot/imported/turnbasedcombatthumbnail.png-be4205d1db7126f04fd4945334fb458c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/turnbasedcombatthumbnail.png" 14 | dest_files=["res://.godot/imported/turnbasedcombatthumbnail.png-be4205d1db7126f04fd4945334fb458c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /assets/images/turnbasedcombatthumbnail.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://clod0jbktndxk" 6 | path="res://.godot/imported/turnbasedcombatthumbnail.png-cbda9acc7435b15b7d6a7a6f0dfa9e0b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/images/turnbasedcombatthumbnail.png" 14 | dest_files=["res://.godot/imported/turnbasedcombatthumbnail.png-cbda9acc7435b15b7d6a7a6f0dfa9e0b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://isjjmhovpspw" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /Placeholder.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://i6v7uo8qdg1d" 6 | path="res://.godot/imported/Placeholder.svg-fd77a570b41e30d4615a14c06020e6c3.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://Placeholder.svg" 14 | dest_files=["res://.godot/imported/Placeholder.svg-fd77a570b41e30d4615a14c06020e6c3.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cute Fame Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battler_info.gd: -------------------------------------------------------------------------------- 1 | extends VBoxContainer 2 | 3 | # Declare the progress bar node 4 | @onready var name_label = $PlayerNameLabel 5 | @onready var health_bar = $PlayerHealthBar 6 | @onready var sp_bar = $PlayerSPBar 7 | 8 | func add_character(character: Node): 9 | if character: 10 | show() 11 | update_character_info(character) 12 | 13 | 14 | func update_character_info(character: Node): 15 | if character and is_instance_valid(character): 16 | name_label.text = character.character_name 17 | health_bar.max_value = character.max_health 18 | health_bar.value = character.current_health 19 | 20 | func update_player_stats(character: Node): 21 | if character and is_instance_valid(character): 22 | $PlayerNameLabel.text = character.character_name 23 | $PlayerHealthBar.max_value = character.max_health 24 | $PlayerHealthBar.value = character.current_health 25 | sp_bar.max_value = character.max_sp 26 | sp_bar.value = character.current_sp 27 | 28 | func update_enemy_stats(character: Node): 29 | if character and is_instance_valid(character): 30 | name_label.text = character.character_name 31 | health_bar.max_value = character.max_health 32 | health_bar.value = character.current_health 33 | -------------------------------------------------------------------------------- /battle-manager/battlehud/item_button.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | signal item_selected(item: Item) 4 | 5 | @export var current_item: Item: 6 | set(value): 7 | print("Setting item resource:", value) 8 | current_item = value 9 | if is_inside_tree(): 10 | _update_display() 11 | var count:int = 1 12 | 13 | func _ready(): 14 | print("Button _ready called") 15 | if current_item: 16 | print("Initial item: ", current_item) 17 | _update_display() 18 | 19 | func setup(item: Item, amount:int = 1) -> void: 20 | print("Setup called with item: ", item) 21 | current_item = item 22 | count = amount 23 | _update_display() 24 | 25 | func _update_display() -> void: 26 | print("Updating display for current_item:", current_item) 27 | var container = $HBoxContainer 28 | var name_label = container.get_node_or_null("HBox#ItemName") 29 | var count_label = container.get_node_or_null("HBox#ItemCount") 30 | var icon_rect = container.get_node_or_null("HBox#Icon") 31 | 32 | if current_item is Item: 33 | name_label.text = current_item.item_name 34 | count_label.text = "x" + str(count) 35 | if current_item.icon: 36 | icon_rect.texture = current_item.icon 37 | 38 | func _pressed(): 39 | print("Button pressed, emitting skill:", current_item) 40 | item_selected.emit(current_item) 41 | -------------------------------------------------------------------------------- /database/troops/troops_template.gd: -------------------------------------------------------------------------------- 1 | class_name Troops 2 | extends Resource 3 | 4 | 5 | enum Formation { 6 | FRONT_ROW, 7 | TRIANGLE, 8 | CIRCLE_PLAYER, 9 | CUSTOM_MARKERS 10 | } 11 | 12 | @export var troop_name: String = "" 13 | @export var troop_description: String = "" 14 | @export var enemy_group: EnemyGroup = null # Use EnemyGroup resource instead of Array[String] 15 | @export var formation: Formation = Formation.FRONT_ROW 16 | @export var damage_reduction: float = 1.0 17 | 18 | # Helper function to get the array of loaded enemy scenes 19 | func get_enemy_scenes() -> Array: 20 | if not enemy_group: 21 | push_warning("No enemy group assigned to troop: ", troop_name) 22 | return [] 23 | 24 | var loaded_scenes: Array = [] 25 | for scene in enemy_group.enemy_scenes: 26 | if scene: 27 | loaded_scenes.append(scene) 28 | else: 29 | push_warning("Null enemy scene in group: ", enemy_group.group_name) 30 | return loaded_scenes 31 | 32 | # Get formation display name 33 | func get_formation_name() -> String: 34 | match formation: 35 | Formation.FRONT_ROW: 36 | return "Front Row" 37 | Formation.TRIANGLE: 38 | return "Triangle" 39 | Formation.CIRCLE_PLAYER: 40 | return "Circle Around Player" 41 | Formation.CUSTOM_MARKERS: 42 | return "Custom Markers" 43 | _: 44 | return "Unknown" 45 | -------------------------------------------------------------------------------- /database/items/items_template.gd: -------------------------------------------------------------------------------- 1 | class_name Item 2 | extends Resource 3 | 4 | enum TARGETS_TYPES { 5 | MULTIPLE_TARGETS = 0, 6 | SINGLE_TARGETS = 1, 7 | ALL_TARGETS = 2 8 | } 9 | 10 | ## General Information 11 | @export var item_name : String = "" 12 | @export var description : String = "" 13 | @export var item_tags : String = "" 14 | @export var icon : Resource 15 | @export var is_battle_item:bool = true 16 | @export var is_key_item:bool = false: 17 | set(key_item): 18 | is_key_item = key_item 19 | is_stackable = !key_item 20 | @export var is_stackable:bool = true 21 | 22 | ## Mechanics 23 | @export var base_power : int = 0 24 | @export var critical_rate : int = 0 25 | @export var effectiveness : int = 100 26 | @export var element : GlobalBattleSettings.Elements = GlobalBattleSettings.Elements.Physical 27 | @export var target_type : TARGETS_TYPES = TARGETS_TYPES.SINGLE_TARGETS 28 | @export var skill_type : GlobalBattleSettings.ItemTypes = GlobalBattleSettings.ItemTypes.HEALING 29 | 30 | ## Effects 31 | @export var hp_delta : int = 0 # Positive for healing, negative for HP cost 32 | @export var sp_delta : int = 0 # Positive for SP restore, negative for SP cost 33 | @export var effect_type : String = "Damage" # Damage, Heal, Buff, etc. 34 | 35 | ## Animation & Visuals 36 | @export var animation_name : String = "" 37 | -------------------------------------------------------------------------------- /gettingstarted.md: -------------------------------------------------------------------------------- 1 | ## Hello. 2 | ### This provided file is for newcomers to get started on the project, This will be updated to help those people very soon, So keep in check of the repo and it's resources. 3 | 4 | This plugin is provided with it's listed license. 5 | # Steps to utilize/download the template: 6 | First, let's assume you need to set up and install the repository first. You may want to use the GitHub desktop client, Or if that does not work. You can simply open a terminal in any folder, Then git clone the repository with the provided arguments. 7 | ```git clone https://github.com/Cute-Fame-Studio/3D-TurnBasedCombat``` 8 | 9 | ## Step 2: 10 | Start up your game engine (Godot 4+) and manually add the project, You can possibly right-click and open the project's main folder depending on how you configured Godot. Then, Once the project is opened, The easiest way to test and use the template, Is with. 11 | 12 | ## Step 3 13 | Load up the file in: ```res://maps/battle_scenes/test.tscn```, After it's opened, You begin to see the project in its current working state. The following is up to you, I encourage you to take the time to submit pull requests and code reviews of what was used to make the battle system. You can as well as follows along with tasks waiting to be implemented in the project tab of the repository: [TaskBoard/ToDo](https://github.com/orgs/Cute-Fame-Studio/projects/2) 14 | 15 | # Thank you for listening! 16 | -------------------------------------------------------------------------------- /assets/globals/save-system/save_loader.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var save_path # folder/file path to location of save files 4 | var save_folder # folder location of save files 5 | var save_file # save file name and extension (example: game_save.tres) 6 | var game_session: SavedGame 7 | 8 | # Save Methods 9 | func save_game(): 10 | save_all_data() 11 | var dir = DirAccess.open(save_path) 12 | if not dir.dir_exists(save_folder): dir.make_dir(save_folder) # if save folder doesn't exist, create it 13 | ResourceSaver.save(game_session, save_path + save_folder + save_file) 14 | 15 | func save_all_data(): 16 | gather_battlers() 17 | 18 | func gather_battlers(): 19 | get_tree().call_group("battler_data", "on_save_game", game_session.char_data) 20 | 21 | # Load Methods 22 | func load_game(): 23 | var game_save = ResourceLoader.load(save_path + save_folder + save_file) as SavedGame 24 | if game_save == null: print("No save file found."); return # exit function if no save 25 | 26 | load_all_data(game_save) 27 | 28 | func load_all_data(game_save: SavedGame): 29 | load_battler_data(game_save) 30 | 31 | func load_battler_data(game_save: SavedGame): 32 | if (game_save.char_data == null): print("No save data found."); return # exit function if no save 33 | 34 | battler_group(game_save) 35 | 36 | # Load Data Types 37 | func battler_group(game_save: SavedGame): 38 | get_tree().call_group("battler_data", "on_load_game", game_save.char_data) 39 | -------------------------------------------------------------------------------- /battle-manager/battlehud/item-button-preset.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://52sutka7rtgo"] 2 | 3 | [ext_resource type="Script" uid="uid://ec0y25we1j16" path="res://battle-manager/battlehud/item_button.gd" id="1_d51ap"] 4 | [ext_resource type="Texture2D" uid="uid://desmmvfvehe3b" path="res://assets/images/icons/Asset 11.png" id="3_8hd16"] 5 | 6 | [node name="ItemRow" type="Button"] 7 | custom_minimum_size = Vector2(259, 40) 8 | anchors_preset = -1 9 | anchor_right = 0.224826 10 | anchor_bottom = 0.0617284 11 | grow_vertical = 2 12 | size_flags_horizontal = 3 13 | size_flags_vertical = 3 14 | script = ExtResource("1_d51ap") 15 | metadata/_edit_use_anchors_ = true 16 | 17 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 18 | layout_mode = 1 19 | anchors_preset = 14 20 | anchor_top = 0.5 21 | anchor_right = 1.0 22 | anchor_bottom = 0.5 23 | offset_top = -16.0 24 | offset_bottom = 16.0 25 | grow_horizontal = 2 26 | grow_vertical = 2 27 | 28 | [node name="HBox#Icon" type="TextureRect" parent="HBoxContainer"] 29 | custom_minimum_size = Vector2(32, 32) 30 | layout_mode = 2 31 | texture = ExtResource("3_8hd16") 32 | expand_mode = 1 33 | stretch_mode = 5 34 | 35 | [node name="HBox#ItemName" type="Label" parent="HBoxContainer"] 36 | layout_mode = 2 37 | size_flags_horizontal = 3 38 | text = "Item Name" 39 | 40 | [node name="HBox#ItemCount" type="Label" parent="HBoxContainer"] 41 | custom_minimum_size = Vector2(80, 0) 42 | layout_mode = 2 43 | size_flags_horizontal = 3 44 | text = "Count" 45 | horizontal_alignment = 2 46 | -------------------------------------------------------------------------------- /replace/regular_map/backtogame.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://c4ar16ynql13b"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://tqowxvpoq16k" path="res://replace/regular_map/playableplayer/playableplayer.tscn" id="1_vp4s0"] 4 | 5 | [sub_resource type="BoxMesh" id="BoxMesh_kebba"] 6 | 7 | [sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_j7h1g"] 8 | 9 | [node name="Backtogame" type="Node3D"] 10 | 11 | [node name="Label3D" type="Label3D" parent="."] 12 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.30019, -7.75121) 13 | text = "Back To The Game!" 14 | font_size = 302 15 | 16 | [node name="MeshInstance3D" type="MeshInstance3D" parent="."] 17 | transform = Transform3D(19.3718, 0, 0, 0, 0.246551, 0, 0, 0, 13.3728, 0, 0, 0) 18 | mesh = SubResource("BoxMesh_kebba") 19 | 20 | [node name="StaticBody3D" type="StaticBody3D" parent="MeshInstance3D"] 21 | 22 | [node name="CollisionShape3D" type="CollisionShape3D" parent="MeshInstance3D/StaticBody3D"] 23 | shape = SubResource("WorldBoundaryShape3D_j7h1g") 24 | 25 | [node name="basic-player" parent="." instance=ExtResource("1_vp4s0")] 26 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.214493, 0) 27 | 28 | [node name="Camera3D" type="Camera3D" parent="."] 29 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.82092, 8.62277) 30 | 31 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] 32 | transform = Transform3D(1, 0, 0, 0, -0.409493, 0.912313, 0, -0.912313, -0.409493, 0, 9.12673, 0) 33 | directional_shadow_mode = 0 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## 3D Turn Based Battle System 2 | 3 | Animation state's will be used for basic animation cycling when attacking, Additionally you'll need to add a keyframe in your own animations. 4 | 5 | 6 | 7 | These are using retargted animations, Just to show that they are possible with a 3d battle system like this. 8 | 9 | ## Similar Systems. 10 | 11 | The development of the battle system itself is being approached like the popular engine, RPG Maker! 12 | 13 | With modular script's to allow inheritance to battler's and reused data/ai/functionality with all players in battle. 14 | 15 | Though, With that being said! This project shouldn't be overcomplicated for a 2d implementation, It just won't be done by this same project, As the focus here has been commited to making a useable battle system for game's akin to Persona, Final Fantasy, And Pokémon. 16 | 17 | ## Project Description: 18 | This battle system was gonna be used in my own projects, But as a collabrative effort, It will be open sourced, And maintained by the owner of this github repo. (The Game In Question Is Still In Development) [- Cute Fame 3D (Working Title)] 19 | 20 | ### Combat Basis (TBS System). 21 | 22 | ## Getting Started/Project TaskBoard: 23 | 24 | ### [Getting Started](https://github.com/Cute-Fame-Studio/3D-TurnBasedCombat/blob/3a52040dce2baa93689853395029f7fb2f1c978d/gettingstarted.md) 25 | ### [TaskBoard/ToDo](https://github.com/orgs/Cute-Fame-Studio/projects/2) 26 | 27 | Godot 4.0 - 4.4 (Stable). 28 | 29 | ## Current Progress 30 | 31 | -------------------------------------------------------------------------------- /battle-manager/battlehud/skill-button-preset.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://xtvr2wp8ojvb"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://desmmvfvehe3b" path="res://assets/images/icons/Asset 11.png" id="1_1gef3"] 4 | [ext_resource type="Script" uid="uid://bp8uc3di8qowc" path="res://battle-manager/battlehud/skill_button.gd" id="1_rlbxm"] 5 | [ext_resource type="Resource" uid="uid://ddoo4r86oyqyr" path="res://database/skills/fireball.tres" id="2_f222w"] 6 | 7 | [node name="SkillRow" type="Button"] 8 | custom_minimum_size = Vector2(259, 40) 9 | anchors_preset = -1 10 | anchor_right = 0.224826 11 | anchor_bottom = 0.0617284 12 | grow_vertical = 2 13 | size_flags_horizontal = 3 14 | size_flags_vertical = 3 15 | script = ExtResource("1_rlbxm") 16 | current_skill = ExtResource("2_f222w") 17 | metadata/_edit_use_anchors_ = true 18 | 19 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 20 | layout_mode = 1 21 | anchors_preset = 15 22 | anchor_right = 1.0 23 | anchor_bottom = 1.0 24 | grow_horizontal = 2 25 | grow_vertical = 2 26 | 27 | [node name="HBox#Icon" type="TextureRect" parent="HBoxContainer"] 28 | clip_contents = true 29 | custom_minimum_size = Vector2(32, 32) 30 | layout_mode = 2 31 | texture = ExtResource("1_1gef3") 32 | expand_mode = 1 33 | stretch_mode = 5 34 | 35 | [node name="HBox#SkillName" type="Label" parent="HBoxContainer"] 36 | layout_mode = 2 37 | size_flags_horizontal = 3 38 | text = "Skill Name" 39 | 40 | [node name="HBox#SkillCost" type="Label" parent="HBoxContainer"] 41 | custom_minimum_size = Vector2(80, 0) 42 | layout_mode = 2 43 | size_flags_horizontal = 3 44 | text = "15 SP" 45 | horizontal_alignment = 2 46 | -------------------------------------------------------------------------------- /assets/globals/battle-settings/GlobalBattleSettings.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Must be set/used to get player turn. 4 | var activeBattler 5 | 6 | # These variables do not NEED to have stuff filled out. 7 | # These variables would/can be a place to define what sound an animation player will play. 8 | # It's goal is to not keep data hard to change, And keeping reuseability. 9 | var hit_sound: AudioStream 10 | var critical_sound: AudioStream 11 | var miss_sound : AudioStream 12 | var escape_sound: AudioStream 13 | var enemy_defeat_sound: AudioStream = null 14 | var boss_defeat_sound: AudioStream = null 15 | var ally_party: int = 0 16 | 17 | # Set damage calculation type (based on other game damage calcs) 18 | var Global_Damage_Calc_Type : Damage_Calc_Type = Damage_Calc_Type.PKMN 19 | 20 | enum Elements { 21 | Physical = 0, 22 | EARTH = 1, 23 | AIR = 2, 24 | FIRE = 3, 25 | WATER = 4, 26 | MAGIC = 5, 27 | NONE = 6, # In theory this would ensure everyone can be effected. 28 | ENDLIST} 29 | 30 | enum SkillTypes { 31 | SKILLS = 0, 32 | HEALING = 1, #Placerholder. 33 | HEALTHREDUCTIONATTACKS = 2, 34 | ENDLIST} 35 | 36 | enum ItemTypes { 37 | HEALING = 0, 38 | THROWABLE = 1, 39 | BUFFS = 2, 40 | ENDLIST} 41 | 42 | enum WeaponTypes { 43 | HANDS = 0, 44 | SWORD = 1, 45 | AXE = 2, 46 | BOW = 3, 47 | CLAWS = 4, 48 | UNDEFINED = 5, 49 | ENDLIST} 50 | 51 | enum ArmorTypes { 52 | REGULARARMOR = 0, 53 | LIGHTARMOR = 1, 54 | MAGICARMOR = 2, 55 | HEAVYARMOR = 3, 56 | METALARMOR = 4, 57 | ENDLIST} 58 | 59 | enum Difficulties { 60 | EASY = 0, 61 | NORMAL = 1, 62 | HARD = 2, 63 | ENDLIST} 64 | 65 | enum Damage_Calc_Type { 66 | PKMN = 0, 67 | DRGNQST = 1 68 | } 69 | # Start Functions down here. 70 | 71 | 72 | # Find the first battler, This may be set to allow anyone to go first later. For situations 73 | # Where they get hit first before battle. 74 | func set_active_battler(character): 75 | activeBattler = character 76 | -------------------------------------------------------------------------------- /database/skills/skills_template.gd: -------------------------------------------------------------------------------- 1 | class_name Skill 2 | extends Resource 3 | 4 | enum TARGETS_TYPES { 5 | SINGLE_ENEMY, # Target one enemy 6 | MULTIPLE_ENEMIES, # Target all enemies 7 | SINGLE_ALLY, # Target one ally 8 | MULTIPLE_ALLIES, # Target all allies 9 | SELF_TARGET, # Target only self 10 | ALL_TARGETS # Target everyone (rare, but useful for some effects) 11 | } 12 | 13 | enum EFFECT_TYPE { # Rename this. 14 | DAMAGE = 0, 15 | HEAL = 1, 16 | BUFF = 2 17 | } 18 | 19 | ## General Information 20 | @export var skill_name : String = "" 21 | @export var description : String = "" 22 | @export var skill_color : String = "" 23 | @export var skill_tags : String = "" 24 | @export var icon : Resource 25 | 26 | ## Mechanics 27 | @export var base_power : int = 0 28 | @export var critical_rate : int = 0 29 | @export var hit_chance : int = 100 30 | @export var sp_cost : int = 0 31 | @export var hp_cost : int = 0 32 | @export var element : GlobalBattleSettings.Elements = GlobalBattleSettings.Elements.Physical 33 | @export var target_type : TARGETS_TYPES = TARGETS_TYPES.SINGLE_ENEMY 34 | @export var skill_type : GlobalBattleSettings.SkillTypes = GlobalBattleSettings.SkillTypes.SKILLS 35 | 36 | ## Effects 37 | @export var hp_delta : int = 0 # Positive for healing, negative for HP cost 38 | @export var sp_delta : int = 0 # Positive for SP restore, negative for SP cost 39 | @export var effect_type : EFFECT_TYPE = EFFECT_TYPE.DAMAGE # Damage, Heal, Buff, etc. 40 | 41 | ## Animation & Visuals 42 | @export var animation_name : String = "" 43 | 44 | @export var applies_state: State 45 | @export var state_apply_chance: int = 100 # Percentage chance to apply state 46 | 47 | func can_use(user: Node) -> bool: 48 | # Change current_hp to current_health to match battler_ally.gd 49 | if hp_cost > 0 and user.current_health <= hp_cost: 50 | return false 51 | if sp_cost > 0 and user.current_sp <= sp_cost: 52 | return false 53 | return true 54 | 55 | func apply_costs(user: Node) -> void: 56 | if hp_cost > 0: 57 | user.current_health -= hp_cost 58 | if sp_cost > 0: 59 | user.current_sp -= sp_cost 60 | -------------------------------------------------------------------------------- /battle-manager/battlehud/skill_button.gd: -------------------------------------------------------------------------------- 1 | extends Button 2 | 3 | signal skill_selected(skill: Resource) 4 | 5 | @export var current_skill: Resource: 6 | set(value): 7 | print("Setting skill resource:", value) 8 | current_skill = value 9 | if is_inside_tree(): 10 | if value is Skill: 11 | print("Setting Skill resource with name:", value.skill_name) 12 | elif value is CharacterAbilities: 13 | print("Setting CharacterAbilities resource with name:", value.ability_name) 14 | _update_display() 15 | 16 | func _ready(): 17 | print("Button _ready called") 18 | if current_skill: 19 | print("Initial skill: ", current_skill) 20 | _update_display() 21 | 22 | func setup(skill: Resource) -> void: 23 | print("Setup called with skill: ", skill) 24 | current_skill = skill 25 | _update_display() 26 | 27 | func _update_display() -> void: 28 | print("Updating display for current_skill:", current_skill) 29 | var container = $HBoxContainer 30 | var name_label = container.get_node_or_null("HBox#SkillName") 31 | var cost_label = container.get_node_or_null("HBox#SkillCost") 32 | var icon_rect = container.get_node_or_null("HBox#Icon") 33 | 34 | if current_skill is Skill: 35 | name_label.text = current_skill.skill_name 36 | cost_label.text = str(current_skill.sp_cost) + " SP" 37 | if current_skill.icon: 38 | icon_rect.texture = current_skill.icon 39 | 40 | # Check if skill can be used and disable button if not 41 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 42 | var can_use = true 43 | if battle_manager and battle_manager.current_character: 44 | can_use = current_skill.can_use(battle_manager.current_character) 45 | 46 | disabled = !can_use 47 | if !can_use: 48 | modulate = Color(0.5, 0.5, 0.5, 0.7) # Gray out disabled skills 49 | else: 50 | modulate = Color.WHITE 51 | elif current_skill is CharacterAbilities: 52 | # Here's where we need to map abilities to proper skill names 53 | var skill_name = "" 54 | match current_skill.ability_name.to_lower(): 55 | "damage": 56 | skill_name = "Fireball" 57 | "heal": 58 | skill_name = "Heal" 59 | _: 60 | skill_name = current_skill.ability_name 61 | 62 | name_label.text = skill_name 63 | cost_label.text = str(int(current_skill.number_value)) + " SP" 64 | 65 | func _pressed(): 66 | print("Button pressed, emitting skill:", current_skill) 67 | skill_selected.emit(current_skill) 68 | -------------------------------------------------------------------------------- /battle-manager/enemy.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://bmu11p8vvo71p"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://bcqxqucgohr4x" path="res://battle-manager/allies/y_bot-ally.tscn" id="1_yb6qq"] 4 | [ext_resource type="Script" uid="uid://d2mintfjxyqei" path="res://BattlerStats.gd" id="2_abxtc"] 5 | 6 | [sub_resource type="Resource" id="Resource_pggsg"] 7 | script = ExtResource("2_abxtc") 8 | character_name = "Enemy" 9 | metadata/_custom_type_script = "uid://d2mintfjxyqei" 10 | 11 | [sub_resource type="ViewportTexture" id="ViewportTexture_4gyqm"] 12 | viewport_path = NodePath("DamageIndicator/DamageIndicator") 13 | 14 | [sub_resource type="ViewportTexture" id="ViewportTexture_qi2p4"] 15 | viewport_path = NodePath("HealthBar/SubViewport") 16 | 17 | [node name="Enemy" instance=ExtResource("1_yb6qq")] 18 | stats = SubResource("Resource_pggsg") 19 | team = 1 20 | 21 | [node name="GeneralSkeleton" parent="Armature" index="0"] 22 | bones/0/position = Vector3(-0.0017119393, 0.9463986, -0.087445915) 23 | bones/1/rotation = Quaternion(-1.5308156e-05, 0.01583408, -0.0012270863, 0.9998739) 24 | bones/2/rotation = Quaternion(0.02301413, 0.015911365, 0.0027388267, 0.9996048) 25 | bones/3/rotation = Quaternion(-0.045068186, 0.0152217485, -0.009950133, 0.9988184) 26 | bones/4/rotation = Quaternion(0.022309057, -0.047138598, 0.0070193564, 0.99861455) 27 | bones/5/rotation = Quaternion(0.01499479, 0.045454368, -0.0048674233, 0.998842) 28 | bones/8/rotation = Quaternion(0.016108433, 0.8153838, -0.53816235, -0.21277031) 29 | bones/9/rotation = Quaternion(0.31069562, -0.6345554, 0.35013422, 0.614991) 30 | bones/10/rotation = Quaternion(-0.09396038, 0.6163155, 0.012964428, 0.78176636) 31 | bones/32/rotation = Quaternion(0.062504366, 0.7945442, -0.57565796, 0.18278584) 32 | bones/33/rotation = Quaternion(0.2933226, 0.6434761, -0.33055708, 0.62500584) 33 | bones/34/rotation = Quaternion(-0.08720445, -0.5709572, -0.053365737, 0.8145891) 34 | bones/55/rotation = Quaternion(-0.0004829365, 0.3390976, 0.9397295, -0.043830123) 35 | bones/56/rotation = Quaternion(0.00051486224, 0.9674287, -0.25304744, -0.0069642295) 36 | bones/57/rotation = Quaternion(-0.027444124, 0.69867694, -0.71450436, 0.024102258) 37 | bones/60/rotation = Quaternion(0.0011318345, 0.16462722, 0.98550755, 0.040883273) 38 | bones/61/rotation = Quaternion(-0.0005259753, 0.96598256, -0.2585089, 0.0071145296) 39 | bones/62/rotation = Quaternion(-0.13364375, 0.5386045, -0.7825855, -0.2821429) 40 | 41 | [node name="DamageIndicatorSprite" parent="DamageIndicator" index="1"] 42 | texture = SubResource("ViewportTexture_4gyqm") 43 | 44 | [node name="BattlerHealthBar" parent="HealthBar" index="0"] 45 | texture = SubResource("ViewportTexture_qi2p4") 46 | -------------------------------------------------------------------------------- /battle-manager/ai_manager.gd: -------------------------------------------------------------------------------- 1 | ## [color=green]AI Manager[/color] 2 | ## [br][br] 3 | ## This class manages AI action selection based on Skills available to the battler, 4 | ## the battler's Intelligence, and AI Type. 5 | ## [br][br] 6 | ## Logic to handle decision-making for NPCs in combat should be put in here. 7 | extends Node 8 | 9 | 10 | # Pulled from Enemy (deprecated/removed class) 11 | func choose_action(this:Battler, opposing_team:Array, _ally_team:Array, battle_manager:BattleManager) -> void: 12 | battle_manager.current_battler = this 13 | match this.ai_type: 14 | Battler.AIType.AGGRESSIVE: 15 | aggressive_action(this, opposing_team, battle_manager) 16 | Battler.AIType.DEFENSIVE: 17 | defensive_action(this, opposing_team, battle_manager) 18 | 19 | func aggressive_action(this:Battler, players: Array, battle_manager:BattleManager): 20 | var target = choose_target(this, players) 21 | if target: 22 | battle_manager.current_target = target 23 | battle_manager.battler_attacking = true 24 | this.attack_anim(target) 25 | 26 | func defensive_action(this:Battler, players: Array, battle_manager:BattleManager): 27 | if float(this.current_health) / this.max_health < 0.3: 28 | this.defend() 29 | else: 30 | aggressive_action(this, players, battle_manager) 31 | 32 | func choose_target(this:Battler, targets: Array) -> Node: 33 | print("=== AI TARGET SELECTION ===") 34 | print("AI Character: ", this.character_name) 35 | print("AI Intelligence: ", this.intelligence) 36 | print("Available targets: ", targets.size()) 37 | for target in targets: 38 | if target is Battler: 39 | print(" - ", target.character_name, " (HP: ", target.current_health, "/", target.max_health, ")") 40 | 41 | # Filter out defeated targets 42 | var valid_targets = [] 43 | for target:Battler in targets: 44 | if target != this and !target.is_defeated(): 45 | valid_targets.append(target) 46 | 47 | if valid_targets.is_empty(): 48 | print("No valid targets found!") 49 | return null 50 | 51 | # Use intelligence to determine targeting strategy 52 | var intelligence = this.intelligence 53 | var rand_value = randi() % 100 54 | 55 | print("Random value: ", rand_value, " vs Intelligence: ", intelligence) 56 | 57 | if rand_value < intelligence: 58 | # Smart targeting - choose based on strategy 59 | var chosen = get_weakest_target(valid_targets) 60 | print("Smart targeting chose: ", chosen.character_name if chosen else "NULL") 61 | return chosen 62 | else: 63 | # Random targeting - choose any valid target 64 | var chosen = valid_targets[randi() % valid_targets.size()] 65 | print("Random targeting chose: ", chosen.character_name if chosen else "NULL") 66 | return chosen 67 | 68 | func get_weakest_target(targets: Array) -> Node: 69 | var weakest = null 70 | for target:Battler in targets: 71 | if target != self and (weakest == null or target.current_health < weakest.current_health): 72 | weakest = target 73 | return weakest 74 | -------------------------------------------------------------------------------- /battle-manager/scripts/experience.gd: -------------------------------------------------------------------------------- 1 | class_name Experience 2 | extends Node 3 | 4 | @export_group("Player") 5 | # Total amount of experience required to hit the next level - updated each time do_level_up() function runs 6 | @export var exp_to_level = 100; 7 | # Multiplier for updating exp_to_level - updated during do_level_up() function to slowly scale down until equals 1 8 | @export var exp_to_level_multiplier = 5; 9 | # Value which scales down the exp_to_level_multiplier 10 | @export var exp_multi_reduction = 0.2; 11 | # Character level that exp_multi_reduction starts scaling down exp_to_level_multiplier 12 | @export var exp_reduce_start_level = 2; 13 | 14 | # Total accumulted exp - use add_exp(amount: int) function to increase 15 | var exp_total = 0; 16 | # Current character level - use do_level_up() function to increase 17 | var char_level = 1; 18 | 19 | @export_group("Enemy") 20 | # Amount of exp enemy grants when killed 21 | @export var expOnKill = 0; 22 | 23 | # 24 | # Experience Handling 25 | # 26 | # Active Call - Used in other scripts to interact with experience 27 | # Passive Call - Only used internally within experience script 28 | # Get Methods - Used to output numeric values without changing them 29 | # Set Methods - Used to alter internal experience values, if needed 30 | # 31 | 32 | # Active Call 33 | # Use anytime exp is increased 34 | func add_exp(amount: int): 35 | exp_total += amount 36 | check_level_up() 37 | 38 | # 39 | # Level Handling 40 | # 41 | 42 | # Passive Call 43 | # Runs anytime add_exp(amount: int) is called 44 | func check_level_up(): 45 | if exp_total >= get_exp_next_level(): 46 | do_level_up() 47 | return true 48 | else: 49 | return false 50 | 51 | # Passive Call 52 | # Activates when check_level_up() determines a level increase 53 | func do_level_up(): 54 | char_level += 1 55 | exp_to_level *= exp_to_level_multiplier 56 | 57 | print("%s gains a level!" % [get_parent().character_name]) 58 | print("%s is now level %d."% [get_parent().character_name, char_level]) 59 | 60 | if char_level > exp_reduce_start_level: 61 | exp_to_level_multiplier -= exp_multi_reduction 62 | if exp_to_level_multiplier < 1: 63 | exp_to_level_multiplier = 1 64 | exp_multi_reduction = 0 65 | 66 | # 67 | # Get Methods 68 | # 69 | 70 | # Outputs value still needed to hit the next char_level 71 | func get_exp_to_level(): 72 | return get_exp_next_level() - exp_total 73 | 74 | # Outputs total value needed to hit the next char_level 75 | func get_exp_next_level(): 76 | return exp_to_level 77 | 78 | # Outputs current exp_total 79 | func get_total_exp(): 80 | return exp_total 81 | 82 | # Outputs current char_level 83 | func get_current_level(): 84 | return char_level 85 | 86 | # Outputs experience enemy gives when killed - expOnKill 87 | func get_exp_on_kill(): 88 | return expOnKill 89 | 90 | # 91 | # Set Methods 92 | # 93 | 94 | # Used to set exp_total to a precise value - not generally used 95 | func set_exp_total(amount: int): 96 | exp_total = amount 97 | -------------------------------------------------------------------------------- /battle-manager/allies/Ybot_Mixamo.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="BoneMap" load_steps=2 format=3 uid="uid://dn0ncjyi7lbwj"] 2 | 3 | [sub_resource type="SkeletonProfileHumanoid" id="SkeletonProfileHumanoid_1mcbj"] 4 | 5 | [resource] 6 | profile = SubResource("SkeletonProfileHumanoid_1mcbj") 7 | bonemap = null 8 | bone_map/Root = &"" 9 | bone_map/Hips = &"mixamorig_Hips" 10 | bone_map/Spine = &"mixamorig_Spine" 11 | bone_map/Chest = &"mixamorig_Spine1" 12 | bone_map/UpperChest = &"mixamorig_Spine2" 13 | bone_map/Neck = &"mixamorig_Neck" 14 | bone_map/Head = &"mixamorig_Head" 15 | bone_map/LeftEye = &"" 16 | bone_map/RightEye = &"" 17 | bone_map/Jaw = &"" 18 | bone_map/LeftShoulder = &"mixamorig_LeftShoulder" 19 | bone_map/LeftUpperArm = &"mixamorig_LeftArm" 20 | bone_map/LeftLowerArm = &"mixamorig_LeftForeArm" 21 | bone_map/LeftHand = &"mixamorig_LeftHand" 22 | bone_map/LeftThumbMetacarpal = &"mixamorig_LeftHandThumb2" 23 | bone_map/LeftThumbProximal = &"mixamorig_LeftHandThumb3" 24 | bone_map/LeftThumbDistal = &"mixamorig_LeftHandThumb4" 25 | bone_map/LeftIndexProximal = &"mixamorig_LeftHandIndex2" 26 | bone_map/LeftIndexIntermediate = &"mixamorig_LeftHandIndex3" 27 | bone_map/LeftIndexDistal = &"mixamorig_LeftHandIndex4" 28 | bone_map/LeftMiddleProximal = &"mixamorig_LeftHandMiddle2" 29 | bone_map/LeftMiddleIntermediate = &"mixamorig_LeftHandMiddle3" 30 | bone_map/LeftMiddleDistal = &"mixamorig_LeftHandMiddle4" 31 | bone_map/LeftRingProximal = &"mixamorig_LeftHandRing2" 32 | bone_map/LeftRingIntermediate = &"mixamorig_LeftHandRing3" 33 | bone_map/LeftRingDistal = &"mixamorig_LeftHandRing4" 34 | bone_map/LeftLittleProximal = &"mixamorig_LeftHandPinky2" 35 | bone_map/LeftLittleIntermediate = &"mixamorig_LeftHandPinky3" 36 | bone_map/LeftLittleDistal = &"mixamorig_LeftHandPinky4" 37 | bone_map/RightShoulder = &"mixamorig_RightShoulder" 38 | bone_map/RightUpperArm = &"mixamorig_RightArm" 39 | bone_map/RightLowerArm = &"mixamorig_RightForeArm" 40 | bone_map/RightHand = &"mixamorig_RightHand" 41 | bone_map/RightThumbMetacarpal = &"mixamorig_RightHandThumb2" 42 | bone_map/RightThumbProximal = &"mixamorig_RightHandThumb3" 43 | bone_map/RightThumbDistal = &"mixamorig_RightHandThumb4" 44 | bone_map/RightIndexProximal = &"mixamorig_RightHandIndex2" 45 | bone_map/RightIndexIntermediate = &"mixamorig_RightHandIndex3" 46 | bone_map/RightIndexDistal = &"mixamorig_RightHandIndex4" 47 | bone_map/RightMiddleProximal = &"mixamorig_RightHandMiddle2" 48 | bone_map/RightMiddleIntermediate = &"mixamorig_RightHandMiddle3" 49 | bone_map/RightMiddleDistal = &"mixamorig_RightHandMiddle4" 50 | bone_map/RightRingProximal = &"mixamorig_RightHandRing2" 51 | bone_map/RightRingIntermediate = &"mixamorig_RightHandRing3" 52 | bone_map/RightRingDistal = &"mixamorig_RightHandRing4" 53 | bone_map/RightLittleProximal = &"mixamorig_RightHandPinky2" 54 | bone_map/RightLittleIntermediate = &"mixamorig_RightHandPinky3" 55 | bone_map/RightLittleDistal = &"mixamorig_RightHandPinky4" 56 | bone_map/LeftUpperLeg = &"mixamorig_LeftUpLeg" 57 | bone_map/LeftLowerLeg = &"mixamorig_LeftLeg" 58 | bone_map/LeftFoot = &"mixamorig_LeftFoot" 59 | bone_map/LeftToes = &"mixamorig_LeftToeBase" 60 | bone_map/RightUpperLeg = &"mixamorig_RightUpLeg" 61 | bone_map/RightLowerLeg = &"mixamorig_RightLeg" 62 | bone_map/RightFoot = &"mixamorig_RightFoot" 63 | bone_map/RightToes = &"mixamorig_RightToeBase" 64 | -------------------------------------------------------------------------------- /battle-manager/scripts/formulas.gd: -------------------------------------------------------------------------------- 1 | class_name Formulas 2 | 3 | # Element effectiveness matrix (Must use constant values, not enum references) 4 | const ELEMENT_MATRIX = { 5 | 0: { # Physical 6 | 0: 1.0, # Physical vs Physical 7 | 1: 1.0, # Physical vs Earth 8 | 2: 1.0, # Physical vs Air 9 | 3: 1.0, # Physical vs Fire 10 | 4: 1.0, # Physical vs Water 11 | 5: 0.5 # Physical vs Magic 12 | }, 13 | 3: { # Fire 14 | 0: 1.0, # Fire vs Physical 15 | 1: 2.0, # Fire vs Earth 16 | 2: 1.0, # Fire vs Air 17 | 4: 0.5, # Fire vs Water 18 | 5: 1.0 # Fire vs Magic 19 | }, 20 | 4: { # Water 21 | 0: 1.0, # Water vs Physical 22 | 1: 0.5, # Water vs Earth 23 | 2: 1.0, # Water vs Air 24 | 3: 2.0, # Water vs Fire 25 | 5: 1.0 # Water vs Magic 26 | } 27 | # Add other elements as needed 28 | } 29 | 30 | static func physical_damage(attacker, target, damage) -> int: 31 | var offense = attacker.stats.attack + damage 32 | var defense = attacker.stats.defense 33 | var total_damage = max(0, offense-defense) 34 | return total_damage * element_wheel(attacker.stats.element, target.stats.element) 35 | 36 | static func element_wheel(attack_element, defend_element) -> float: 37 | var multiplier = 1 38 | 39 | if attack_element == defend_element + 1: 40 | multiplier = 0.5 41 | else: if attack_element == defend_element - 1: 42 | multiplier = 2 43 | 44 | return multiplier 45 | 46 | ## Calculates damage based on stats, skill, and designated global damage calculation type.[br] 47 | ## [br] 48 | ## [color=cyan]Function Parameters:[/color][br] 49 | ## [Battler] [param attacker][br] 50 | ## [Battler] [param target][br] 51 | ## [Skill] [param skill][br] 52 | ## [br] 53 | ## [color=cyan]Global Damage Calculation Types:[/color][br] 54 | ## [b]PKMN[/b] - Damage calculation similar to Gen 3+ Pokemon.[br] 55 | ## [url=https://bulbapedia.bulbagarden.net/wiki/Damage#Generation_III]Pokemon Gen 3 Damage Calculation[/url][br] 56 | ## [code]((((2.0 * attacker.exp_node.char_level) / 5.0 + 2) * skill.base_power * (atk / def)) / 50.0) + 2[/code][br] 57 | ## [br] 58 | ## [b]DRGNQST[/b] - Damage calculation similar to Dragon Quest.[br] 59 | ## [url=https://dragonquestcosmos.fandom.com/wiki/Formulas#Damage_Calculation]Dragon Quest Damage Calculation[/url][br] 60 | ## [code]((atk - (def / 2.0) + (((atk - (def/2.0) + 1.0) * randf_range(0, 255)) / 256.0)) / 4) * (skill.base_power / 50.0)[/code][br] 61 | ## [br] 62 | ## Default damage calculation.[br] 63 | ## [code](attacker.attack - target.defense) * (skill.power / 50)[/code][br] 64 | ## [br] 65 | ## [color=orange]!All listed formulas have their value multiplied by the type[br] 66 | ## effectiveness value returned by [method element_wheel].[br] 67 | ## [color=red]!!Minimum value that can be returned by all formulas is [b]1[/b].[/color][br] 68 | static func calculate_damage(attacker:Battler, target:Battler, skill:Skill) -> int: 69 | var atk:float = float(attacker.stats.attack) 70 | var def:float = float(target.stats.defense) 71 | var damage:int = 1 72 | match GlobalBattleSettings.Global_Damage_Calc_Type: 73 | GlobalBattleSettings.Damage_Calc_Type.PKMN: 74 | damage = max(1, int((((((2.0 * attacker.exp_node.char_level) / 5.0 + 2) * skill.base_power * (atk / def)) / 50.0) + 2) * element_wheel(skill.element, target.stats.element))) 75 | GlobalBattleSettings.Damage_Calc_Type.DRGNQST: 76 | damage = max(1, int(((atk - (def / 2.0) + (((atk - (def/2.0) + 1.0) * randf_range(0, 255)) / 256.0)) / 4) * (skill.base_power / 50.0) * element_wheel(skill.element, target.stats.element))) 77 | _: 78 | damage = max(1, int((atk-def) * (skill.base_power/50.0) * element_wheel(skill.element, target.stats.element))) 79 | return damage 80 | -------------------------------------------------------------------------------- /battle-manager/allies/YBot.glb.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="scene" 4 | importer_version=1 5 | type="PackedScene" 6 | uid="uid://unpqnv34y004" 7 | path="res://.godot/imported/YBot.glb-24ad5b74e7b90804ab118e74faddcc68.scn" 8 | 9 | [deps] 10 | 11 | source_file="res://battle-manager/allies/YBot.glb" 12 | dest_files=["res://.godot/imported/YBot.glb-24ad5b74e7b90804ab118e74faddcc68.scn"] 13 | 14 | [params] 15 | 16 | nodes/root_type="CharacterBody3D" 17 | nodes/root_name="" 18 | nodes/apply_root_scale=true 19 | nodes/root_scale=1.0 20 | nodes/import_as_skeleton_bones=false 21 | nodes/use_node_type_suffixes=true 22 | meshes/ensure_tangents=true 23 | meshes/generate_lods=true 24 | meshes/create_shadow_meshes=true 25 | meshes/light_baking=1 26 | meshes/lightmap_texel_size=0.2 27 | meshes/force_disable_compression=false 28 | skins/use_named_skins=true 29 | animation/import=true 30 | animation/fps=30 31 | animation/trimming=false 32 | animation/remove_immutable_tracks=true 33 | animation/import_rest_as_RESET=false 34 | import_script/path="" 35 | _subresources={ 36 | "materials": { 37 | "Alpha_Body_MAT": { 38 | "use_external/enabled": false, 39 | "use_external/path": "res://extra-throwaway/Alpha_Body_MAT.tres" 40 | }, 41 | "Alpha_Joints_MAT": { 42 | "use_external/enabled": false, 43 | "use_external/path": "res://extra-throwaway/Alpha_Joints_MAT.tres" 44 | } 45 | }, 46 | "nodes": { 47 | "PATH:Armature/Skeleton3D": { 48 | "retarget/bone_map": Object(BoneMap,"resource_local_to_scene":false,"resource_name":"","profile":Object(SkeletonProfileHumanoid,"resource_local_to_scene":false,"resource_name":"","root_bone":&"Root","scale_base_bone":&"Hips","group_size":4,"bone_size":56,"script":null) 49 | ,"bonemap":null,"bone_map/Root":&"","bone_map/Hips":&"mixamorig_Hips","bone_map/Spine":&"mixamorig_Spine","bone_map/Chest":&"mixamorig_Spine1","bone_map/UpperChest":&"mixamorig_Spine2","bone_map/Neck":&"mixamorig_Neck","bone_map/Head":&"mixamorig_Head","bone_map/LeftEye":&"","bone_map/RightEye":&"","bone_map/Jaw":&"","bone_map/LeftShoulder":&"mixamorig_LeftShoulder","bone_map/LeftUpperArm":&"mixamorig_LeftArm","bone_map/LeftLowerArm":&"mixamorig_LeftForeArm","bone_map/LeftHand":&"mixamorig_LeftHand","bone_map/LeftThumbMetacarpal":&"mixamorig_LeftHandThumb2","bone_map/LeftThumbProximal":&"mixamorig_LeftHandThumb3","bone_map/LeftThumbDistal":&"mixamorig_LeftHandThumb4","bone_map/LeftIndexProximal":&"mixamorig_LeftHandIndex2","bone_map/LeftIndexIntermediate":&"mixamorig_LeftHandIndex3","bone_map/LeftIndexDistal":&"mixamorig_LeftHandIndex4","bone_map/LeftMiddleProximal":&"mixamorig_LeftHandMiddle2","bone_map/LeftMiddleIntermediate":&"mixamorig_LeftHandMiddle3","bone_map/LeftMiddleDistal":&"mixamorig_LeftHandMiddle4","bone_map/LeftRingProximal":&"mixamorig_LeftHandRing2","bone_map/LeftRingIntermediate":&"mixamorig_LeftHandRing3","bone_map/LeftRingDistal":&"mixamorig_LeftHandRing4","bone_map/LeftLittleProximal":&"mixamorig_LeftHandPinky2","bone_map/LeftLittleIntermediate":&"mixamorig_LeftHandPinky3","bone_map/LeftLittleDistal":&"mixamorig_LeftHandPinky4","bone_map/RightShoulder":&"mixamorig_RightShoulder","bone_map/RightUpperArm":&"mixamorig_RightArm","bone_map/RightLowerArm":&"mixamorig_RightForeArm","bone_map/RightHand":&"mixamorig_RightHand","bone_map/RightThumbMetacarpal":&"mixamorig_RightHandThumb2","bone_map/RightThumbProximal":&"mixamorig_RightHandThumb3","bone_map/RightThumbDistal":&"mixamorig_RightHandThumb4","bone_map/RightIndexProximal":&"mixamorig_RightHandIndex2","bone_map/RightIndexIntermediate":&"mixamorig_RightHandIndex3","bone_map/RightIndexDistal":&"mixamorig_RightHandIndex4","bone_map/RightMiddleProximal":&"mixamorig_RightHandMiddle2","bone_map/RightMiddleIntermediate":&"mixamorig_RightHandMiddle3","bone_map/RightMiddleDistal":&"mixamorig_RightHandMiddle4","bone_map/RightRingProximal":&"mixamorig_RightHandRing2","bone_map/RightRingIntermediate":&"mixamorig_RightHandRing3","bone_map/RightRingDistal":&"mixamorig_RightHandRing4","bone_map/RightLittleProximal":&"mixamorig_RightHandPinky2","bone_map/RightLittleIntermediate":&"mixamorig_RightHandPinky3","bone_map/RightLittleDistal":&"mixamorig_RightHandPinky4","bone_map/LeftUpperLeg":&"mixamorig_LeftUpLeg","bone_map/LeftLowerLeg":&"mixamorig_LeftLeg","bone_map/LeftFoot":&"mixamorig_LeftFoot","bone_map/LeftToes":&"mixamorig_LeftToeBase","bone_map/RightUpperLeg":&"mixamorig_RightUpLeg","bone_map/RightLowerLeg":&"mixamorig_RightLeg","bone_map/RightFoot":&"mixamorig_RightFoot","bone_map/RightToes":&"mixamorig_RightToeBase","script":null) 50 | 51 | } 52 | } 53 | } 54 | gltf/naming_version=1 55 | gltf/embedded_image_handling=1 56 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /maps/battle_scenes/test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=16 format=3 uid="uid://b56ncgnrwugct"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://bcqxqucgohr4x" path="res://battle-manager/allies/y_bot-ally.tscn" id="1_2aqym"] 4 | [ext_resource type="PackedScene" uid="uid://bvhah4cfn4txb" path="res://battle-manager/BattleManager.tscn" id="1_t14x8"] 5 | [ext_resource type="Resource" uid="uid://d0noky2jlhk4u" path="res://database/troops/test_troop.tres" id="3_yxp8s"] 6 | [ext_resource type="Resource" uid="uid://w5n4kf04c1fp" path="res://database/skills/normal_attack.tres" id="4_q6nlu"] 7 | [ext_resource type="Script" uid="uid://d2mintfjxyqei" path="res://BattlerStats.gd" id="4_xecl5"] 8 | [ext_resource type="Script" uid="uid://m02b4cy78or" path="res://database/items/items_template.gd" id="5_xecl5"] 9 | [ext_resource type="Script" uid="uid://c2j4t3rigq4gk" path="res://battle-manager/inventory/inventory.gd" id="6_yxp8s"] 10 | 11 | [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_2ddfu"] 12 | sky_top_color = Color(0.447336, 0.519571, 0.620102, 1) 13 | sky_cover_modulate = Color(0.801182, 0.452923, 0.347646, 1) 14 | 15 | [sub_resource type="Sky" id="Sky_4vui1"] 16 | sky_material = SubResource("ProceduralSkyMaterial_2ddfu") 17 | 18 | [sub_resource type="Environment" id="Environment_misr6"] 19 | background_mode = 2 20 | sky = SubResource("Sky_4vui1") 21 | fog_enabled = true 22 | fog_light_energy = 1.34 23 | volumetric_fog_density = 0.02 24 | 25 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_3fc4g"] 26 | albedo_color = Color(0.192279, 0.227401, 0.23218, 1) 27 | 28 | [sub_resource type="Resource" id="Resource_yxp8s"] 29 | script = ExtResource("4_xecl5") 30 | metadata/_custom_type_script = "uid://d2mintfjxyqei" 31 | 32 | [sub_resource type="Resource" id="Resource_daget"] 33 | script = ExtResource("5_xecl5") 34 | item_name = "Other item" 35 | description = "Does other stuff" 36 | item_tags = "Hrmm" 37 | metadata/_custom_type_script = "uid://m02b4cy78or" 38 | 39 | [sub_resource type="Resource" id="Resource_fa7uu"] 40 | script = ExtResource("5_xecl5") 41 | item_name = "Crazy Item" 42 | description = "Does crazy stuff" 43 | item_tags = "AHHH" 44 | metadata/_custom_type_script = "uid://m02b4cy78or" 45 | 46 | [sub_resource type="Resource" id="Resource_4yi8j"] 47 | script = ExtResource("6_yxp8s") 48 | collection = Dictionary[ExtResource("5_xecl5"), int]({ 49 | SubResource("Resource_daget"): 5, 50 | SubResource("Resource_fa7uu"): 3 51 | }) 52 | metadata/_custom_type_script = "uid://c2j4t3rigq4gk" 53 | 54 | [node name="Battle-Manager" instance=ExtResource("1_t14x8")] 55 | default_attack = ExtResource("4_q6nlu") 56 | mouse_input_toggle = false 57 | active_troop = ExtResource("3_yxp8s") 58 | 59 | [node name="Control" parent="BattleHUD" index="0"] 60 | mouse_filter = 1 61 | 62 | [node name="PlayerSPBar" type="ProgressBar" parent="BattleHUD/Control/Players/AllAllies/AllyStats2" index="2"] 63 | layout_mode = 2 64 | 65 | [node name="PlayerSPBar" type="ProgressBar" parent="BattleHUD/Control/Players/AllAllies/AllyStats3" index="2"] 66 | layout_mode = 2 67 | 68 | [node name="PlayerSPBar" type="ProgressBar" parent="BattleHUD/Control/Players/AllAllies/AllyStats4" index="2"] 69 | layout_mode = 2 70 | 71 | [node name="WorldEnvironment" type="WorldEnvironment" parent="." index="1"] 72 | environment = SubResource("Environment_misr6") 73 | 74 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment" index="0"] 75 | transform = Transform3D(1, 0, 0, 0, 0.0715845, 0.997435, 0, -0.997435, 0.0715845, 0, 17.7598, 0) 76 | light_energy = 1.646 77 | shadow_enabled = true 78 | 79 | [node name="Platform" type="CSGBox3D" parent="." index="2"] 80 | use_collision = true 81 | size = Vector3(100, 0.5, 100) 82 | material = SubResource("StandardMaterial3D_3fc4g") 83 | 84 | [node name="Ally1" parent="." index="3" instance=ExtResource("1_2aqym")] 85 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0.260986, -1.00599) 86 | stats = SubResource("Resource_yxp8s") 87 | inventory = SubResource("Resource_4yi8j") 88 | 89 | [node name="Ally2" parent="." index="4" instance=ExtResource("1_2aqym")] 90 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0.260986, -1.00599) 91 | stats = SubResource("Resource_yxp8s") 92 | inventory = SubResource("Resource_4yi8j") 93 | 94 | [node name="Camera3D" type="Camera3D" parent="." index="5"] 95 | transform = Transform3D(-0.730587, -0.228461, 0.643466, -0.0331128, 0.953111, 0.300804, -0.682016, 0.198457, -0.703896, 2.48554, 3.00572, -3.08627) 96 | 97 | [node name="FRONTROW" type="Marker3D" parent="." index="6" groups=["FRONTROW"]] 98 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.22, 3) 99 | 100 | [editable path="BattleHUD"] 101 | -------------------------------------------------------------------------------- /docs/troops.txt: -------------------------------------------------------------------------------- 1 | ======================================== 2 | TROOP SYSTEM DOCUMENTATION 3 | ======================================== 4 | 5 | OVERVIEW: 6 | Troops are a way to spawn multiple enemies with predefined formations. The system uses EnemyGroup resources to organize enemy scenes and Troops resources to manage formations and spawn behavior. 7 | 8 | ======================================== 9 | KEY RESOURCES 10 | ======================================== 11 | 12 | 1. EnemyGroup (database/troops/enemy_group.gd) 13 | - Stores a collection of enemy scenes (PackedScene objects) 14 | - Fields: 15 | * group_name: Name of the enemy group 16 | * enemy_scenes: Array[PackedScene] - Drag enemy .tscn files here in inspector 17 | 18 | 2. Troops (database/troops/troops_template.gd) 19 | - Defines a troop encounter 20 | - Fields: 21 | * troop_name: Name of the troop 22 | * troop_description: Description of the troop 23 | * enemy_group: Reference to an EnemyGroup resource 24 | * formation: Formation type (enum) 25 | * damage_reduction: Multiplier applied to all enemies (1.0 = normal) 26 | 27 | ======================================== 28 | FORMATIONS 29 | ======================================== 30 | 31 | FRONT_ROW: 32 | - Enemies spawn in a horizontal line facing the player 33 | - Centered along X axis, positioned in front (positive Z) 34 | - Uses FRONTROW marker if available for center point (X/Z positioning only) 35 | - Default Y position: 0.261 (matches ally height) or uses marker Y if present 36 | 37 | TRIANGLE: 38 | - Enemies arranged in widening rows 39 | - Back row has fewer enemies, front rows expand outward 40 | - Rows progress forward along Z axis 41 | - Inherits FRONTROW marker behavior for base positioning 42 | 43 | CIRCLE_PLAYER: 44 | - Enemies form a circle around the player 45 | - Evenly distributed by angle 46 | - All spawn at radius 5.0 units from player center 47 | - Inherits FRONTROW marker behavior for base positioning 48 | 49 | CUSTOM_MARKERS: 50 | - Requires enemy_markers (Node3D) reference in BattleManager 51 | - Searches for child Marker3D nodes and spawns enemies on them 52 | - If no markers found, falls back to FRONT_ROW formation 53 | - Each marker spawns one enemy in order 54 | 55 | ======================================== 56 | MARKER-BASED POSITIONING 57 | ======================================== 58 | 59 | FRONTROW Marker: 60 | - Marker3D node named/grouped as "FRONTROW" 61 | - Provides base X/Z center point for all procedural formations 62 | - Y position also inherited (prevents ground clipping) 63 | - Optional - system uses default (0, 0.261, 0) if not found 64 | - Recommended setup: 65 | * Position at center of battle arena 66 | * Y height should match your ground level or desired spawn height 67 | * Rename/group as "FRONTROW" so system can find it 68 | 69 | Custom Markers (for CUSTOM_MARKERS formation): 70 | - Requires enemy_markers property set on BattleManager 71 | - Assign a Node3D containing Marker3D children 72 | - One marker per enemy in order 73 | - Allows precise placement for boss fights or specific encounters 74 | 75 | ======================================== 76 | Y POSITION BEHAVIOR 77 | ======================================== 78 | 79 | All formations now respect Y positioning: 80 | - Uses FRONTROW marker's Y if available (best practice) 81 | - Falls back to 0.261 (default ally height) if no marker 82 | - If enemies spawn in ground: Create FRONTROW marker at correct Y height 83 | - Alternative: Use custom marker formation for manual placement 84 | - Physics/collision can pull enemies down if needed 85 | 86 | ======================================== 87 | SETUP WORKFLOW 88 | ======================================== 89 | 90 | 1. Create EnemyGroup resource: 91 | - Right click in database/troops/ 92 | - New Resource -> EnemyGroup 93 | - Set group_name 94 | - Drag enemy .tscn files into enemy_scenes array 95 | 96 | 2. Create Troops resource: 97 | - Right click in database/troops/ 98 | - New Resource -> Troops 99 | - Set troop_name and description 100 | - Assign EnemyGroup to enemy_group field 101 | - Choose formation type 102 | - Optional: Set damage_reduction multiplier 103 | 104 | 3. Assign to BattleManager: 105 | - Set active_troop export variable to your Troops resource 106 | - Enemies spawn automatically on battle start 107 | 108 | 4. For marker-based formations: 109 | - Create Marker3D in your battle scene 110 | - Name it "FRONTROW" or add to "FRONTROW" group 111 | - Position at desired spawn center point 112 | - Formations will use it as reference 113 | 114 | ======================================== 115 | SPAWNING DURING BATTLE 116 | ======================================== 117 | 118 | Reinforcements can spawn during combat using: 119 | BattleManager.spawn_reinforcements(troop_resource) 120 | 121 | This adds new enemies mid-battle while maintaining turn order and formations. 122 | Useful for wave-based encounters or reinforcement mechanics. 123 | 124 | ======================================== 125 | NOTES FOR NON-AI IMPLEMENTATION 126 | ======================================== 127 | 128 | Key functions in BattleManager: 129 | - get_formation_position(index, total, formation, marker): Calculate spawn position 130 | - spawn_troop(troop, parent): Initial enemy spawning 131 | - spawn_reinforcements(troop, parent): Mid-battle spawning 132 | 133 | The system searches for FRONTROW marker in scene groups on each spawn. 134 | Formations are procedurally calculated - no need to pre-place markers for 135 | FRONT_ROW, TRIANGLE, or CIRCLE_PLAYER unless you want custom positioning. 136 | -------------------------------------------------------------------------------- /battle-manager/inventory/inventory.gd: -------------------------------------------------------------------------------- 1 | class_name Inventory 2 | extends Resource 3 | 4 | # Use Inventory Resolution to manage interactions.. 5 | # if item couldn't be added to inventory for some reason, shouldn't remove item from existence 6 | enum Resolution { 7 | SUCCESS, 8 | NO_ITEM, AT_CAPACITY, IS_EMPTY, 9 | NOT_FOUND, OUT_OF_BOUNDS, DUPLICATE, 10 | NO_CHANGE 11 | } 12 | 13 | @export var has_max_limit:bool = true: 14 | set(limit): 15 | has_max_limit = limit 16 | if has_max_limit and collection.size() > max_size: 17 | resize_collection(max_size) 18 | @export var max_size:int = 30: 19 | set(size): 20 | max_size = size 21 | if has_max_limit and collection.size() > size: 22 | resize_collection(size) 23 | @export var collection:Dictionary[Item, int] = {}: 24 | set(items): 25 | # If somebody tries to manually bypass size restrictions, this should prevent that 26 | if has_max_limit and items.size() > max_size: 27 | resize_collection(max_size) 28 | collection = items 29 | 30 | func add_item_to_collection(item:Item, amount:int = 1) -> Resolution: 31 | if !item: 32 | return Resolution.NO_ITEM 33 | if has_max_limit and collection.size() >= max_size: 34 | return Resolution.AT_CAPACITY 35 | 36 | # Special Case Handling 37 | if amount < 1 and collection.has(item): 38 | if amount < 0: 39 | return remove_item_from_collection(item, -amount) 40 | return Resolution.NO_CHANGE 41 | 42 | # Add the item(s) 43 | if collection.has(item) and item.is_stackable: 44 | collection[item] += amount 45 | else: 46 | collection[item] = amount 47 | return Resolution.SUCCESS 48 | 49 | func remove_item_from_collection(item:Item, amount:int = 1, remove_all:bool = false) -> Resolution: 50 | if collection.is_empty(): 51 | return Resolution.IS_EMPTY 52 | if !item: 53 | return Resolution.NO_ITEM 54 | if !collection.has(item): 55 | return Resolution.NOT_FOUND 56 | 57 | # Special Case Handling 58 | if amount > collection[item]: 59 | remove_all = true 60 | if amount < 1: 61 | if amount < 0: 62 | return add_item_to_collection(item, -amount) 63 | return Resolution.NO_CHANGE 64 | 65 | # Remove the item(s) 66 | if remove_all: 67 | collection.erase(item) 68 | else: 69 | if collection[item] > amount: 70 | collection[item] -= amount 71 | else: 72 | collection.erase(item) 73 | return Resolution.SUCCESS 74 | 75 | func replace_item_in_collection(item_to_remove:Item, item_to_add:Item, amount:int = 1, copy_count:bool = false, not_found_do_add:bool = false) -> Resolution: 76 | if collection.is_empty(): 77 | if !not_found_do_add: 78 | return Resolution.IS_EMPTY 79 | if !item_to_remove: 80 | if !not_found_do_add: 81 | return Resolution.NO_ITEM 82 | if has_max_limit and collection.size() >= max_size: 83 | if !collection.has(item_to_remove): 84 | return Resolution.AT_CAPACITY 85 | # NOTE: Currently, if same items to add/remove, just use add or remove func... 86 | if item_to_remove == item_to_add: 87 | var diff:int = collection[item_to_remove] - collection[item_to_add] 88 | if diff > 0: 89 | return add_item_to_collection(item_to_remove, diff) 90 | elif diff < 0: 91 | return remove_item_from_collection(item_to_remove, diff) 92 | return Resolution.NO_CHANGE 93 | # Pass the checks, add the item_to_add and remove item_to_remove (if found/present) 94 | if collection.has(item_to_remove): 95 | if item_to_add.is_stackable: 96 | if copy_count: 97 | collection[item_to_add] = collection[item_to_remove] 98 | else: 99 | collection[item_to_add] = amount 100 | else: 101 | collection[item_to_add] = amount 102 | collection.erase(item_to_remove) 103 | else: 104 | # not_found_do_add == true 105 | collection[item_to_add] = amount 106 | # If not stopped in other checks, item was found or not_found_do_add == true 107 | return Resolution.SUCCESS 108 | 109 | enum ResId { ITEM, RESOLUTION } 110 | # If dict returned is empty, assume no change or failure/error based on rules 111 | # NOTE: Use this function is adding multiple different items as one-ofs... 112 | func add_items_to_collection(items:Array[Item] = []) -> Dictionary[int, Dictionary]: 113 | var resolutions:Dictionary[int, Dictionary] = {} 114 | if !items or items.is_empty(): 115 | return resolutions 116 | 117 | var attempt:int = 0 118 | for item:Item in items: 119 | attempt += 1 120 | if has_max_limit and collection.size() >= max_size and !collection.has(item): 121 | resolutions.set(attempt, {ResId.ITEM : item, ResId.RESOLUTION : Resolution.AT_CAPACITY}) 122 | else: 123 | if collection.has(item): 124 | collection[item] += 1 125 | else: 126 | collection[item] = 1 127 | resolutions.set(attempt, {ResId.ITEM : item, ResId.RESOLUTION : Resolution.SUCCESS}) 128 | return resolutions 129 | 130 | func remove_items_from_collection(items:Array[Item] = [], allow_partial_removal:bool = false, remove_all:bool = false) -> Dictionary[int, Dictionary]: 131 | var resolutions:Dictionary[int, Dictionary] = {} 132 | if !items or items.is_empty(): 133 | return {} 134 | 135 | var attempt:int = 0 136 | var _removals:Dictionary[int, Item] = {} 137 | for item:Item in items: 138 | attempt += 1 139 | if !collection.has(item): 140 | if !allow_partial_removal: 141 | return {} 142 | else: 143 | resolutions.set(attempt, {ResId.ITEM : item, ResId.RESOLUTION : Resolution.NOT_FOUND}) 144 | continue 145 | 146 | if remove_all: 147 | collection.erase(item) 148 | else: 149 | # Remove items one-by-one 150 | if collection[item] > 1: 151 | collection[item] -= 1 152 | else: 153 | collection.erase(item) 154 | resolutions.set(attempt, {ResId.ITEM : item, ResId.RESOLUTION : Resolution.SUCCESS}) 155 | return resolutions 156 | 157 | func resize_collection(new_size:int=max_size) -> void: 158 | if collection.size() > new_size: 159 | var skip:int = 0 160 | while collection.size() > new_size or skip >= collection.size(): 161 | var it:Item = collection.keys()[collection.size() - 1 - skip] 162 | if !it.is_key_item: 163 | collection.erase(it) 164 | else: 165 | skip += 1 166 | 167 | func clear_collection() -> void: 168 | collection.clear() 169 | -------------------------------------------------------------------------------- /replace/regular_map/playableplayer/playableplayer.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://tqowxvpoq16k"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://unpqnv34y004" path="res://assets/allies/YBot.glb" id="1_i6t4c"] 4 | 5 | [sub_resource type="GDScript" id="GDScript_8e5hc"] 6 | resource_name = "playableplayerscript" 7 | script/source = "extends CharacterBody3D 8 | 9 | 10 | const SPEED = 5.0 11 | const JUMP_VELOCITY = 4.5 12 | 13 | 14 | func _physics_process(delta: float) -> void: 15 | # Add the gravity. 16 | if not is_on_floor(): 17 | velocity += get_gravity() * delta 18 | 19 | # Handle jump. 20 | if Input.is_action_just_pressed(\"ui_accept\") and is_on_floor(): 21 | velocity.y = JUMP_VELOCITY 22 | 23 | # Get the input direction and handle the movement/deceleration. 24 | # As good practice, you should replace UI actions with custom gameplay actions. 25 | var input_dir := Input.get_vector(\"ui_left\", \"ui_right\", \"ui_up\", \"ui_down\") 26 | var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() 27 | if direction: 28 | velocity.x = direction.x * SPEED 29 | velocity.z = direction.z * SPEED 30 | else: 31 | velocity.x = move_toward(velocity.x, 0, SPEED) 32 | velocity.z = move_toward(velocity.z, 0, SPEED) 33 | 34 | move_and_slide() 35 | " 36 | 37 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_445w7"] 38 | resource_name = "Alpha_Body_MAT" 39 | cull_mode = 2 40 | albedo_color = Color(1, 0.344018, 0.172081, 1) 41 | roughness = 0.552786 42 | 43 | [sub_resource type="BoxShape3D" id="BoxShape3D_iwxkb"] 44 | size = Vector3(1, 2.4248, 1) 45 | 46 | [node name="basic-player" instance=ExtResource("1_i6t4c")] 47 | script = SubResource("GDScript_8e5hc") 48 | 49 | [node name="GeneralSkeleton" parent="Armature" index="0"] 50 | bones/0/position = Vector3(-1.60699e-05, 0.978864, 1.4695e-05) 51 | bones/0/rotation = Quaternion(-0.00863847, -0.00445693, -0.00843747, 0.999917) 52 | bones/1/rotation = Quaternion(0.102279, 0.00491141, 0.0212029, 0.994518) 53 | bones/2/rotation = Quaternion(-0.0205155, 0.00469559, -0.00737195, 0.999751) 54 | bones/3/rotation = Quaternion(-0.0204074, 0.0041072, -0.00775946, 0.999753) 55 | bones/4/rotation = Quaternion(-0.0203927, 0.0555198, 0.0490986, 0.997041) 56 | bones/5/rotation = Quaternion(0.0609376, 0.0864517, -0.0157317, 0.994266) 57 | bones/7/rotation = Quaternion(0.485644, 0.491046, 0.51222, -0.510543) 58 | bones/8/rotation = Quaternion(0.0806214, 0.777719, -0.622491, 0.0340435) 59 | bones/9/rotation = Quaternion(0.13253, -0.654822, 0.0509053, 0.742329) 60 | bones/10/rotation = Quaternion(-0.0833954, 0.700716, 0.085982, 0.703313) 61 | bones/11/rotation = Quaternion(-0.0525923, 0.00012055, -0.0784966, 0.995526) 62 | bones/12/rotation = Quaternion(-0.127402, 0.732569, 0.160819, 0.649036) 63 | bones/13/rotation = Quaternion(0.0766016, 0.0373358, -0.0297666, 0.995918) 64 | bones/14/scale = Vector3(1.00002, 1, 0.999985) 65 | bones/15/rotation = Quaternion(0.0719518, 0.0182408, -0.0331228, 0.996691) 66 | bones/16/rotation = Quaternion(0.161798, -0.000212267, -0.00222963, 0.986821) 67 | bones/17/rotation = Quaternion(0.125967, 0.000215158, -0.00163189, 0.992033) 68 | bones/19/rotation = Quaternion(0.118486, -0.0229991, -0.04708, 0.991572) 69 | bones/20/rotation = Quaternion(0.232708, -0.000246316, -0.00583723, 0.972529) 70 | bones/21/rotation = Quaternion(0.101242, 0.000317425, -0.000500411, 0.994862) 71 | bones/23/rotation = Quaternion(0.0965588, -0.0512746, -0.0835336, 0.990489) 72 | bones/24/rotation = Quaternion(0.283379, -0.00109676, -0.00278559, 0.959003) 73 | bones/25/rotation = Quaternion(0.134329, 0.000140116, -0.00621021, 0.990917) 74 | bones/27/rotation = Quaternion(0.205464, -0.0748475, -0.0852966, 0.972063) 75 | bones/28/rotation = Quaternion(0.295028, -0.000161037, -0.00191641, 0.955487) 76 | bones/29/rotation = Quaternion(0.129829, 8.57711e-05, -0.00146967, 0.991535) 77 | bones/31/rotation = Quaternion(0.522451, -0.44197, -0.521384, -0.509771) 78 | bones/32/rotation = Quaternion(-0.155195, 0.79716, -0.567383, 0.136113) 79 | bones/33/rotation = Quaternion(0.0742741, 0.602695, -0.140929, 0.781909) 80 | bones/34/rotation = Quaternion(-0.0642399, -0.564712, -0.0510655, 0.821198) 81 | bones/35/rotation = Quaternion(-0.0525936, -0.000134021, 0.0784958, 0.995526) 82 | bones/36/rotation = Quaternion(-0.127408, -0.732579, -0.160827, 0.649022) 83 | bones/37/rotation = Quaternion(0.0765993, -0.0373476, 0.0297601, 0.995918) 84 | bones/38/scale = Vector3(1.00002, 1, 0.999984) 85 | bones/39/rotation = Quaternion(0.0719526, -0.0182467, 0.033119, 0.996691) 86 | bones/40/rotation = Quaternion(0.161798, 0.000211433, 0.00223115, 0.986821) 87 | bones/41/rotation = Quaternion(0.125967, -0.000215769, 0.00163186, 0.992033) 88 | bones/43/rotation = Quaternion(0.118487, 0.0229899, 0.0470842, 0.991572) 89 | bones/44/rotation = Quaternion(0.232708, 0.00024426, 0.00583637, 0.972529) 90 | bones/45/rotation = Quaternion(0.101242, -0.000317425, 0.000500113, 0.994862) 91 | bones/47/rotation = Quaternion(0.0965587, 0.0512594, 0.0835441, 0.990489) 92 | bones/48/rotation = Quaternion(0.283378, 0.00109579, 0.00278571, 0.959004) 93 | bones/49/rotation = Quaternion(0.13433, -0.000141457, 0.00620985, 0.990917) 94 | bones/51/rotation = Quaternion(0.205456, 0.0748599, 0.0851764, 0.972075) 95 | bones/52/rotation = Quaternion(0.295023, 0.000136673, 0.00225782, 0.955487) 96 | bones/53/rotation = Quaternion(0.129833, -0.000114352, 0.00125268, 0.991535) 97 | bones/55/rotation = Quaternion(0.085921, 0.105342, 0.986961, -0.0861909) 98 | bones/56/rotation = Quaternion(0.00121408, 0.987668, -0.153478, -0.0308882) 99 | bones/57/rotation = Quaternion(-0.0456521, 0.678682, -0.731281, 0.0503463) 100 | bones/58/rotation = Quaternion(2.50247e-08, 0.998807, 0.0488378, 4.67567e-08) 101 | bones/60/rotation = Quaternion(-0.0867203, 0.0711536, 0.993326, 0.026822) 102 | bones/61/rotation = Quaternion(0.0124195, 0.983949, -0.176236, -0.0251224) 103 | bones/62/rotation = Quaternion(0.0287591, 0.638591, -0.768508, -0.0277372) 104 | bones/63/rotation = Quaternion(3.90188e-08, 0.999404, 0.0345182, 1.15245e-09) 105 | 106 | [node name="Alpha_Surface" parent="Armature/GeneralSkeleton" index="1"] 107 | surface_material_override/0 = SubResource("StandardMaterial3D_445w7") 108 | 109 | [node name="CollisionShape3D" type="CollisionShape3D" parent="." index="2"] 110 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.712402, 0) 111 | shape = SubResource("BoxShape3D_iwxkb") 112 | -------------------------------------------------------------------------------- /battle-manager/allies/battle_tree_node.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="AnimationNodeStateMachine" load_steps=28 format=3 uid="uid://c7waot7s0me6v"] 2 | 3 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_3rsfg"] 4 | animation = &"Locomotion-Library/attack1" 5 | 6 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_q7yk4"] 7 | animation = &"armature_stand" 8 | 9 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_f8jrq"] 10 | animation = &"Locomotion-Library/idle2" 11 | 12 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_gyp3t"] 13 | animation = &"Locomotion-Library/jump" 14 | 15 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_qu72c"] 16 | animation = &"Locomotion-Library/kick1" 17 | 18 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_lxots"] 19 | animation = &"Locomotion-Library/walk" 20 | 21 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_shbi4"] 22 | animation = &"skill-animations/strong-attack" 23 | 24 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_re1va"] 25 | animation = &"Locomotion-Library/turn left" 26 | 27 | [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_peoh0"] 28 | animation = &"Locomotion-Library/turn right" 29 | 30 | [sub_resource type="AnimationNodeStateMachineTransition" id="idle_to_walk"] 31 | xfade_time = 0.2 32 | advance_condition = &"is_walking" 33 | 34 | [sub_resource type="AnimationNodeStateMachineTransition" id="walk_to_turn"] 35 | xfade_time = 0.2 36 | advance_condition = &"is_turning" 37 | 38 | [sub_resource type="AnimationNodeStateMachineTransition" id="turn_to_attack"] 39 | xfade_time = 0.2 40 | advance_condition = &"is_attacking" 41 | 42 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_bn3j8"] 43 | xfade_time = 0.2 44 | advance_condition = &"battle_idle" 45 | 46 | [sub_resource type="AnimationNodeStateMachineTransition" id="idle_to_turn_right"] 47 | xfade_time = 0.3 48 | advance_condition = &"is_turning_right" 49 | 50 | [sub_resource type="AnimationNodeStateMachineTransition" id="idle_to_turn_left"] 51 | xfade_time = 0.3 52 | advance_condition = &"is_turning_left" 53 | 54 | [sub_resource type="AnimationNodeStateMachineTransition" id="turn_to_idle"] 55 | xfade_time = 0.2 56 | advance_mode = 2 57 | 58 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_lm7ud"] 59 | xfade_time = 0.2 60 | advance_condition = &"exampe_condition" 61 | 62 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_00hc0"] 63 | xfade_time = 0.2 64 | 65 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_1rigw"] 66 | advance_mode = 2 67 | 68 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_xdrmx"] 69 | advance_mode = 2 70 | 71 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_k0l0t"] 72 | advance_mode = 2 73 | 74 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_4t2y6"] 75 | advance_mode = 2 76 | 77 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_0idrr"] 78 | xfade_time = 0.2 79 | 80 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_uno5v"] 81 | xfade_time = 0.2 82 | 83 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_e4e1w"] 84 | advance_mode = 2 85 | 86 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_ys4go"] 87 | switch_mode = 1 88 | advance_mode = 2 89 | advance_condition = &"defend" 90 | 91 | [sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_xi3w4"] 92 | switch_mode = 1 93 | advance_mode = 2 94 | 95 | [resource] 96 | states/End/position = Vector2(1205, 100) 97 | states/Start/position = Vector2(210, 94) 98 | states/attack/node = SubResource("AnimationNodeAnimation_3rsfg") 99 | states/attack/position = Vector2(658, 88) 100 | states/defend/node = SubResource("AnimationNodeAnimation_q7yk4") 101 | states/defend/position = Vector2(229.58777, -8.376553) 102 | states/idle1/node = SubResource("AnimationNodeAnimation_f8jrq") 103 | states/idle1/position = Vector2(429, 19) 104 | states/jump/node = SubResource("AnimationNodeAnimation_gyp3t") 105 | states/jump/position = Vector2(667, 13) 106 | states/kick/node = SubResource("AnimationNodeAnimation_qu72c") 107 | states/kick/position = Vector2(463, 286) 108 | states/run/node = SubResource("AnimationNodeAnimation_lxots") 109 | states/run/position = Vector2(250, 191) 110 | states/skill-strong-attack/node = SubResource("AnimationNodeAnimation_shbi4") 111 | states/skill-strong-attack/position = Vector2(638, 235) 112 | states/turn_left/node = SubResource("AnimationNodeAnimation_re1va") 113 | states/turn_left/position = Vector2(973, 213.5) 114 | states/turn_right/node = SubResource("AnimationNodeAnimation_peoh0") 115 | states/turn_right/position = Vector2(957, 100) 116 | transitions = ["idle1", "run", SubResource("idle_to_walk"), "run", "turn_left", SubResource("walk_to_turn"), "run", "turn_right", SubResource("walk_to_turn"), "turn_left", "attack", SubResource("turn_to_attack"), "turn_right", "attack", SubResource("turn_to_attack"), "attack", "idle1", SubResource("AnimationNodeStateMachineTransition_bn3j8"), "idle1", "turn_right", SubResource("idle_to_turn_right"), "idle1", "turn_left", SubResource("idle_to_turn_left"), "turn_right", "idle1", SubResource("turn_to_idle"), "turn_left", "idle1", SubResource("turn_to_idle"), "idle1", "kick", SubResource("AnimationNodeStateMachineTransition_lm7ud"), "idle1", "jump", SubResource("AnimationNodeStateMachineTransition_00hc0"), "turn_left", "turn_right", SubResource("AnimationNodeStateMachineTransition_1rigw"), "turn_right", "turn_left", SubResource("AnimationNodeStateMachineTransition_xdrmx"), "turn_right", "run", SubResource("AnimationNodeStateMachineTransition_k0l0t"), "run", "idle1", SubResource("AnimationNodeStateMachineTransition_4t2y6"), "kick", "idle1", SubResource("AnimationNodeStateMachineTransition_0idrr"), "jump", "idle1", SubResource("AnimationNodeStateMachineTransition_uno5v"), "Start", "run", SubResource("AnimationNodeStateMachineTransition_e4e1w"), "idle1", "defend", SubResource("AnimationNodeStateMachineTransition_ys4go"), "defend", "idle1", SubResource("AnimationNodeStateMachineTransition_xi3w4")] 117 | graph_offset = Vector2(10.587771, -35.376553) 118 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battlehud.gd: -------------------------------------------------------------------------------- 1 | class_name BattleHud 2 | extends CanvasLayer 3 | 4 | signal action_selected(action: String, skill:Skill) 5 | signal menu_opened 6 | 7 | @onready var skill_button_scene: PackedScene = preload("res://battle-manager/battlehud/skill-button-preset.tscn") 8 | @onready var skill_container = $Control/Skills/ScrollContainer/BoxContainer 9 | 10 | @onready var item_button_scene: PackedScene = preload("res://battle-manager/battlehud/item-button-preset.tscn") 11 | @onready var item_container = $Control/Items/ScrollContainer/BoxContainer 12 | 13 | @onready var action_buttons: BoxContainer = $Control/ActionButtons 14 | @onready var ally_stats: BoxContainer = $Control/Players/AllAllies/AllyStats 15 | @onready var enemy_stats: BoxContainer = $Control/Enemies/AllEnemies/EnemyStats 16 | @onready var battle_result = $Control/BattleResults 17 | @onready var battle_result_label: Label = $Control/BattleResults/BattleResultLabel # I personally think this should be removed. 18 | @onready var skill_select: Control = $Control/Skills 19 | @onready var item_select: Control = $Control/Items 20 | 21 | var activeBattler: Node = null 22 | var enemy: Node = null 23 | 24 | var active_allies = [] 25 | var active_enemies = [] 26 | 27 | # Health bar-related nodes 28 | @onready var player_health_bar = $Control/Players/AllAllies/AllyStats/PlayerHealthBar 29 | @onready var enemy_health_bar = $Control/Enemies/AllEnemies/EnemyStats/EnemyHealthBar 30 | 31 | # Add SP bar reference 32 | @onready var player_sp_bar = $Control/Players/AllAllies/AllyStats/PlayerSPBar 33 | 34 | func _ready(): 35 | print("Inside BattleHUD _ready()") 36 | skill_select.visible = false 37 | item_select.visible = false 38 | battle_result_label.hide() 39 | hide_action_buttons() 40 | 41 | # Initialize health bars 42 | if player_health_bar and enemy_health_bar: 43 | player_health_bar.value = 0 44 | enemy_health_bar.value = 0 45 | 46 | func on_start_combat(enemy_node: Node): 47 | enemy = enemy_node 48 | update_health_bars() 49 | 50 | func on_add_character(character: Node): 51 | if character.is_in_group("players"): 52 | if ally_stats.has_method("update_player_stats"): 53 | ally_stats.update_player_stats(character) 54 | else: 55 | if ally_stats.has_method("update_enemy_stats"): 56 | enemy_stats.update_enemy_stats(character) 57 | 58 | # Modify update_health_bars 59 | func update_health_bars(): 60 | for i in range(active_allies.size()): 61 | var ally = active_allies[i] 62 | var container = $Control/Players/AllAllies.get_child(i) 63 | container.update_character_info(ally) 64 | 65 | for i in range(active_enemies.size()): 66 | var target_enemy = active_enemies[i] 67 | var container = $Control/Enemies/AllEnemies.get_child(i) 68 | container.update_character_info(target_enemy) 69 | 70 | func set_activebattler(character: Node): 71 | activeBattler = character 72 | 73 | func show_action_buttons(_character: Node): 74 | action_buttons.show() 75 | # You can customize this part to show different actions based on the character 76 | 77 | func hide_action_buttons(): 78 | action_buttons.hide() 79 | 80 | func update_character_info(): 81 | if enemy and enemy_stats: 82 | enemy_stats.update_enemy_stats(enemy) 83 | 84 | if ally_stats.has_method("update_player_stats") and activeBattler: 85 | ally_stats.update_player_stats(activeBattler) 86 | if ally_stats.has_method("update_enemy_stats") and enemy: 87 | enemy_stats.update_enemy_stats(enemy) 88 | 89 | func show_battle_result(result: String): 90 | battle_result_label.text = result 91 | battle_result_label.show() 92 | 93 | # Health bar update functions 94 | func update_player_health_bar(): 95 | if activeBattler: 96 | # Health bar 97 | player_health_bar.max_value = activeBattler.max_health 98 | player_health_bar.value = activeBattler.current_health 99 | player_health_bar.show() 100 | 101 | # SP bar 102 | player_sp_bar.max_value = activeBattler.max_sp 103 | player_sp_bar.value = activeBattler.current_sp 104 | player_sp_bar.show() 105 | 106 | func update_enemy_health_bar(): 107 | if enemy: 108 | enemy_health_bar.max_value = enemy.max_health 109 | enemy_health_bar.value = enemy.current_health 110 | enemy_health_bar.show() 111 | 112 | # Action button signals 113 | func _on_attack_pressed(): 114 | print("Attack button pressed") 115 | # Check if there are any valid enemies instead of relying on single enemy variable 116 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 117 | if battle_manager and battle_manager.enemies.size() > 0: 118 | print("Emitting action_selected signal") 119 | action_selected.emit("attack", null) 120 | hide_action_buttons() 121 | else: 122 | print("Error: No enemies available for targeting") 123 | 124 | func _on_defend_pressed(): 125 | hide_action_buttons() 126 | action_selected.emit("defend", null) 127 | 128 | func setup_skill_list(battler: Node) -> void: 129 | # Clear existing skill buttons 130 | for child in skill_container.get_children(): 131 | child.queue_free() 132 | 133 | # Create new skill buttons using visible skills only 134 | if battler.skill_list.size() > 0: 135 | var visible_skills = battler.skill_node.get_visible_skills() 136 | for skill in visible_skills: 137 | var button = skill_button_scene.instantiate() 138 | skill_container.add_child(button) 139 | button.setup(skill) 140 | button.skill_selected.connect(_on_skill_selected) 141 | menu_opened.emit() 142 | 143 | func setup_item_list(battler: Battler) -> void: 144 | # Clear existing skill buttons 145 | for child in item_container.get_children(): 146 | child.queue_free() 147 | # If no items, no point in continuing to do logic here 148 | if !battler.inventory or battler.inventory.collection.is_empty(): 149 | return 150 | 151 | # Create new skill buttons 152 | for item:Item in battler.inventory.collection.keys(): 153 | if !item.is_battle_item: 154 | continue 155 | var button = item_button_scene.instantiate() 156 | item_container.add_child(button) 157 | # Pass the skill resource directly since it should already be a Skill resource 158 | button.setup(item, battler.inventory.collection.get(item)) 159 | button.item_selected.connect(_on_item_selected) 160 | menu_opened.emit() 161 | 162 | func _on_skill_selected(skill: Resource) -> void: 163 | skill_select.visible = false 164 | action_selected.emit("skill", skill) 165 | 166 | func _on_item_selected(item: Item) -> void: 167 | item_select.visible = false 168 | action_selected.emit("item", item) 169 | 170 | func _on_skills_pressed() -> void: 171 | hide_action_buttons() 172 | setup_skill_list(activeBattler) 173 | skill_select.visible = true 174 | 175 | func _on_items_pressed() -> void: 176 | hide_action_buttons() 177 | setup_item_list(activeBattler) 178 | item_select.visible = true 179 | 180 | func _on_run_pressed() -> void: 181 | hide_action_buttons() 182 | action_selected.emit("run", null) 183 | 184 | # Add other action button handlers as needed (Skills, Item, Run) 185 | 186 | # Function to update all UI elements 187 | func update_ui(): 188 | update_character_info() 189 | update_player_health_bar() 190 | update_enemy_health_bar() 191 | 192 | # Call this function when the battle starts or when switching to 3D 193 | func prepare_for_3d(): 194 | # Create a new SubViewport 195 | var viewport = SubViewport.new() 196 | viewport.size = Vector2(1024, 600) # Adjust size as needed 197 | viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS 198 | 199 | # Move all children of this CanvasLayer to the SubViewport 200 | for child in get_children(): 201 | remove_child(child) 202 | viewport.add_child(child) 203 | 204 | # Add the SubViewport to a new TextureRect 205 | var texture_rect = TextureRect.new() 206 | texture_rect.texture = viewport.get_texture() 207 | add_child(texture_rect) 208 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [addons] 12 | 13 | resources_spreadsheet_view/array_color_tint=100.0 14 | resources_spreadsheet_view/color_rows=true 15 | resources_spreadsheet_view/array_min_width=128.0 16 | resources_spreadsheet_view/resource_preview_size=32.0 17 | resources_spreadsheet_view/clip_headers=false 18 | resources_spreadsheet_view/dupe_arrays=true 19 | resources_spreadsheet_view/context_menu_on_leftclick=false 20 | resources_spreadsheet_view/fold_docks=false 21 | 22 | [application] 23 | 24 | config/name="Turn-Based Combat" 25 | config/description="[Open Source] Godot 4 Turn Based Battle System made for the game series: \"Cute Fame\", Avaliable for use freely with given credit to the project. (Which may change in the future) 26 | 27 | Repository: https://github.com/Cute-Fame-Studio/3D-TurnBasedCombat" 28 | config/version="1.0" 29 | run/main_scene="res://maps/battle_scenes/test.tscn" 30 | config/features=PackedStringArray("4.5", "Forward Plus") 31 | config/icon="res://icon.svg" 32 | 33 | [autoload] 34 | 35 | GlobalBattleSettings="*res://assets/globals/battle-settings/GlobalBattleSettings.gd" 36 | SaveLoader="*res://assets/globals/save-system/save_loader.gd" 37 | AIManager="*res://battle-manager/ai_manager.gd" 38 | SignalBus="*res://assets/globals/signal_bus.gd" 39 | ItemHandler="*res://battle-manager/inventory/item_handler.gd" 40 | 41 | [dotnet] 42 | 43 | project/assembly_name="Turn-Based Combat" 44 | 45 | [file_customization] 46 | 47 | folder_colors={ 48 | "res://animations/": "red", 49 | "res://assets/": "orange", 50 | "res://battle-manager/": "teal", 51 | "res://database/": "blue" 52 | } 53 | 54 | [global_group] 55 | 56 | battler_data="" 57 | FRONTROW="" 58 | 59 | [input] 60 | 61 | ui_left={ 62 | "deadzone": 0.5, 63 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 64 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) 65 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null) 66 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) 67 | ] 68 | } 69 | ui_right={ 70 | "deadzone": 0.5, 71 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 72 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null) 73 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null) 74 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) 75 | ] 76 | } 77 | ui_up={ 78 | "deadzone": 0.5, 79 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 80 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) 81 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null) 82 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) 83 | ] 84 | } 85 | ui_down={ 86 | "deadzone": 0.5, 87 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 88 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) 89 | , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null) 90 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) 91 | ] 92 | } 93 | Select={ 94 | "deadzone": 0.2, 95 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) 96 | ] 97 | } 98 | Cancel={ 99 | "deadzone": 0.2, 100 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 101 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194308,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 102 | , Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(104, 28),"global_position":Vector2(113, 76),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) 103 | ] 104 | } 105 | Confirm={ 106 | "deadzone": 0.2, 107 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) 108 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battleresults.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://cn21e4r3htvwd"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://i6v7uo8qdg1d" path="res://Placeholder.svg" id="1_23vvh"] 4 | [ext_resource type="Texture2D" uid="uid://desmmvfvehe3b" path="res://assets/images/icons/Asset 11.png" id="2_1ilkb"] 5 | 6 | [sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_1ilkb"] 7 | 8 | [sub_resource type="Theme" id="Theme_1ilkb"] 9 | Label/font_sizes/font_size = 16 10 | 11 | [sub_resource type="Gradient" id="Gradient_8qws1"] 12 | colors = PackedColorArray(0.37413925, 0.37413928, 0.37413922, 1, 0.7162962, 0.7162962, 0.7162962, 1) 13 | 14 | [sub_resource type="GradientTexture2D" id="GradientTexture2D_xpspd"] 15 | gradient = SubResource("Gradient_8qws1") 16 | fill = 1 17 | fill_from = Vector2(0.5, 0.5) 18 | 19 | [node name="Control" type="Control"] 20 | layout_mode = 3 21 | anchors_preset = 15 22 | anchor_right = 1.0 23 | anchor_bottom = 1.0 24 | grow_horizontal = 2 25 | grow_vertical = 2 26 | 27 | [node name="ColorRect" type="ColorRect" parent="."] 28 | layout_mode = 1 29 | anchors_preset = 9 30 | anchor_bottom = 1.0 31 | offset_right = 441.0 32 | grow_vertical = 2 33 | color = Color(0.28485027, 0.2848503, 0.28485018, 0.4862745) 34 | 35 | [node name="Label" type="Label" parent="."] 36 | layout_mode = 1 37 | offset_left = 104.0 38 | offset_right = 326.0 39 | offset_bottom = 39.0 40 | theme_override_font_sizes/font_size = 28 41 | text = "Battle Results" 42 | vertical_alignment = 3 43 | uppercase = true 44 | 45 | [node name="BoxContainer" type="BoxContainer" parent="."] 46 | layout_mode = 0 47 | offset_top = 199.0 48 | offset_right = 441.0 49 | offset_bottom = 604.0 50 | theme_override_constants/separation = 8 51 | vertical = true 52 | 53 | [node name="BoxContainer2" type="BoxContainer" parent="BoxContainer"] 54 | layout_mode = 2 55 | 56 | [node name="TextureRect" type="TextureRect" parent="BoxContainer/BoxContainer2"] 57 | clip_contents = true 58 | custom_minimum_size = Vector2(32, 32) 59 | layout_mode = 2 60 | texture = ExtResource("1_23vvh") 61 | expand_mode = 1 62 | 63 | [node name="Label" type="Label" parent="BoxContainer/BoxContainer2"] 64 | layout_mode = 2 65 | theme_override_font_sizes/font_size = 24 66 | text = "Experince" 67 | 68 | [node name="Label2" type="Label" parent="BoxContainer/BoxContainer2"] 69 | layout_mode = 2 70 | size_flags_horizontal = 3 71 | theme_override_font_sizes/font_size = 26 72 | text = "100" 73 | horizontal_alignment = 2 74 | vertical_alignment = 1 75 | 76 | [node name="HSeparator2" type="HSeparator" parent="BoxContainer"] 77 | layout_mode = 2 78 | 79 | [node name="BoxContainer3" type="BoxContainer" parent="BoxContainer"] 80 | layout_mode = 2 81 | 82 | [node name="TextureRect" type="TextureRect" parent="BoxContainer/BoxContainer3"] 83 | clip_contents = true 84 | custom_minimum_size = Vector2(32, 32) 85 | layout_mode = 2 86 | texture = ExtResource("2_1ilkb") 87 | expand_mode = 1 88 | 89 | [node name="Label" type="Label" parent="BoxContainer/BoxContainer3"] 90 | layout_mode = 2 91 | theme_override_font_sizes/font_size = 24 92 | text = "Cash/Gold" 93 | 94 | [node name="Label2" type="Label" parent="BoxContainer/BoxContainer3"] 95 | layout_mode = 2 96 | size_flags_horizontal = 3 97 | theme_override_font_sizes/font_size = 26 98 | text = "£10" 99 | horizontal_alignment = 2 100 | vertical_alignment = 1 101 | 102 | [node name="HSeparator" type="HSeparator" parent="BoxContainer"] 103 | layout_mode = 2 104 | 105 | [node name="BoxContainer" type="BoxContainer" parent="BoxContainer"] 106 | layout_mode = 2 107 | 108 | [node name="TextureRect" type="TextureRect" parent="BoxContainer/BoxContainer"] 109 | clip_contents = true 110 | custom_minimum_size = Vector2(32, 32) 111 | layout_mode = 2 112 | texture = SubResource("PlaceholderTexture2D_1ilkb") 113 | 114 | [node name="Label" type="Label" parent="BoxContainer/BoxContainer"] 115 | layout_mode = 2 116 | text = "ITEMS" 117 | 118 | [node name="BoxContainer4" type="BoxContainer" parent="BoxContainer"] 119 | layout_mode = 2 120 | size_flags_horizontal = 3 121 | theme = SubResource("Theme_1ilkb") 122 | alignment = 2 123 | vertical = true 124 | 125 | [node name="BoxContainer3" type="BoxContainer" parent="BoxContainer/BoxContainer4"] 126 | layout_mode = 2 127 | 128 | [node name="Label" type="Label" parent="BoxContainer/BoxContainer4/BoxContainer3"] 129 | layout_mode = 2 130 | text = "Trash" 131 | 132 | [node name="Label2" type="Label" parent="BoxContainer/BoxContainer4/BoxContainer3"] 133 | layout_mode = 2 134 | size_flags_horizontal = 3 135 | text = "X1" 136 | horizontal_alignment = 2 137 | vertical_alignment = 1 138 | 139 | [node name="BoxContainer4" type="BoxContainer" parent="BoxContainer/BoxContainer4"] 140 | layout_mode = 2 141 | 142 | [node name="Label" type="Label" parent="BoxContainer/BoxContainer4/BoxContainer4"] 143 | layout_mode = 2 144 | text = "Opened Soda" 145 | 146 | [node name="Label2" type="Label" parent="BoxContainer/BoxContainer4/BoxContainer4"] 147 | layout_mode = 2 148 | size_flags_horizontal = 3 149 | text = "X1" 150 | horizontal_alignment = 2 151 | vertical_alignment = 1 152 | 153 | [node name="HSeparator4" type="HSeparator" parent="BoxContainer"] 154 | layout_mode = 2 155 | 156 | [node name="GridContainer" type="GridContainer" parent="."] 157 | layout_mode = 0 158 | offset_left = 670.0 159 | offset_right = 1150.0 160 | offset_bottom = 126.0 161 | columns = 4 162 | 163 | [node name="VBoxContainer" type="VBoxContainer" parent="GridContainer"] 164 | layout_mode = 2 165 | 166 | [node name="TextureRect" type="TextureRect" parent="GridContainer/VBoxContainer"] 167 | clip_contents = true 168 | custom_minimum_size = Vector2(64, 64) 169 | layout_mode = 2 170 | texture = SubResource("GradientTexture2D_xpspd") 171 | expand_mode = 1 172 | 173 | [node name="Label" type="Label" parent="GridContainer/VBoxContainer"] 174 | layout_mode = 2 175 | text = "Your Character" 176 | 177 | [node name="bar" type="ProgressBar" parent="GridContainer/VBoxContainer"] 178 | layout_mode = 2 179 | value = 48.0 180 | rounded = true 181 | show_percentage = false 182 | 183 | [node name="HBoxContainer" type="HBoxContainer" parent="GridContainer/VBoxContainer"] 184 | layout_mode = 2 185 | 186 | [node name="Label2" type="Label" parent="GridContainer/VBoxContainer/HBoxContainer"] 187 | layout_mode = 2 188 | text = "Lv. 20" 189 | 190 | [node name="Label3" type="Label" parent="GridContainer/VBoxContainer/HBoxContainer"] 191 | layout_mode = 2 192 | size_flags_horizontal = 10 193 | text = "X1.0" 194 | 195 | [node name="VBoxContainer2" type="VBoxContainer" parent="GridContainer"] 196 | layout_mode = 2 197 | 198 | [node name="TextureRect" type="TextureRect" parent="GridContainer/VBoxContainer2"] 199 | clip_contents = true 200 | custom_minimum_size = Vector2(64, 64) 201 | layout_mode = 2 202 | texture = SubResource("GradientTexture2D_xpspd") 203 | expand_mode = 1 204 | 205 | [node name="Label" type="Label" parent="GridContainer/VBoxContainer2"] 206 | layout_mode = 2 207 | text = "Your Character" 208 | 209 | [node name="bar" type="ProgressBar" parent="GridContainer/VBoxContainer2"] 210 | layout_mode = 2 211 | value = 48.0 212 | rounded = true 213 | show_percentage = false 214 | 215 | [node name="HBoxContainer" type="HBoxContainer" parent="GridContainer/VBoxContainer2"] 216 | layout_mode = 2 217 | 218 | [node name="Label2" type="Label" parent="GridContainer/VBoxContainer2/HBoxContainer"] 219 | layout_mode = 2 220 | text = "Lv. 20" 221 | 222 | [node name="Label3" type="Label" parent="GridContainer/VBoxContainer2/HBoxContainer"] 223 | layout_mode = 2 224 | size_flags_horizontal = 10 225 | text = "X1.0" 226 | 227 | [node name="VBoxContainer3" type="VBoxContainer" parent="GridContainer"] 228 | layout_mode = 2 229 | 230 | [node name="TextureRect" type="TextureRect" parent="GridContainer/VBoxContainer3"] 231 | clip_contents = true 232 | custom_minimum_size = Vector2(64, 64) 233 | layout_mode = 2 234 | texture = SubResource("GradientTexture2D_xpspd") 235 | expand_mode = 1 236 | 237 | [node name="Label" type="Label" parent="GridContainer/VBoxContainer3"] 238 | layout_mode = 2 239 | text = "Your Character" 240 | 241 | [node name="bar" type="ProgressBar" parent="GridContainer/VBoxContainer3"] 242 | layout_mode = 2 243 | value = 48.0 244 | rounded = true 245 | show_percentage = false 246 | 247 | [node name="HBoxContainer" type="HBoxContainer" parent="GridContainer/VBoxContainer3"] 248 | layout_mode = 2 249 | 250 | [node name="Label2" type="Label" parent="GridContainer/VBoxContainer3/HBoxContainer"] 251 | layout_mode = 2 252 | text = "Lv. 20" 253 | 254 | [node name="Label3" type="Label" parent="GridContainer/VBoxContainer3/HBoxContainer"] 255 | layout_mode = 2 256 | size_flags_horizontal = 10 257 | text = "X1.0" 258 | 259 | [node name="VBoxContainer4" type="VBoxContainer" parent="GridContainer"] 260 | layout_mode = 2 261 | 262 | [node name="TextureRect" type="TextureRect" parent="GridContainer/VBoxContainer4"] 263 | clip_contents = true 264 | custom_minimum_size = Vector2(64, 64) 265 | layout_mode = 2 266 | texture = SubResource("GradientTexture2D_xpspd") 267 | expand_mode = 1 268 | 269 | [node name="Label" type="Label" parent="GridContainer/VBoxContainer4"] 270 | layout_mode = 2 271 | text = "Your Character" 272 | 273 | [node name="bar" type="ProgressBar" parent="GridContainer/VBoxContainer4"] 274 | layout_mode = 2 275 | value = 48.0 276 | rounded = true 277 | show_percentage = false 278 | 279 | [node name="HBoxContainer" type="HBoxContainer" parent="GridContainer/VBoxContainer4"] 280 | layout_mode = 2 281 | 282 | [node name="Label2" type="Label" parent="GridContainer/VBoxContainer4/HBoxContainer"] 283 | layout_mode = 2 284 | text = "Lv. 20" 285 | 286 | [node name="Label3" type="Label" parent="GridContainer/VBoxContainer4/HBoxContainer"] 287 | layout_mode = 2 288 | size_flags_horizontal = 10 289 | text = "X1.0" 290 | -------------------------------------------------------------------------------- /battle-manager/battlehud/battlehud.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://cyjcaceen0235"] 2 | 3 | [ext_resource type="Script" uid="uid://835wmi7qej15" path="res://battle-manager/battlehud/battlehud.gd" id="1_0qukq"] 4 | [ext_resource type="Script" uid="uid://dbbsrqrispnmb" path="res://battle-manager/battlehud/battler_info.gd" id="2_64dmw"] 5 | [ext_resource type="Theme" uid="uid://crdk7x2bxdhob" path="res://replace/theme_test.tres" id="2_uiugx"] 6 | [ext_resource type="PackedScene" uid="uid://xtvr2wp8ojvb" path="res://battle-manager/battlehud/skill-button-preset.tscn" id="4_t5sg5"] 7 | 8 | [sub_resource type="GDScript" id="GDScript_mbt63"] 9 | resource_name = "EnemyStats" 10 | script/source = "extends VBoxContainer 11 | 12 | # Declare the progress bar node 13 | @onready var name_label = $EnemyNameLabel 14 | @onready var health_bar = $EnemyHealthBar 15 | 16 | func add_character(_character: Node): 17 | pass 18 | 19 | func update_enemy_stats(character: Node): 20 | if character and is_instance_valid(character): 21 | name_label.text = character.character_name 22 | health_bar.max_value = character.max_health 23 | health_bar.value = character.current_health 24 | 25 | func update_character_info(character: Node): 26 | update_enemy_stats(character) 27 | " 28 | 29 | [sub_resource type="GDScript" id="GDScript_djde1"] 30 | resource_name = "EnemyStats" 31 | script/source = "extends VBoxContainer 32 | 33 | # Declare the progress bar node 34 | @onready var enemy_health_bar: ProgressBar = $EnemyHealthBar 35 | 36 | func add_character(_character: Node): 37 | pass 38 | 39 | #func update_enemy_stats(character: Node): 40 | ## Update enemy-specific UI elements 41 | #$EnemyNameLabel.text = character.character_name 42 | #$EnemyHealthBar.max_value = character.max_health 43 | #$EnemyHealthBar.value = character.current_health 44 | 45 | #func update_enemy_info(enemy: Node): 46 | #$EnemyNameLabel.text = enemy.character_name 47 | #$EnemyHealthBar.max_value = enemy.max_health 48 | #$EnemyHealthBar.value = enemy.current_health 49 | " 50 | 51 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7jqjx"] 52 | 53 | [node name="BattleHUD" type="CanvasLayer" groups=["BattleHud"]] 54 | script = ExtResource("1_0qukq") 55 | 56 | [node name="Control" type="Control" parent="."] 57 | layout_mode = 3 58 | anchors_preset = 15 59 | anchor_right = 1.0 60 | anchor_bottom = 1.0 61 | grow_horizontal = 2 62 | grow_vertical = 2 63 | theme = ExtResource("2_uiugx") 64 | 65 | [node name="ActionButtons" type="BoxContainer" parent="Control"] 66 | layout_mode = 1 67 | anchors_preset = 2 68 | anchor_top = 1.0 69 | anchor_bottom = 1.0 70 | offset_top = -226.0 71 | offset_right = 238.0 72 | grow_vertical = 0 73 | size_flags_horizontal = 0 74 | size_flags_vertical = 4 75 | theme = ExtResource("2_uiugx") 76 | alignment = 2 77 | vertical = true 78 | 79 | [node name="Attack" type="Button" parent="Control/ActionButtons"] 80 | layout_mode = 2 81 | size_flags_horizontal = 0 82 | size_flags_vertical = 8 83 | text = "Attack 84 | " 85 | 86 | [node name="Skills" type="Button" parent="Control/ActionButtons"] 87 | layout_mode = 2 88 | size_flags_horizontal = 0 89 | text = "Skills" 90 | 91 | [node name="Items" type="Button" parent="Control/ActionButtons"] 92 | layout_mode = 2 93 | size_flags_horizontal = 0 94 | text = "Items" 95 | 96 | [node name="Defend" type="Button" parent="Control/ActionButtons"] 97 | layout_mode = 2 98 | size_flags_horizontal = 0 99 | text = "Defend" 100 | 101 | [node name="Run" type="Button" parent="Control/ActionButtons"] 102 | layout_mode = 2 103 | size_flags_horizontal = 0 104 | text = "Run" 105 | 106 | [node name="Enemies" type="Control" parent="Control"] 107 | layout_mode = 1 108 | anchors_preset = 1 109 | anchor_left = 1.0 110 | anchor_right = 1.0 111 | offset_left = -212.0 112 | offset_bottom = 228.0 113 | grow_horizontal = 0 114 | 115 | [node name="AllEnemies" type="BoxContainer" parent="Control/Enemies"] 116 | layout_mode = 1 117 | anchors_preset = 15 118 | anchor_right = 1.0 119 | anchor_bottom = 1.0 120 | grow_horizontal = 2 121 | grow_vertical = 2 122 | vertical = true 123 | 124 | [node name="EnemyStats" type="VBoxContainer" parent="Control/Enemies/AllEnemies"] 125 | layout_mode = 2 126 | script = SubResource("GDScript_mbt63") 127 | 128 | [node name="EnemyNameLabel" type="Label" parent="Control/Enemies/AllEnemies/EnemyStats"] 129 | layout_mode = 2 130 | text = "Test" 131 | 132 | [node name="EnemyHealthBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats"] 133 | layout_mode = 2 134 | 135 | [node name="EnemySPBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats"] 136 | layout_mode = 2 137 | 138 | [node name="EnemyStats2" type="VBoxContainer" parent="Control/Enemies/AllEnemies"] 139 | visible = false 140 | layout_mode = 2 141 | script = SubResource("GDScript_djde1") 142 | 143 | [node name="EnemyNameLabel" type="Label" parent="Control/Enemies/AllEnemies/EnemyStats2"] 144 | layout_mode = 2 145 | text = "Test" 146 | 147 | [node name="EnemyHealthBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats2"] 148 | layout_mode = 2 149 | 150 | [node name="EnemySPBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats2"] 151 | layout_mode = 2 152 | 153 | [node name="EnemyStats3" type="VBoxContainer" parent="Control/Enemies/AllEnemies"] 154 | visible = false 155 | layout_mode = 2 156 | script = SubResource("GDScript_mbt63") 157 | 158 | [node name="EnemyNameLabel" type="Label" parent="Control/Enemies/AllEnemies/EnemyStats3"] 159 | layout_mode = 2 160 | text = "Test" 161 | 162 | [node name="EnemyHealthBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats3"] 163 | layout_mode = 2 164 | 165 | [node name="EnemySPBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats3"] 166 | layout_mode = 2 167 | 168 | [node name="EnemyStats4" type="VBoxContainer" parent="Control/Enemies/AllEnemies"] 169 | visible = false 170 | layout_mode = 2 171 | script = SubResource("GDScript_djde1") 172 | 173 | [node name="EnemyNameLabel" type="Label" parent="Control/Enemies/AllEnemies/EnemyStats4"] 174 | layout_mode = 2 175 | text = "Test" 176 | 177 | [node name="EnemyHealthBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats4"] 178 | layout_mode = 2 179 | 180 | [node name="EnemySPBar" type="ProgressBar" parent="Control/Enemies/AllEnemies/EnemyStats4"] 181 | layout_mode = 2 182 | 183 | [node name="Players" type="Control" parent="Control"] 184 | anchors_preset = 0 185 | offset_right = 183.0 186 | offset_bottom = 170.0 187 | 188 | [node name="AllAllies" type="BoxContainer" parent="Control/Players"] 189 | layout_mode = 0 190 | offset_right = 183.0 191 | offset_bottom = 228.0 192 | vertical = true 193 | 194 | [node name="AllyStats" type="VBoxContainer" parent="Control/Players/AllAllies"] 195 | layout_mode = 2 196 | script = ExtResource("2_64dmw") 197 | 198 | [node name="PlayerNameLabel" type="Label" parent="Control/Players/AllAllies/AllyStats"] 199 | layout_mode = 2 200 | text = "Test" 201 | 202 | [node name="PlayerHealthBar" type="ProgressBar" parent="Control/Players/AllAllies/AllyStats"] 203 | layout_mode = 2 204 | 205 | [node name="PlayerSPBar" type="ProgressBar" parent="Control/Players/AllAllies/AllyStats"] 206 | layout_mode = 2 207 | 208 | [node name="AllyStats2" type="VBoxContainer" parent="Control/Players/AllAllies"] 209 | visible = false 210 | layout_mode = 2 211 | script = ExtResource("2_64dmw") 212 | 213 | [node name="PlayerNameLabel" type="Label" parent="Control/Players/AllAllies/AllyStats2"] 214 | layout_mode = 2 215 | text = "Test" 216 | 217 | [node name="PlayerHealthBar" type="ProgressBar" parent="Control/Players/AllAllies/AllyStats2"] 218 | layout_mode = 2 219 | 220 | [node name="AllyStats3" type="VBoxContainer" parent="Control/Players/AllAllies"] 221 | visible = false 222 | layout_mode = 2 223 | script = ExtResource("2_64dmw") 224 | 225 | [node name="PlayerNameLabel" type="Label" parent="Control/Players/AllAllies/AllyStats3"] 226 | layout_mode = 2 227 | text = "Test" 228 | 229 | [node name="PlayerHealthBar" type="ProgressBar" parent="Control/Players/AllAllies/AllyStats3"] 230 | layout_mode = 2 231 | 232 | [node name="AllyStats4" type="VBoxContainer" parent="Control/Players/AllAllies"] 233 | visible = false 234 | layout_mode = 2 235 | script = ExtResource("2_64dmw") 236 | 237 | [node name="PlayerNameLabel" type="Label" parent="Control/Players/AllAllies/AllyStats4"] 238 | layout_mode = 2 239 | text = "Test" 240 | 241 | [node name="PlayerHealthBar" type="ProgressBar" parent="Control/Players/AllAllies/AllyStats4"] 242 | layout_mode = 2 243 | 244 | [node name="BattleResults" type="Control" parent="Control"] 245 | anchors_preset = 0 246 | offset_right = 40.0 247 | offset_bottom = 40.0 248 | 249 | [node name="BattleResultLabel" type="Label" parent="Control/BattleResults"] 250 | layout_mode = 0 251 | offset_top = 225.0 252 | offset_right = 1152.0 253 | offset_bottom = 365.0 254 | 255 | [node name="Skills" type="Control" parent="Control"] 256 | visible = false 257 | layout_mode = 1 258 | anchors_preset = 7 259 | anchor_left = 0.5 260 | anchor_top = 1.0 261 | anchor_right = 0.5 262 | anchor_bottom = 1.0 263 | offset_left = -250.0 264 | offset_top = -164.0 265 | offset_right = 201.0 266 | grow_horizontal = 2 267 | grow_vertical = 0 268 | 269 | [node name="ScrollContainer" type="ScrollContainer" parent="Control/Skills"] 270 | clip_contents = false 271 | layout_mode = 1 272 | anchors_preset = 15 273 | anchor_right = 1.0 274 | anchor_bottom = 1.0 275 | grow_horizontal = 2 276 | grow_vertical = 2 277 | theme_override_styles/panel = SubResource("StyleBoxFlat_7jqjx") 278 | horizontal_scroll_mode = 0 279 | vertical_scroll_mode = 2 280 | metadata/_edit_use_anchors_ = true 281 | 282 | [node name="BoxContainer" type="BoxContainer" parent="Control/Skills/ScrollContainer"] 283 | layout_mode = 2 284 | size_flags_horizontal = 3 285 | vertical = true 286 | 287 | [node name="SkillRow" parent="Control/Skills/ScrollContainer/BoxContainer" instance=ExtResource("4_t5sg5")] 288 | layout_mode = 2 289 | size_flags_vertical = 4 290 | 291 | [node name="SkillRow2" parent="Control/Skills/ScrollContainer/BoxContainer" instance=ExtResource("4_t5sg5")] 292 | layout_mode = 2 293 | 294 | [node name="Items" type="Control" parent="Control"] 295 | visible = false 296 | layout_mode = 1 297 | anchors_preset = 7 298 | anchor_left = 0.5 299 | anchor_top = 1.0 300 | anchor_right = 0.5 301 | anchor_bottom = 1.0 302 | offset_left = -250.0 303 | offset_top = -164.0 304 | offset_right = 201.0 305 | grow_horizontal = 2 306 | grow_vertical = 0 307 | 308 | [node name="ScrollContainer" type="ScrollContainer" parent="Control/Items"] 309 | clip_contents = false 310 | layout_mode = 1 311 | anchors_preset = 15 312 | anchor_right = 1.0 313 | anchor_bottom = 1.0 314 | grow_horizontal = 2 315 | grow_vertical = 2 316 | theme_override_styles/panel = SubResource("StyleBoxFlat_7jqjx") 317 | horizontal_scroll_mode = 0 318 | vertical_scroll_mode = 2 319 | metadata/_edit_use_anchors_ = true 320 | 321 | [node name="BoxContainer" type="BoxContainer" parent="Control/Items/ScrollContainer"] 322 | layout_mode = 2 323 | size_flags_horizontal = 3 324 | vertical = true 325 | 326 | [node name="SkillRow" parent="Control/Items/ScrollContainer/BoxContainer" instance=ExtResource("4_t5sg5")] 327 | layout_mode = 2 328 | size_flags_vertical = 4 329 | 330 | [node name="SkillRow2" parent="Control/Items/ScrollContainer/BoxContainer" instance=ExtResource("4_t5sg5")] 331 | layout_mode = 2 332 | 333 | [connection signal="pressed" from="Control/ActionButtons/Attack" to="." method="_on_attack_pressed"] 334 | [connection signal="pressed" from="Control/ActionButtons/Skills" to="." method="_on_skills_pressed"] 335 | [connection signal="pressed" from="Control/ActionButtons/Items" to="." method="_on_items_pressed"] 336 | [connection signal="pressed" from="Control/ActionButtons/Defend" to="." method="_on_defend_pressed"] 337 | [connection signal="pressed" from="Control/ActionButtons/Run" to="." method="_on_run_pressed"] 338 | -------------------------------------------------------------------------------- /battle-manager/scripts/battler_scripts/battler.gd: -------------------------------------------------------------------------------- 1 | class_name Battler 2 | extends CharacterBody3D 3 | 4 | signal anim_damage() 5 | enum TEAM {ALLY, ENEMY} 6 | 7 | @export var stats: BattlerStats 8 | @export var inventory: Inventory 9 | @export var default_attack:Skill # Basic attack as a skill 10 | @export_group("Team and AI Controls") 11 | ## Define the battler's Team - Allies are Player-controlled 12 | @export var team: TEAM # This will default to ALLY 13 | ## If the battler will act independent of player selection, how optimal is it? 14 | ## 0 = Randumb, 100 = Big Brain 15 | @export_range(0, 100, 1) var intelligence:int 16 | enum AIType {AGGRESSIVE, DEFENSIVE} 17 | ## If this battler acts on its own, what is its strategy/approach to combat? 18 | ## Interacts with intelligence to make "optimal" decision. 19 | @export var ai_type:AIType 20 | 21 | var character_name: String 22 | var max_health: int 23 | var attack: int 24 | var defense: int 25 | var agility: int 26 | 27 | var current_health: int 28 | var current_sp: int = 100 # Add SP variables 29 | var max_sp: int = 100 # Add max SP 30 | var is_defending: bool = false 31 | var current_target = null 32 | 33 | # Walking animation system 34 | var is_walking: bool = false 35 | var walk_target_position: Vector3 36 | var original_position: Vector3 37 | 38 | # Per-battler walking settings (override global if set) 39 | @export_group("Walking Settings", "walking") 40 | ## Distance at which this battler requires walking to target. -1.0 = use global setting 41 | @export var custom_walking_distance: float = -1.0 42 | ## Speed of walking animation for this battler. -1.0 = use global setting 43 | @export var custom_walking_speed: float = -1.0 44 | ## Walking animation name for this battler. Empty = use global setting 45 | @export var custom_walking_animation: String = "" 46 | ## Whether this battler requires walking before attacking 47 | @export var requires_walking: bool = true 48 | ## 49 | ## CUSTOM WALKING SETTINGS EXPLAINED: 50 | ## -1.0 values mean "use the global setting from BattleManager" 51 | ## Set positive values to override global settings for this specific battler 52 | ## Example: Set custom_walking_distance = 5.0 for a big monster that needs more space 53 | ## Example: Set custom_walking_speed = 1.0 for a slow character 54 | ## Example: Set custom_walking_animation = "my_walk_anim" for custom animation 55 | 56 | # Targeting controls 57 | @onready var material:Material = %Alpha_Surface.material_override 58 | @onready var select_outline:Shader = preload("res://assets/shaders/battler_select_shader.gdshader") 59 | var is_selectable: bool = false: 60 | set(value): 61 | is_selectable = value 62 | if !is_selectable: 63 | is_targeted = false 64 | material.next_pass = null 65 | _update_highlight() 66 | 67 | var is_targeted: bool = false: 68 | set(value): 69 | is_targeted = value 70 | _update_highlight() 71 | 72 | var mouse_hover: bool = false: 73 | set(value): 74 | mouse_hover = value 75 | _update_highlight() 76 | 77 | var is_valid_target: bool = false 78 | var is_default_target: bool = false 79 | var is_keyboard_selected: bool = false # Track if selected via keyboard 80 | var is_mouse_selected: bool = false # Track if selected via mouse 81 | 82 | func _update_highlight() -> void: 83 | if !is_selectable or !is_valid_target: 84 | material.next_pass = null 85 | return 86 | 87 | # Clear any existing highlight first 88 | material.next_pass = null 89 | 90 | # Mouse hover takes priority over everything else 91 | if mouse_hover and is_selectable: 92 | # White hover outline (highest priority) 93 | var hover_mat = ShaderMaterial.new() 94 | hover_mat.shader = select_outline 95 | hover_mat.set_shader_parameter("color", Color.WHITE) 96 | hover_mat.set_shader_parameter("thickness", 0.02) 97 | hover_mat.set_shader_parameter("alpha", 0.6) 98 | material.next_pass = hover_mat 99 | elif is_targeted or is_mouse_selected or is_keyboard_selected or is_default_target: 100 | # Main selection outline (cyan for all input methods) 101 | var shader_mat = ShaderMaterial.new() 102 | shader_mat.shader = select_outline 103 | shader_mat.set_shader_parameter("color", Color.CYAN) 104 | shader_mat.set_shader_parameter("thickness", 0.025) 105 | shader_mat.set_shader_parameter("alpha", 1.0) 106 | material.next_pass = shader_mat 107 | 108 | # Add debug info to confirm which battler is highlighted 109 | print("Highlighting battler: ", character_name, " with cyan outline") 110 | 111 | @export_group("Special Dependencies") 112 | @onready var basic_attack_animation = "attack" 113 | @onready var state_machine = $AnimationTree["parameters/playback"] 114 | @export var skill_node: SkillList 115 | @onready var skill_list: Array[Skill] = [] 116 | @onready var exp_node: Experience = get_node("Experience") 117 | @export var damage_indicator_subviewport:SubViewport 118 | @onready var anim_tree: AnimationTree = $AnimationTree 119 | 120 | func _ready(): 121 | SignalBus.select_target.connect(check_select_target) 122 | SignalBus.allow_select_target.connect(set_selectable) 123 | SignalBus.hover_target.connect(check_hover_target) 124 | SignalBus.clear_default_selection.connect(_clear_default_selection) 125 | if !default_attack: 126 | default_attack = load("res://database/skills/normal_attack.tres") 127 | 128 | # If all battlers use the same material, or if there are duplicate battlers of the same type 129 | # this ensures that they have uniquely assigned materials so the shader does not apply 130 | # to ALL of them 131 | var dupe_mat:Material = material.duplicate() 132 | %Alpha_Surface.material_override = dupe_mat 133 | material = %Alpha_Surface.material_override 134 | 135 | if stats: 136 | # Basic stats 137 | character_name = stats.character_name 138 | %BattlerNameLabel.text = character_name 139 | 140 | max_health = stats.max_health 141 | current_health = max_health 142 | %BattlerHealthBar.max_value = max_health 143 | %BattlerHealthBar.value = current_health 144 | 145 | attack = stats.attack 146 | defense = stats.defense 147 | agility = stats.agility 148 | 149 | # SP stats 150 | max_sp = stats.max_sp 151 | current_sp = max_sp 152 | 153 | if skill_node: 154 | var updated_skill_list:Array[Skill] = skill_node.get_skills() 155 | for skill in updated_skill_list: 156 | if skill is Skill and !skill_list.has(skill): 157 | skill_list.append(skill) 158 | 159 | if default_attack: 160 | skill_list.append(default_attack) 161 | else: 162 | push_error("BattlerStats resource not set!") 163 | 164 | # Assign to group based on team 165 | if team == TEAM.ENEMY: 166 | add_to_group("enemies") 167 | elif team == TEAM.ALLY: 168 | add_to_group("players") 169 | print("Current Element: ", stats.element) 170 | 171 | # Set up animation parameters 172 | anim_tree.set("parameters/conditions/is_turning_right", false) 173 | anim_tree.set("parameters/conditions/is_turning_left", false) 174 | 175 | func _input(event: InputEvent) -> void: 176 | # Check if mouse input is enabled in battle manager 177 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 178 | if battle_manager and not battle_manager.mouse_input_toggle: 179 | return 180 | 181 | if event.is_action_pressed("Select") and is_selectable and is_valid_target: 182 | print("=== MOUSE INPUT DEBUG ===") 183 | print("Battler: ", character_name) 184 | print("Event: ", event) 185 | print("Is selectable: ", is_selectable) 186 | print("Is valid target: ", is_valid_target) 187 | # Allow selection if valid target, regardless of hover state 188 | select_target() 189 | elif event is InputEventScreenTouch and event.pressed and is_valid_target: 190 | # For touchscreen devices, treat touch as selection 191 | if is_selectable: 192 | select_target() 193 | elif event is InputEventScreenTouch and !event.pressed: 194 | # Touch release - clear hover state 195 | has_hover(false) 196 | 197 | func _mouse_enter() -> void: 198 | # Check if mouse input is enabled in battle manager 199 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 200 | if battle_manager and not battle_manager.mouse_input_toggle: 201 | return 202 | 203 | print("=== MOUSE ENTER DEBUG ===") 204 | print("Battler: ", character_name) 205 | print("Is valid target: ", is_valid_target) 206 | if is_valid_target: 207 | has_hover(true) 208 | 209 | func _mouse_exit() -> void: 210 | # Check if mouse input is enabled in battle manager 211 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 212 | if battle_manager and not battle_manager.mouse_input_toggle: 213 | return 214 | 215 | print("=== MOUSE EXIT DEBUG ===") 216 | print("Battler: ", character_name) 217 | has_hover(false) 218 | func has_hover(hover:bool = false) -> void: 219 | # Only allow hover if this battler is a valid target 220 | if hover and !is_valid_target: 221 | return 222 | mouse_hover = hover 223 | 224 | # Emit hover signal when hovering over a valid target 225 | if hover and is_valid_target: 226 | SignalBus.hover_target.emit(self) 227 | 228 | func set_selectable(can_target: bool) -> void: 229 | # Don't override is_valid_target here - that's set by BattleManager based on skill requirements 230 | is_selectable = can_target and is_valid_target 231 | 232 | if !is_selectable: 233 | clear_all_selections() 234 | _update_highlight() 235 | 236 | func check_select_target(target:Battler) -> void: 237 | if target != self and is_targeted: 238 | deselect_as_target() 239 | 240 | func check_hover_target(target:Battler) -> void: 241 | # Emit signal to clear all default selections 242 | SignalBus.clear_default_selection.emit() 243 | 244 | func _clear_default_selection() -> void: 245 | # Clear this battler's default selection if it has one 246 | if is_default_target: 247 | is_default_target = false 248 | _update_highlight() 249 | 250 | func select_target() -> void: 251 | print("=== TARGET SELECTION DEBUG ===") 252 | print("Battler: ", character_name) 253 | print("Is selectable: ", is_selectable) 254 | print("Is valid target: ", is_valid_target) 255 | print("Is targeted: ", is_targeted) 256 | 257 | # Will probably want to also add logic that prevents selecting invalid targets 258 | # Clear all other selection states first 259 | is_keyboard_selected = false 260 | is_default_target = false 261 | is_mouse_selected = true 262 | is_targeted = true 263 | print("Emitting select_target signal for: ", character_name) 264 | SignalBus.select_target.emit(self) 265 | 266 | func deselect_as_target() -> void: 267 | is_targeted = false 268 | is_mouse_selected = false 269 | is_keyboard_selected = false 270 | is_default_target = false 271 | # Force update the highlight to clear it 272 | _update_highlight() 273 | 274 | func set_as_default_target() -> void: 275 | # Clear other selection states first 276 | is_mouse_selected = false 277 | is_keyboard_selected = false 278 | is_default_target = true 279 | is_targeted = true 280 | # Force update the highlight 281 | _update_highlight() 282 | 283 | func set_as_keyboard_target() -> void: 284 | # Clear other selection types first 285 | is_mouse_selected = false 286 | is_default_target = false 287 | is_keyboard_selected = true 288 | is_targeted = true 289 | # Force update the highlight 290 | _update_highlight() 291 | 292 | func clear_all_selections() -> void: 293 | is_targeted = false 294 | is_mouse_selected = false 295 | is_keyboard_selected = false 296 | is_default_target = false 297 | mouse_hover = false 298 | material.next_pass = null 299 | 300 | 301 | func is_defeated() -> bool: 302 | return current_health <= 0 303 | 304 | func get_attack_damage(target) -> int: 305 | print("PLAYER: calculating damage for target: ", target.character_name) 306 | var damage = attack + randi() % 5 307 | return Formulas.physical_damage(self, target, damage) 308 | 309 | @onready var floating_damage_num:PackedScene = preload("res://battle-manager/damage_number.tscn") 310 | func take_damage(amount: int, attacker: Battler = null) -> void: 311 | var damage_reduction = defense 312 | if is_defending: 313 | damage_reduction *= 2 314 | is_defending = false 315 | 316 | var damage_num: DamageNumber = floating_damage_num.instantiate() 317 | var damage_taken = max(0, amount - damage_reduction) 318 | damage_num.value = damage_taken 319 | damage_indicator_subviewport.add_child(damage_num) 320 | current_health -= damage_taken 321 | %BattlerHealthBar.value = current_health 322 | if current_health < 0: 323 | current_health = 0 324 | 325 | print("%s took %d damage. Health: %d/%d" % [character_name, damage_taken, current_health, max_health]) 326 | 327 | # Check if this battler is defeated and should be removed 328 | if current_health <= 0 and team == TEAM.ENEMY: 329 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 330 | if battle_manager and battle_manager.remove_defeated_enemies: 331 | print("Removing defeated enemy: ", character_name) 332 | # Remove from turn order 333 | if battle_manager.turn_order.has(self): 334 | battle_manager.turn_order.erase(self) 335 | # Remove from enemies array 336 | if battle_manager.enemies.has(self): 337 | battle_manager.enemies.erase(self) 338 | # Remove from scene 339 | call_deferred("queue_free") 340 | 341 | # Handle counter if we have the state 342 | if attacker and !attacker.is_defeated(): 343 | for state in active_states.values(): 344 | if state is CounterState: 345 | state.perform_counter(self, attacker) 346 | break 347 | 348 | func take_healing(amount: int): 349 | var healing = min(amount, max_health - current_health) 350 | 351 | current_health += healing 352 | %BattlerHealthBar.value = current_health 353 | print("%s received %d healing. Health: %d/%d" % [character_name, healing, current_health, max_health]) 354 | return healing 355 | 356 | func defend(): 357 | is_defending = true 358 | print("%s is defending. Defense doubled for the next attack." % character_name) 359 | 360 | # Play defend animation 361 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 362 | var defend_anim = battle_manager.get_animation("defend") if battle_manager else "defend" 363 | state_machine.travel(defend_anim) 364 | 365 | func gain_experience(amount: int): 366 | print("%s gained %d experience!" % [character_name, amount]) 367 | print("%s needs %d to level up!" % [character_name, get_exp_stat().get_exp_to_level()]) 368 | 369 | func battle_run(): 370 | pass 371 | 372 | func battle_item(item:Item, target:Battler) -> void: 373 | # TODO: Check if user can legally use this item/take this action 374 | if inventory.remove_item_from_collection(item) == Inventory.Resolution.SUCCESS: 375 | ItemHandler.use_item(item, target) 376 | # Integrate a simple inventory system. Trying to do this alone may cause mistakes. 377 | 378 | func battle_idle(): 379 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 380 | var anim_name = battle_manager.get_animation("idle") if battle_manager else "idle1" 381 | state_machine.travel(anim_name) 382 | 383 | func attack_anim(target) -> int: 384 | print("PLAYER: Starting attack sequence for target: ", target.character_name) 385 | current_target = target 386 | 387 | # First turn to face walking direction 388 | await turn_to_face_target(target) 389 | 390 | # Start walking if needed 391 | if await walk_to_target(target): 392 | print("Walking to target") 393 | state_machine.travel("walk") # Changed from "run" to "walk" 394 | while is_walking: 395 | await get_tree().create_timer(0.016).timeout 396 | 397 | # Turn to face target again after walking (in case target moved) 398 | await turn_to_face_target(target) 399 | 400 | # Now perform the attack 401 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 402 | var attack_anim = battle_manager.get_animation("attack") if battle_manager else "attack" 403 | state_machine.travel(attack_anim) 404 | 405 | return 0 406 | 407 | #func deal_damage(): 408 | #return get_attack_damage(target) 409 | 410 | func use_skill(skill:Skill, target) -> int: 411 | if skill.can_use(self): 412 | # Turn to face target before using skill 413 | await turn_to_face_target(target) 414 | 415 | # Try to walk to target first for damage skills 416 | if skill.effect_type == Skill.EFFECT_TYPE.DAMAGE and await walk_to_target(target): 417 | # Wait for walking to complete, then use skill 418 | await get_tree().create_timer(0.1).timeout 419 | while is_walking: 420 | await get_tree().create_timer(0.1).timeout 421 | 422 | # Use skill animation name if available, otherwise use attack animation 423 | var anim_name = skill.animation_name 424 | if anim_name.is_empty(): 425 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 426 | anim_name = battle_manager.get_animation("attack") if battle_manager else "attack" 427 | 428 | state_machine.travel(anim_name) 429 | skill.apply_costs(self) 430 | 431 | match skill.effect_type: 432 | "Damage": 433 | return Formulas.calculate_damage(self, target, skill) 434 | "Heal": 435 | return skill.base_power 436 | _: 437 | return 0 438 | return 0 439 | 440 | func wait_attack(): 441 | if self.is_defending: 442 | return 443 | await $AnimationTree.animation_finished 444 | 445 | # If we walked to attack, wait for return movement to complete 446 | if original_position != Vector3.ZERO and not is_walking: 447 | # Start return movement if not already started 448 | return_to_original_position() 449 | # Wait for return movement to complete 450 | while is_walking: 451 | await get_tree().create_timer(0.1).timeout 452 | 453 | battle_idle() 454 | 455 | # Turning animation system 456 | var is_turning: bool = false 457 | var turn_target_rotation: float 458 | 459 | var is_turning_right: bool = false 460 | var is_turning_left: bool = false 461 | 462 | func turn_to_face_target(target: Battler) -> void: 463 | var direction = target.global_position - global_position 464 | var target_angle = atan2(direction.x, direction.z) 465 | var current_angle = rotation.y 466 | var angle_diff = wrapf(target_angle - current_angle, -PI, PI) 467 | 468 | if abs(angle_diff) > 0.1: 469 | is_turning = true 470 | turn_target_rotation = target_angle 471 | 472 | # Start appropriate turn animation 473 | if angle_diff > 0: 474 | state_machine.travel("turn_right") 475 | else: 476 | state_machine.travel("turn_left") 477 | 478 | # Wait for turn animation to complete 479 | while is_turning: 480 | # Smoothly interpolate rotation to match animation 481 | rotation.y = lerp_angle(rotation.y, turn_target_rotation, 0.1) 482 | 483 | if abs(wrapf(rotation.y - turn_target_rotation, -PI, PI)) < 0.05: 484 | rotation.y = turn_target_rotation 485 | is_turning = false 486 | 487 | # Ensure we transition back to idle through the animation system 488 | state_machine.travel("idle1") 489 | 490 | await get_tree().create_timer(0.016).timeout 491 | 492 | await get_tree().create_timer(0.016).timeout 493 | 494 | # Walking animation system functions 495 | func walk_to_target(target: Battler) -> bool: 496 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 497 | if not battle_manager: 498 | print("No battle manager found, skipping walking") 499 | return false 500 | 501 | # Check if walking is enabled globally and for this battler 502 | if not battle_manager.enable_walking_to_target or not requires_walking: 503 | return false 504 | 505 | # Get walking settings (custom or global) 506 | var walking_distance = custom_walking_distance if custom_walking_distance > 0 else battle_manager.walking_distance_threshold 507 | var walking_speed = custom_walking_speed if custom_walking_speed > 0 else battle_manager.walking_speed 508 | var walking_anim = custom_walking_animation if custom_walking_animation != "" else battle_manager.get_animation("walk") # Changed from "run" to "walk" 509 | 510 | var distance_to_target = global_position.distance_to(target.global_position) 511 | if distance_to_target <= walking_distance: 512 | print("Target is close enough, no walking needed") 513 | return false 514 | 515 | # Store original position for return 516 | original_position = global_position # Calculate target position (move closer to target) 517 | var direction = (target.global_position - global_position).normalized() 518 | walk_target_position = target.global_position - direction * walking_distance 519 | 520 | # Start walking animation 521 | set_walking(true) 522 | 523 | # Move towards target 524 | var tween = create_tween() 525 | tween.tween_property(self, "global_position", walk_target_position, 526 | global_position.distance_to(walk_target_position) / walking_speed) 527 | tween.tween_callback(_on_walk_complete) 528 | 529 | return true 530 | 531 | func _try_animation(anim_name: String) -> bool: 532 | # Try to travel to animation, return false if it fails 533 | var current_state = state_machine.get_current_node() 534 | if current_state == anim_name: 535 | return true 536 | 537 | # Try to travel to the animation 538 | state_machine.travel(anim_name) 539 | 540 | # Check if the travel was successful by waiting a frame 541 | await get_tree().process_frame 542 | var new_state = state_machine.get_current_node() 543 | return new_state == anim_name 544 | 545 | func _on_walk_complete(): 546 | set_walking(false) 547 | battle_idle() 548 | 549 | func return_to_original_position(): 550 | if is_walking or original_position == Vector3.ZERO: 551 | return 552 | 553 | var battle_manager = get_tree().get_first_node_in_group("battle_manager") 554 | if not battle_manager: 555 | return 556 | 557 | var walking_speed = custom_walking_speed if custom_walking_speed > 0 else battle_manager.walking_speed 558 | var walking_anim = custom_walking_animation if custom_walking_animation != "" else battle_manager.get_animation("run") 559 | 560 | is_walking = true 561 | if not await _try_animation(walking_anim): 562 | state_machine.travel("idle1") 563 | 564 | var tween = create_tween() 565 | tween.tween_property(self, "global_position", original_position, 566 | global_position.distance_to(original_position) / walking_speed) 567 | tween.tween_callback(_on_return_complete) 568 | 569 | func _on_return_complete(): 570 | is_walking = false 571 | original_position = Vector3.ZERO 572 | battle_idle() 573 | 574 | func get_exp_stat(): 575 | return exp_node 576 | 577 | # # # 578 | # Call methods 579 | # # # 580 | func call_attack(): 581 | print("PLAYER: Animation hit point reached") 582 | anim_damage.emit() 583 | 584 | # # # 585 | # Save System 586 | # # # 587 | func on_save_game(save_data): 588 | var new_data = BattlerData.new() 589 | new_data.current_health = current_health # Using consistent property name 590 | new_data.current_sp = current_sp 591 | new_data.current_exp = get_exp_stat().get_total_exp() 592 | new_data.current_level = get_exp_stat().get_current_level() 593 | new_data.skill_list = skill_list 594 | 595 | save_data["charNameOrID"] = new_data 596 | 597 | func on_load_game(load_data): 598 | var save_data = load_data["charNameOrID"] as BattlerData 599 | if save_data == null: 600 | print("Battler data is empty.") 601 | return 602 | 603 | current_health = save_data.current_health # Using consistent property name 604 | current_sp = save_data.current_sp 605 | get_exp_stat().exp_total = save_data.current_exp 606 | get_exp_stat().char_level = save_data.current_level 607 | skill_node.character_skills = save_data.skill_list 608 | 609 | func regenerate_sp(): 610 | if stats and current_sp < max_sp: 611 | var regen_amount = 5 # Default SP regeneration 612 | if stats.has_method("get") and stats.get("sp_regen") != null: 613 | regen_amount = stats.sp_regen 614 | current_sp = min(current_sp + regen_amount, max_sp) 615 | print("%s recovered %d SP. SP: %d/%d" % [character_name, regen_amount, current_sp, max_sp]) 616 | 617 | func can_target_with_skill(skill: Skill, target: Battler) -> bool: 618 | print("=== CAN TARGET WITH SKILL DEBUG ===") 619 | print("Skill: ", skill.skill_name) 620 | print("Target: ", target.character_name) 621 | print("Target team: ", target.team) 622 | print("Self team: ", team) 623 | 624 | if !skill or !target: 625 | print("Skill or target is null!") 626 | return false 627 | 628 | # Check if skill can be used (costs) 629 | if !skill.can_use(self): 630 | print("Skill cannot be used due to costs!") 631 | return false 632 | 633 | # Check target type compatibility 634 | match skill.target_type: 635 | Skill.TARGETS_TYPES.SELF_TARGET: 636 | var result = target == self 637 | print("SELF_TARGET check: ", result) 638 | return result 639 | Skill.TARGETS_TYPES.SINGLE_ENEMY: 640 | var result = target.team == TEAM.ENEMY and target != self 641 | print("SINGLE_ENEMY check: ", result) 642 | return result 643 | Skill.TARGETS_TYPES.MULTIPLE_ENEMIES: 644 | var result = target.team == TEAM.ENEMY and target != self 645 | print("MULTIPLE_ENEMIES check: ", result) 646 | return result 647 | Skill.TARGETS_TYPES.SINGLE_ALLY: 648 | var result = target.team == TEAM.ALLY and target != self 649 | print("SINGLE_ALLY check: ", result) 650 | return result 651 | Skill.TARGETS_TYPES.MULTIPLE_ALLIES: 652 | var result = target.team == TEAM.ALLY and target != self 653 | print("MULTIPLE_ALLIES check: ", result) 654 | return result 655 | Skill.TARGETS_TYPES.ALL_TARGETS: 656 | var result = target != self 657 | print("ALL_TARGETS check: ", result) 658 | return result 659 | _: 660 | print("Unknown target type!") 661 | return false 662 | 663 | var active_states: Dictionary = {} # {state_name: State} 664 | 665 | # And add these helper functions for state management 666 | func apply_state(state: State) -> void: 667 | if state == null: 668 | return 669 | active_states[state.state_name] = state.duplicate() 670 | print("[STATE] %s was afflicted with %s!" % [character_name, state.state_name]) 671 | 672 | func remove_state(state_name: String) -> void: 673 | if active_states.has(state_name): 674 | active_states.erase(state_name) 675 | print("[STATE] %s is no longer affected by %s" % [character_name, state_name]) 676 | 677 | func process_states() -> void: 678 | var states_to_remove = [] 679 | 680 | for state_name in active_states: 681 | var state = active_states[state_name] 682 | 683 | # Handle DOT/HOT effects 684 | if state.damage_per_turn != 0: 685 | take_damage(state.damage_per_turn) 686 | 687 | # Handle duration 688 | if state.turns_active > 0: 689 | state.turns_active -= 1 690 | if state.turns_active <= 0: 691 | states_to_remove.append(state_name) 692 | 693 | # Remove expired states 694 | for state_name in states_to_remove: 695 | remove_state(state_name) 696 | 697 | func set_walking(value: bool): 698 | is_walking = value 699 | anim_tree.set("parameters/conditions/is_walking", value) 700 | 701 | func set_defending(value: bool): 702 | is_defending = value 703 | anim_tree.set("parameters/conditions/is_defending", value) 704 | -------------------------------------------------------------------------------- /battle-manager/battlemanager.gd: -------------------------------------------------------------------------------- 1 | class_name BattleManager 2 | extends Node3D 3 | 4 | enum BattleEndCondition { WIN, DEFEAT, ESCAPE } 5 | 6 | var skip_turn = false # Please please please, Change the functionality or entirely remove this down the line. 7 | var players: Array[Battler] = [] 8 | var enemies: Array[Battler] = [] 9 | var turn_order: Array[Battler] = [] 10 | var current_turn: int = 0 11 | 12 | var current_character:Battler 13 | var current_target:Battler 14 | var in_target_selection:bool = false 15 | var in_menu_selection:bool = false 16 | 17 | @onready var ActionButtons = find_child("ActionButtons") 18 | # For referencing and setting variables in the battle settings. 19 | @onready var battle_settings = GlobalBattleSettings 20 | 21 | var current_battler 22 | # Defualt animation should check for weapon's later down the road, And adapt to using them with unique animations. 23 | @export var default_animation = "Locomotion-Library/idle2" # Unused, But i reccomend gettomg the stats and animation from the database. 24 | @export var default_attack:Skill 25 | # Added by repo owner, Fame. To test compatibility with returning after a battle. 26 | @export var game_map = "res://replace/regular_map/backtogame.tscn" 27 | @onready var hud: BattleHud = $BattleHUD 28 | 29 | # Toggles For Battles 30 | @export_group("Toggle Buttons") 31 | @export var attack_toggle: bool = true 32 | @export var skills_toggle: bool = true 33 | @export var defend_toggle: bool = true 34 | @export var item_toggle: bool = true 35 | @export var run_toggle: bool = true 36 | @export var mouse_input_toggle: bool = true 37 | 38 | # Walking Animation System 39 | @export_group("Walking Animation System", "walking") 40 | @export var enable_walking_to_target: bool = true 41 | @export var walking_distance_threshold: float = 2.0 42 | @export var walking_speed: float = 3.0 43 | #@export var walking_animation_name: String = "walk" 44 | ## Global walking settings. Individual battlers can override these with custom values. 45 | ## If a battler has custom_walking_distance > 0, it will use that instead of walking_distance_threshold. 46 | ## Same applies to custom_walking_speed and custom_walking_animation. 47 | 48 | # Animation Dictionary System 49 | @export_group("Animation Dictionary", "animations") 50 | @export var animation_dictionary: Dictionary = { 51 | "run": "run", # Changed from "run" to "walk" 52 | "turn_left": "Locomotion-Library/turn left", 53 | "turn_right": "Locomotion-Library/turn right", 54 | "attack": "Locomotion-Library/attack1", 55 | "idle": "Locomotion-Library/idle2", 56 | "defend": "armature_stand" 57 | } 58 | ## Dictionary of animation names. Keys are animation types, values are actual animation names. 59 | ## This allows customization without assuming specific animation library names. 60 | 61 | # Rotation and Battle Settings 62 | @export_group("Battle Settings", "battle") 63 | @export var invert_rotation_axis: bool = true 64 | @export var remove_defeated_enemies: bool = true 65 | @export var instant_turning: bool = false 66 | ## Invert rotation axis for characters with unusual pre-existing rotations. 67 | ## Remove defeated enemies entirely from the battle scene. 68 | ## Instant turning skips turn animations for immediate rotation. 69 | 70 | # Troop System 71 | @export_group("Troop System", "troop") 72 | @export var active_troop: Troops = null 73 | @export var enemy_markers: Node3D = null # Reference to parent node containing Marker3D nodes for formation positions 74 | @export var use_procedural_formation: bool = true 75 | ## Active troop to spawn. If set, will override manual enemy placement. 76 | ## Reference to the parent node containing Marker3D children for custom formations. 77 | ## If true, positions will be calculated procedurally based on formation type. 78 | 79 | # Helper function to get animation name from dictionary 80 | func get_animation(animation_type: String) -> String: 81 | if animation_dictionary.has(animation_type): 82 | return animation_dictionary[animation_type] 83 | else: 84 | print("Warning: Animation type '", animation_type, "' not found in dictionary, using idle1") 85 | return "idle1" 86 | 87 | # Helper function to calculate enemy positions based on formation 88 | func get_formation_position(enemy_index: int, total_enemies: int, formation: int, base_marker: Marker3D = null) -> Vector3: 89 | var base_position = Vector3.ZERO 90 | var base_y = 0.261 # Default Y position (matches ally height) 91 | var base_x = 0.0 92 | var base_z = 0.0 93 | var use_marker = base_marker != null 94 | 95 | # Use base marker's position if provided 96 | if use_marker: 97 | base_x = base_marker.global_position.x 98 | base_z = base_marker.global_position.z 99 | base_y = base_marker.global_position.y 100 | 101 | match formation: 102 | Troops.Formation.FRONT_ROW: 103 | # Horizontal line formation (centered on marker or default) 104 | var spacing = 2.0 105 | var start_x = -(total_enemies - 1) * spacing / 2.0 106 | var offset_z = 0.0 if use_marker else 5.0 107 | return Vector3(base_x + start_x + enemy_index * spacing, base_y, base_z + offset_z) 108 | 109 | Troops.Formation.TRIANGLE: 110 | # Triangle formation - rows get wider 111 | var row = int(sqrt(enemy_index)) 112 | var pos_in_row = enemy_index - (row * (row + 1) / 2) 113 | var spacing = 2.0 114 | var row_width = (row + 1) * spacing 115 | var start_x = -row_width / 2.0 116 | var z_offset = (0.0 if use_marker else 5.0) - (row * 1.5) 117 | return Vector3(base_x + start_x + pos_in_row * spacing, base_y, base_z + z_offset) 118 | 119 | Troops.Formation.CIRCLE_PLAYER: 120 | # Circle formation around marker or player (at origin) 121 | var angle = (TAU / total_enemies) * enemy_index 122 | var radius = 5.0 123 | var center_z = base_z + (0.0 if use_marker else 5.0) 124 | return Vector3(base_x + cos(angle) * radius, base_y, center_z + sin(angle) * radius) 125 | 126 | Troops.Formation.CUSTOM_MARKERS: 127 | # Will use marker positions instead 128 | return base_position 129 | 130 | _: 131 | return base_position 132 | 133 | # Spawn enemies from troop data 134 | func spawn_troop(troop: Troops, parent_node: Node3D = self) -> void: 135 | if not troop or not troop.enemy_group or troop.enemy_group.enemy_scenes.is_empty(): 136 | push_error("Invalid troop or no enemy group/scenes defined!") 137 | return 138 | 139 | print("Spawning troop: ", troop.troop_name) 140 | print("Enemy scenes: ", troop.enemy_group.enemy_scenes.size()) 141 | 142 | # Try to find FRONTROW marker for base positioning 143 | var front_row_marker: Marker3D = null 144 | for node in get_tree().get_nodes_in_group("FRONTROW"): 145 | if node is Marker3D: 146 | front_row_marker = node 147 | break 148 | 149 | if not front_row_marker: 150 | print("FRONTROW marker not found, using default center position") 151 | else: 152 | print("Using FRONTROW marker at position: ", front_row_marker.global_position) 153 | 154 | var loaded_scenes = troop.get_enemy_scenes() 155 | var marker_index = 0 156 | 157 | for i in range(loaded_scenes.size()): 158 | var enemy_scene = loaded_scenes[i] 159 | if not enemy_scene: 160 | push_warning("Failed to load enemy scene at index ", i) 161 | continue 162 | 163 | var enemy = enemy_scene.instantiate() 164 | if not enemy is Battler: 165 | push_warning("Enemy scene is not a Battler at index ", i, " in group: ", troop.enemy_group.group_name) 166 | enemy.queue_free() 167 | continue 168 | 169 | # Calculate position based on formation 170 | var spawn_position: Vector3 171 | 172 | if troop.formation == Troops.Formation.CUSTOM_MARKERS: 173 | # Use Marker3D positions if available 174 | if enemy_markers and marker_index < enemy_markers.get_child_count(): 175 | spawn_position = enemy_markers.get_child(marker_index).global_position 176 | marker_index += 1 177 | else: 178 | spawn_position = get_formation_position(i, loaded_scenes.size(), Troops.Formation.FRONT_ROW, front_row_marker) 179 | push_warning("Custom marker formation requested but no markers found, using front row fallback") 180 | else: 181 | spawn_position = get_formation_position(i, loaded_scenes.size(), troop.formation, front_row_marker) 182 | 183 | # Set enemy position 184 | enemy.global_position = spawn_position 185 | 186 | # Face enemy toward player (negative Z direction) 187 | # Calculate angle to face player at origin 188 | var direction_to_player = Vector3.ZERO - spawn_position 189 | enemy.rotation.y = atan2(direction_to_player.x, direction_to_player.z) 190 | 191 | # Add to scene 192 | parent_node.add_child(enemy) 193 | 194 | # Add to enemies group 195 | enemy.add_to_group("enemies") 196 | 197 | # Apply troop-wide damage reduction if applicable 198 | if troop.damage_reduction != 1.0: 199 | enemy.damage_multiplier = troop.damage_reduction 200 | 201 | print("Spawned enemy at index ", i, ": ", enemy.character_name, " at position ", spawn_position) 202 | 203 | # Spawn additional enemies during battle (for troop-to-troop battles that continue) 204 | func spawn_reinforcements(troop: Troops, parent_node: Node3D = self) -> void: 205 | if not troop or not troop.enemy_group or troop.enemy_group.enemy_scenes.is_empty(): 206 | push_error("Invalid troop or no enemy group/scenes defined!") 207 | return 208 | 209 | print("Spawning reinforcements: ", troop.troop_name) 210 | 211 | # Try to find FRONTROW marker for base positioning 212 | var front_row_marker: Marker3D = null 213 | for node in get_tree().get_nodes_in_group("FRONTROW"): 214 | if node is Marker3D: 215 | front_row_marker = node 216 | break 217 | 218 | var loaded_scenes = troop.get_enemy_scenes() 219 | var current_enemy_count = enemies.size() 220 | 221 | for i in range(loaded_scenes.size()): 222 | var enemy_scene = loaded_scenes[i] 223 | if not enemy_scene: 224 | push_warning("Failed to load enemy scene at index ", i) 225 | continue 226 | 227 | var enemy = enemy_scene.instantiate() 228 | if not enemy is Battler: 229 | push_warning("Enemy scene is not a Battler at index ", i, " in group: ", troop.enemy_group.group_name) 230 | enemy.queue_free() 231 | continue 232 | 233 | # Calculate position based on formation 234 | var spawn_position = get_formation_position(current_enemy_count + i, current_enemy_count + loaded_scenes.size(), troop.formation, front_row_marker) 235 | 236 | # Set enemy position 237 | enemy.global_position = spawn_position 238 | 239 | # Face enemy toward player (negative Z direction) 240 | # Calculate angle to face player at origin 241 | var direction_to_player = Vector3.ZERO - spawn_position 242 | enemy.rotation.y = atan2(direction_to_player.x, direction_to_player.z) 243 | 244 | # Add to scene 245 | parent_node.add_child(enemy) 246 | 247 | # Add to enemies group 248 | enemy.add_to_group("enemies") 249 | 250 | # Add to enemies array 251 | enemies.append(enemy) 252 | 253 | # Add to turn order 254 | turn_order.append(enemy) 255 | 256 | # Apply troop-wide damage reduction if applicable 257 | if troop.damage_reduction != 1.0: 258 | enemy.damage_multiplier = troop.damage_reduction 259 | 260 | # Connect damage signal 261 | if not enemy.anim_damage.is_connected(_on_anim_damage): 262 | enemy.anim_damage.connect(_on_anim_damage) 263 | 264 | # Set battle idle state 265 | enemy.battle_idle() 266 | hud.on_start_combat(enemy) 267 | 268 | print("Spawned reinforcement: ", enemy.character_name, " at position ", spawn_position) 269 | 270 | # Helper function to clean up defeated enemies 271 | func cleanup_defeated_enemies(): 272 | if not remove_defeated_enemies: 273 | return 274 | 275 | # Remove defeated enemies from arrays and scene 276 | var enemies_to_remove = [] 277 | for enemy in enemies: 278 | if enemy.is_defeated(): 279 | enemies_to_remove.append(enemy) 280 | 281 | for enemy in enemies_to_remove: 282 | print("Cleaning up defeated enemy: ", enemy.character_name) 283 | # Remove from turn order 284 | if turn_order.has(enemy): 285 | turn_order.erase(enemy) 286 | # Remove from enemies array 287 | enemies.erase(enemy) 288 | # Remove from scene 289 | enemy.queue_free() 290 | 291 | var is_animating: bool = false 292 | 293 | func _ready(): 294 | add_to_group("battle_manager") 295 | SignalBus.select_target.connect(target_selected) 296 | 297 | if not hud: 298 | push_error("BattleHUD node not found. Please make sure it's added to the scene.") 299 | return 300 | hud.action_selected.connect(_on_action_selected) 301 | hud.menu_opened.connect(_do_menu_selection) 302 | 303 | # Spawn troop if active_troop is set 304 | if active_troop: 305 | print("Active troop detected: ", active_troop.troop_name) 306 | spawn_troop(active_troop, self) 307 | 308 | for player in players: 309 | if not player.anim_damage.is_connected(_on_anim_damage): 310 | player.anim_damage.connect(_on_anim_damage) 311 | initialize_battle() 312 | # Checking Toggles! 313 | ActionButtons.get_node("Attack").disabled = not attack_toggle 314 | ActionButtons.get_node("Skills").disabled = not skills_toggle 315 | ActionButtons.get_node("Defend").disabled = not defend_toggle 316 | ActionButtons.get_node("Items").disabled = not item_toggle 317 | ActionButtons.get_node("Run").disabled = not run_toggle 318 | 319 | func _input(event: InputEvent) -> void: 320 | # Cancel is currently bound to Escape key 321 | if event.is_action_pressed("Cancel"): 322 | print("Pressed cancel") 323 | if in_target_selection: 324 | print("Cancelling Target Selection") 325 | _cancel_action_target_selection() 326 | elif in_menu_selection: 327 | print("Cancelling Menu Selection") 328 | _cancel_menu_selection() 329 | # Confirm is currently bound to Enter key 330 | elif event.is_action_pressed("Confirm") and in_target_selection and current_target: 331 | if !queued_action.is_empty() and (queued_skill or queued_item): 332 | _use_action_on_target() 333 | else: 334 | printerr("MANAGER: Action and/or Skill/Item not queued!") 335 | print("Action: ", queued_action) 336 | print("Skill: ", queued_skill) 337 | print("Item: ", queued_item) 338 | 339 | func initialize_battle(): 340 | # Get all nodes and convert to Battler arrays 341 | players = [] 342 | enemies = [] 343 | 344 | for node in get_tree().get_nodes_in_group("players"): 345 | if node is Battler: 346 | players.append(node as Battler) 347 | 348 | for node in get_tree().get_nodes_in_group("enemies"): 349 | if node is Battler: 350 | enemies.append(node as Battler) 351 | 352 | for player in players: 353 | print("Have players: ", players) 354 | hud.on_add_character(player) 355 | player.battle_idle() 356 | player.anim_damage.connect(_on_anim_damage) 357 | 358 | # Ensure players are at the start of the turn order 359 | turn_order = players + enemies 360 | print("Current turn order: ", turn_order) 361 | 362 | for enemy in enemies: 363 | hud.on_start_combat(enemy) 364 | enemy.battle_idle() 365 | enemy.anim_damage.connect(_on_anim_damage) 366 | 367 | start_next_turn() 368 | 369 | func count_allies(): 370 | # For now, Should be one. 371 | battle_settings.ally_party = 1 372 | 373 | # See _ready() -> SignalBus.select_target.connect() ... Emitted from Battler 374 | func target_selected(target: Battler) -> void: 375 | print("=== BATTLE MANAGER TARGET SELECTED ===") 376 | print("Received target: ", target.character_name) 377 | print("In target selection: ", in_target_selection) 378 | print("Target is selectable: ", target.is_selectable) 379 | 380 | if !in_target_selection or !target.is_selectable: 381 | print("Target selection rejected - not in selection or not selectable") 382 | return 383 | 384 | # Clear ALL targets' selection states first - only ONE highlight allowed 385 | for battler in valid_targets: 386 | if battler is Battler: 387 | battler.deselect_as_target() 388 | 389 | # Clear controller target reference 390 | current_controller_target = null 391 | 392 | # Save this as the last selected target for future reference 393 | last_selected_target = target 394 | 395 | # Update keyboard index to match mouse selection 396 | if target in valid_targets: 397 | keyboard_target_index = valid_targets.find(target) 398 | 399 | current_target = target 400 | print("Set current_target to: ", current_target.character_name) 401 | print("About to call _use_action_on_target") 402 | print("=== VISUAL CONFIRMATION ===") 403 | print("Target with cyan highlight should be: ", target.character_name) 404 | print("Target that will be attacked: ", current_target.character_name) 405 | _use_action_on_target() 406 | 407 | func start_next_turn(): 408 | # Check if battle is over before proceeding 409 | if is_battle_over(): 410 | if all_defeated(players): 411 | end_battle(BattleEndCondition.DEFEAT) 412 | elif all_defeated(enemies): 413 | end_battle(BattleEndCondition.WIN) 414 | else: 415 | #Fallthrough 416 | end_battle() 417 | return 418 | 419 | # Ensure we have valid characters in turn order 420 | if turn_order.is_empty(): 421 | print("No characters in turn order, ending battle") 422 | end_battle() 423 | return 424 | 425 | current_character = turn_order[current_turn] 426 | current_battler = current_character 427 | 428 | if current_character.is_defeated(): 429 | turn_order.erase(current_character) 430 | current_turn = current_turn % turn_order.size() 431 | start_next_turn() 432 | return 433 | 434 | if current_character in players: 435 | print("Player's turn") 436 | player_turn(current_character) 437 | else: 438 | print("Enemy's turn") 439 | enemy_turn(current_character) 440 | 441 | update_hud() 442 | 443 | func player_turn(character): 444 | count_allies() 445 | hud.set_activebattler(character) 446 | hud.show_action_buttons(character) 447 | 448 | var queued_action:String 449 | var queued_skill:Skill 450 | var queued_item:Item 451 | func _on_action_selected(action: String, usable:Resource = current_character.default_attack): 452 | print("=== ACTION SELECTED ===") 453 | print("Action: ", action) 454 | print("Usable: ", usable) 455 | print("Current character: ", current_character.character_name) 456 | 457 | match action: 458 | "defend": 459 | current_character.defend() 460 | end_turn() 461 | return 462 | "run": 463 | escape_battle() 464 | return 465 | 466 | queued_action = action 467 | if action == "attack": 468 | if current_character.default_attack: 469 | queued_skill = current_character.default_attack 470 | else: 471 | queued_skill = default_attack 472 | if usable is Skill: 473 | queued_skill = usable 474 | print("Set queued_skill to: ", queued_skill.skill_name) 475 | elif usable is Item: 476 | queued_item = usable 477 | _do_target_selection() 478 | 479 | var current_controller_target: Battler = null 480 | var valid_targets: Array = [] # Array of Battler objects 481 | var current_default_selector: Battler = null # Track who has the default selection 482 | var last_selected_target: Battler = null # Remember last target for next selection 483 | var keyboard_target_index: int = 0 # Track keyboard navigation position 484 | 485 | # Modify _do_target_selection to set target validity 486 | func _do_target_selection() -> void: 487 | print("\n=== Starting Target Selection ===") 488 | print("DEBUG: enemies array before target selection: ", enemies) 489 | print("DEBUG: players array before target selection: ", players) 490 | in_target_selection = true 491 | current_target = null 492 | valid_targets.clear() 493 | 494 | # First, clear all targeting states 495 | for battler in get_tree().get_nodes_in_group("players") + get_tree().get_nodes_in_group("enemies"): 496 | if battler is Battler: 497 | battler.clear_all_selections() 498 | battler.is_valid_target = false 499 | battler.is_selectable = false 500 | 501 | # Get fresh data from scene to avoid array corruption issues 502 | var current_enemies: Array = [] 503 | var current_players: Array = [] 504 | 505 | for node in get_tree().get_nodes_in_group("enemies"): 506 | if node is Battler: 507 | current_enemies.append(node as Battler) 508 | 509 | for node in get_tree().get_nodes_in_group("players"): 510 | if node is Battler: 511 | current_players.append(node as Battler) 512 | 513 | # Set valid targets based on skill type with proper validation 514 | if queued_skill: 515 | print("=== SKILL TARGETING ===") 516 | print("Skill name: ", queued_skill.skill_name) 517 | print("Skill target type: ", queued_skill.target_type) 518 | print("Current character in players: ", current_character in current_players) 519 | print("Current enemies: ", current_enemies) 520 | print("Current players: ", current_players) 521 | 522 | # Check if skill can be used at all 523 | print("Checking if skill can be used...") 524 | print("Current character SP: ", current_character.current_sp) 525 | print("Current character HP: ", current_character.current_health) 526 | print("Skill SP cost: ", queued_skill.sp_cost) 527 | print("Skill HP cost: ", queued_skill.hp_cost) 528 | 529 | if !queued_skill.can_use(current_character): 530 | print("Cannot use skill - insufficient SP/HP!") 531 | _cancel_action_target_selection() 532 | return 533 | 534 | print("Skill can be used! Proceeding with targeting...") 535 | 536 | match queued_skill.target_type: 537 | Skill.TARGETS_TYPES.SELF_TARGET: 538 | valid_targets = [current_character] 539 | Skill.TARGETS_TYPES.SINGLE_ENEMY: 540 | valid_targets = current_enemies if current_character in current_players else current_players 541 | Skill.TARGETS_TYPES.MULTIPLE_ENEMIES: 542 | valid_targets = current_enemies if current_character in current_players else current_players 543 | _auto_select_multiple_targets() 544 | return 545 | Skill.TARGETS_TYPES.SINGLE_ALLY: 546 | valid_targets = current_players if current_character in current_players else current_enemies 547 | if current_character in valid_targets: # Remove self from valid targets 548 | valid_targets.erase(current_character) 549 | Skill.TARGETS_TYPES.MULTIPLE_ALLIES: 550 | valid_targets = current_players if current_character in current_players else current_enemies 551 | _auto_select_multiple_targets() 552 | return 553 | Skill.TARGETS_TYPES.ALL_TARGETS: 554 | valid_targets = current_players + current_enemies 555 | _auto_select_multiple_targets() 556 | return 557 | 558 | # Filter valid targets based on skill requirements 559 | print("Filtering valid targets based on skill requirements...") 560 | var filtered_targets: Array = [] 561 | for target in valid_targets: 562 | if target is Battler: 563 | var can_target = current_character.can_target_with_skill(queued_skill, target) 564 | print("Can target ", target.character_name, ": ", can_target) 565 | if can_target: 566 | filtered_targets.append(target) 567 | print("Filtered targets count: ", filtered_targets.size()) 568 | valid_targets = filtered_targets 569 | 570 | # Check if we have any valid targets 571 | if valid_targets.is_empty(): 572 | print("No valid targets found for this skill!") 573 | _cancel_action_target_selection() 574 | return 575 | 576 | # Enable only valid targets 577 | print("Valid targets found: ", valid_targets.size()) 578 | for target in valid_targets: 579 | if target is Battler: 580 | target.is_valid_target = true 581 | target.is_selectable = true 582 | print("Enabled target: ", target.character_name) 583 | print(" - Is valid target: ", target.is_valid_target) 584 | print(" - Is selectable: ", target.is_selectable) 585 | 586 | # Set initial controller target - prioritize last selected target 587 | if valid_targets.size() > 0: 588 | var initial_target = valid_targets[0] 589 | 590 | # If we have a last selected target and it's still valid, use it 591 | if last_selected_target and last_selected_target in valid_targets: 592 | initial_target = last_selected_target 593 | keyboard_target_index = valid_targets.find(last_selected_target) 594 | else: 595 | keyboard_target_index = 0 596 | 597 | # Clear all targets first, then set ONLY the default target 598 | for battler in valid_targets: 599 | if battler is Battler: 600 | battler.deselect_as_target() 601 | 602 | current_controller_target = initial_target 603 | current_default_selector = current_controller_target 604 | current_controller_target.set_as_default_target() 605 | 606 | print("Default target set to: ", current_controller_target.character_name) 607 | 608 | SignalBus.allow_select_target.emit(true) 609 | print("=== Target Selection Complete ===\n") 610 | 611 | # Helper function for multiple target selection 612 | func _auto_select_multiple_targets() -> void: 613 | for target in valid_targets: 614 | if target is Battler: 615 | target.is_valid_target = true 616 | target.is_selectable = true 617 | target.is_targeted = true 618 | if valid_targets.size() > 0 and valid_targets[0] is Battler: 619 | current_target = valid_targets[0] 620 | _use_action_on_target() 621 | 622 | # Enhanced controller input handling with better visual feedback 623 | func _unhandled_input(event: InputEvent) -> void: 624 | if !in_target_selection or valid_targets.is_empty(): 625 | return 626 | 627 | if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_left"): 628 | _cycle_controller_target(1 if event.is_action_pressed("ui_right") else -1) 629 | elif event.is_action_pressed("ui_accept"): 630 | if current_controller_target: 631 | current_target = current_controller_target 632 | _use_action_on_target() 633 | elif event.is_action_pressed("ui_cancel"): 634 | _cancel_action_target_selection() 635 | 636 | func _cycle_controller_target(direction: int) -> void: 637 | # Clear ALL targets' selection states first - only ONE highlight allowed 638 | for battler in valid_targets: 639 | if battler is Battler: 640 | battler.deselect_as_target() 641 | 642 | # Calculate new index with proper wrapping 643 | keyboard_target_index += direction 644 | if keyboard_target_index >= valid_targets.size(): 645 | keyboard_target_index = 0 646 | elif keyboard_target_index < 0: 647 | keyboard_target_index = valid_targets.size() - 1 648 | 649 | # Set new keyboard target 650 | if valid_targets.size() > 0 and valid_targets[keyboard_target_index] is Battler: 651 | current_controller_target = valid_targets[keyboard_target_index] 652 | current_default_selector = current_controller_target 653 | current_controller_target.set_as_keyboard_target() 654 | 655 | print("Keyboard navigation: Selected ", current_controller_target.character_name) 656 | print("Current keyboard index: ", keyboard_target_index) 657 | print("Valid targets size: ", valid_targets.size()) 658 | 659 | func _cancel_action_target_selection() -> void: 660 | print("Cancelling target selection") 661 | in_target_selection = false 662 | current_target = null 663 | current_controller_target = null 664 | current_default_selector = null 665 | keyboard_target_index = 0 666 | valid_targets.clear() 667 | 668 | # Clear queued action variables 669 | queued_action = "" 670 | queued_skill = null 671 | queued_item = null 672 | 673 | # Clear all targeting states for all battlers 674 | for battler in get_tree().get_nodes_in_group("players") + get_tree().get_nodes_in_group("enemies"): 675 | if battler is Battler: 676 | battler.clear_all_selections() 677 | # Reset to default selectable state 678 | battler.is_valid_target = true 679 | battler.is_selectable = true 680 | 681 | if in_menu_selection: 682 | _do_menu_selection() 683 | else: 684 | hud.show_action_buttons(current_character) 685 | SignalBus.allow_select_target.emit(false) 686 | 687 | func _cancel_menu_selection() -> void: 688 | in_menu_selection = false 689 | hud.item_select.hide() 690 | hud.skill_select.hide() 691 | hud.show_action_buttons(current_character) 692 | 693 | func _do_menu_selection() -> void: 694 | in_menu_selection = true 695 | hud.hide_action_buttons() 696 | 697 | # Modify _use_action_on_target to handle states properly 698 | func _use_action_on_target() -> void: 699 | print("=== USING ACTION ON TARGET ===") 700 | print("Current target: ", current_target.character_name if current_target else "NULL") 701 | print("Queued action: ", queued_action) 702 | print("Queued skill: ", queued_skill.skill_name if queued_skill else "NULL") 703 | 704 | in_target_selection = false 705 | in_menu_selection = false 706 | is_animating = true 707 | hud.hide_action_buttons() 708 | 709 | match queued_action: 710 | "skill": 711 | if queued_skill: 712 | if queued_skill.effect_type == Skill.EFFECT_TYPE.BUFF: 713 | # For buff skills, just apply the state without damage 714 | if queued_skill.applies_state: 715 | current_target.apply_state(queued_skill.applies_state) 716 | print("Applied %s state to %s" % [queued_skill.applies_state.state_name, current_target.character_name]) 717 | else: 718 | # Normal skill handling with damage 719 | battler_attacking = true 720 | print("Using skill on target: ", current_target.character_name) 721 | current_character.use_skill(queued_skill, current_target) 722 | "attack": 723 | battler_attacking = true 724 | print("Performing attack on target: ", current_target.character_name) 725 | if current_character.default_attack: 726 | current_character.use_skill(current_character.default_attack, current_target) 727 | else: 728 | current_character.attack_anim(current_target) 729 | "item": 730 | print("Using item on target: ", current_target.character_name) 731 | current_character.battle_item(queued_item, current_target) 732 | 733 | queued_action = "" 734 | queued_skill = null 735 | queued_item = null 736 | SignalBus.allow_select_target.emit(false) 737 | end_turn() 738 | 739 | func _on_anim_damage(): 740 | print("=== ANIMATION DAMAGE DEBUG ===") 741 | print("Current character: ", current_character.character_name if current_character else "NULL") 742 | print("Current target: ", current_target.character_name if current_target else "NULL") 743 | print("MANAGER: Processing animation damage") 744 | if current_character and current_target: 745 | var damage = current_character.get_attack_damage(current_target) # Get damage for current target 746 | print("Calculated damage: ", damage, " for target: ", current_target.character_name) 747 | damage_calculation(current_character, current_target, damage) 748 | 749 | func process_exp_gain(user, target): 750 | if not target: 751 | return 752 | var exp_gained = target.get_exp_stat().get_exp_on_kill() 753 | user.get_exp_stat().add_exp(exp_gained) 754 | user.gain_experience(exp_gained) 755 | 756 | # Updating this just to follow pattern being used in battler_enemy 757 | func perform_attack(attacker:Battler, target:Battler): 758 | current_target = target 759 | if attacker.default_attack: 760 | attacker.use_skill(attacker.default_attack, target) 761 | else: 762 | attacker.attack_anim(target) 763 | 764 | func perform_defend(character): 765 | character.defend() 766 | print("%s is defending!" % character.character_name) 767 | 768 | # Don't forget that game's also allow skill's to heal players, So use a universal term and if statements. 769 | func perform_skill(user:Battler, target:Battler, skill:Skill) -> void: 770 | if not skill.can_use(user): 771 | print("Not enough SP/HP to use skill!") 772 | return 773 | 774 | user.use_skill(skill, target) 775 | update_hud() 776 | 777 | func perform_item(user): 778 | var amount = user.skill_heal() 779 | heal_calculation(user, user, amount) 780 | 781 | # Loving the additions of formula's! 782 | func damage_calculation(attacker, target, damage): 783 | damage = Formulas.physical_damage(attacker, target, damage) 784 | print("%s attacks %s for %d damage!" % [attacker.character_name, target.character_name, damage]) 785 | target.take_damage(damage) 786 | hud.update_health_bars() 787 | update_hud() 788 | 789 | func heal_calculation(user, target, amount): 790 | var healing = target.take_healing(amount) 791 | print("%s heals %s for %d health!" % [user.character_name, target.character_name, healing]) 792 | hud.update_health_bars() # Add this line 793 | update_hud() 794 | 795 | func enemy_turn(character:Battler) -> void: 796 | print("=== ENEMY TURN ===") 797 | print("Enemy character: ", character.character_name) 798 | 799 | # Get all players (allies) as targets for enemies 800 | var available_targets: Array = [] 801 | for node in get_tree().get_nodes_in_group("players"): 802 | if node is Battler and !node.is_defeated(): 803 | available_targets.append(node) 804 | 805 | print("Available targets for enemy: ", available_targets.size()) 806 | for target in available_targets: 807 | if target is Battler: 808 | print(" - ", target.character_name, " (HP: ", target.current_health, "/", target.max_health, ")") 809 | 810 | var all_enemies = get_tree().get_nodes_in_group("enemies") 811 | 812 | AIManager.choose_action(character, available_targets, all_enemies, self) 813 | update_hud() 814 | end_turn() 815 | 816 | var battler_attacking:bool = false 817 | func end_turn(): 818 | if skip_turn: 819 | current_turn = (current_turn + 1) % turn_order.size() 820 | is_animating = false 821 | start_next_turn() 822 | else: 823 | if battler_attacking: 824 | print("DEBUG: Waiting for attack animation to complete...") 825 | await current_battler.wait_attack() 826 | print("DEBUG: Attack animation completed, battler_attacking set to false") 827 | battler_attacking = false 828 | 829 | # Process states before SP regen 830 | current_battler.process_states() 831 | current_battler.regenerate_sp() 832 | 833 | # Clean up defeated enemies 834 | cleanup_defeated_enemies() 835 | 836 | # Clear targeting states 837 | print("DEBUG: enemies array before end_turn clear: ", enemies) 838 | for battler in get_tree().get_nodes_in_group("players") + get_tree().get_nodes_in_group("enemies"): 839 | battler.is_valid_target = false 840 | battler.is_selectable = false 841 | battler.is_targeted = false 842 | battler.is_default_target = false 843 | battler.mouse_hover = false 844 | print("DEBUG: enemies array after end_turn clear: ", enemies) 845 | 846 | current_turn = (current_turn + 1) % turn_order.size() 847 | is_animating = false 848 | print("DEBUG: Starting next turn...") 849 | start_next_turn() 850 | 851 | func update_hud(): 852 | hud.update_character_info() 853 | if turn_order[current_turn] in players and not is_animating: 854 | hud.show_action_buttons(turn_order[current_turn]) 855 | else: 856 | hud.hide_action_buttons() 857 | 858 | func is_battle_over(): 859 | return all_defeated(players) or all_defeated(enemies) 860 | 861 | func all_defeated(characters: Array): 862 | for character in characters: 863 | if not character.is_defeated(): 864 | return false 865 | return true 866 | 867 | func escape_battle(): 868 | var base_escape_chance = 70 869 | 870 | # Reduce chance by 10% for each additional ally 871 | var ally_penalty = (battle_settings.ally_party - 1) * 10 872 | 873 | # Calculate final threshold (base - penalties + difficulty mod) 874 | var escape_threshold = base_escape_chance - ally_penalty 875 | 876 | # Generate random number 1-100 877 | var roll = randi_range(1, 100) 878 | 879 | # Check if escape successful 880 | if roll <= escape_threshold: 881 | print("Escape successful! (Rolled %d, needed %d or less)" % [roll, escape_threshold]) 882 | end_battle(BattleEndCondition.ESCAPE) # Use your existing escape scene transition 883 | return true 884 | else: 885 | print("Escape failed! (Rolled %d, needed %d or less)" % [roll, escape_threshold]) 886 | skip_turn = true 887 | end_turn() # Enemy gets a turn after failed escape 888 | return false 889 | 890 | func end_battle(state: BattleEndCondition = BattleEndCondition.WIN): 891 | # ESCAPE always ends the battle abruptly. WIN, Will end the battle and return to normal, DEFEAT will end the battle with game over. 892 | match state: 893 | BattleEndCondition.ESCAPE: 894 | print("cutscene will play.") 895 | pass 896 | BattleEndCondition.WIN: 897 | hud.show_battle_result("Victory! All enemies have been defeated.") 898 | for player in players: 899 | player.gain_experience(100) 900 | for enemy in enemies: 901 | enemy.queue_free() 902 | # Be sure to toggle Enemy's off on the scene you left. 903 | get_tree().change_scene_to_file(game_map) 904 | BattleEndCondition.DEFEAT: 905 | hud.show_battle_result("Game Over. All players have been defeated.") 906 | hud.hide_action_buttons() 907 | 908 | func update_button_states(): 909 | ActionButtons.get_node("Attack").disabled = not attack_toggle 910 | ActionButtons.get_node("Skills").disabled = not skills_toggle 911 | ActionButtons.get_node("Defend").disabled = not defend_toggle 912 | ActionButtons.get_node("Items").disabled = not item_toggle 913 | ActionButtons.get_node("Skills").disabled = not run_toggle 914 | 915 | func _apply_action_effects(target: Battler) -> void: 916 | match queued_action: 917 | "attack": 918 | if current_character.default_attack: 919 | current_character.use_skill(current_character.default_attack, target) 920 | else: 921 | current_character.attack_anim(target) 922 | "skill": 923 | if queued_skill: 924 | current_character.use_skill(queued_skill, target) 925 | "item": 926 | if queued_item: 927 | current_character.battle_item(queued_item, target) 928 | --------------------------------------------------------------------------------