├── 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 |
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 |
--------------------------------------------------------------------------------