upd
This commit is contained in:
commit
c286271083
13 files changed
+1134
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "turn_battle_base",
|
||||||
|
"name": "Turn-based battle engine",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "ct",
|
||||||
|
"description": "Turn-based battle GUI: battle scene, victory/defeat screens, EXP and loot reports",
|
||||||
|
"category": "library, gameplay",
|
||||||
|
"sources": [
|
||||||
|
"src/**/*.cs"
|
||||||
|
],
|
||||||
|
"depends": [ "core>=1", "interface>=1" ]
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bsrphp6130ptb"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dq85ubii8cfid" path="res://Core/Battle/Actions/BattleActionMachine.cs" id="BattleActionMachineScript"]
|
||||||
|
[ext_resource type="Script" uid="uid://c1rwrejr5aoc0" path="res://Core/Battle/BattleSceneContext.cs" id="BattleSceneContextScript"]
|
||||||
|
[ext_resource type="Script" path="src/BattleSpeedControl.cs" id="BattleSpeedControlScript"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bjhtml8ujba7p" path="res://scenes/battle/deadground.tscn" id="DeadgroundScene"]
|
||||||
|
[ext_resource type="Script" uid="uid://icsu2hv2c68r" path="res://Core/UI/Lights/UiLightController.cs" id="UiLightControllerScript"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b3yq032kqhtn4" path="res://scenes/battle/winground.tscn" id="WingroundScene"]
|
||||||
|
|
||||||
|
[node name="BattleScene" type="Control" unique_id=760066045]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("BattleSceneContextScript")
|
||||||
|
|
||||||
|
[node name="BattleBg" type="TextureRect" parent="." unique_id=1553515841]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="Panel" type="Control" parent="." unique_id=1818202398]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="ToolPanel" type="Control" parent="." unique_id=16130515]
|
||||||
|
layout_mode = 1
|
||||||
|
anchor_top = 0.9
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
|
||||||
|
[node name="BattleActionMachine" type="Node" parent="ToolPanel" unique_id=459578957]
|
||||||
|
script = ExtResource("BattleActionMachineScript")
|
||||||
|
|
||||||
|
[node name="BattleSpeedControl" type="Control" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
script = ExtResource("BattleSpeedControlScript")
|
||||||
|
|
||||||
|
[node name="Deadground" parent="." unique_id=2064995106 instance=ExtResource("DeadgroundScene")]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Winground" parent="." unique_id=652178152 instance=ExtResource("WingroundScene")]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Lights" type="Control" parent="." unique_id=1021519456]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
script = ExtResource("UiLightControllerScript")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cdln7efykbahy"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="src/DeadgroundBehaviour.cs" id="DeadgroundBehaviourController"]
|
||||||
|
|
||||||
|
[node name="Deadground" type="Control" unique_id=1769284696]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("DeadgroundBehaviourController")
|
||||||
|
|
||||||
|
[node name="TransitionBg" type="TextureRect" parent="." unique_id=2003806831]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="Content" type="Control" parent="." unique_id=516317159]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="DimBg" type="ColorRect" parent="Content" unique_id=1040388089]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0.7)
|
||||||
|
|
||||||
|
[node name="CenterPanel" type="VBoxContainer" parent="Content" unique_id=695019244]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -200.0
|
||||||
|
offset_top = -100.0
|
||||||
|
offset_right = 200.0
|
||||||
|
offset_bottom = 100.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="Content/CenterPanel" unique_id=1047371679]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
theme_override_colors/font_color = Color(1, 0.3, 0.3, 1)
|
||||||
|
theme_override_font_sizes/font_size = 48
|
||||||
|
text = "ui/battle/defeat"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
auto_translate = true
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="Content/CenterPanel" unique_id=1145465550]
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="MainMenuButton" type="Button" parent="Content/CenterPanel" unique_id=1265108975]
|
||||||
|
custom_minimum_size = Vector2(0, 50)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "ui/battle/endgame/to_main_menu"
|
||||||
|
auto_translate = true
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Content/CenterPanel/MainMenuButton" to="." method="ToMainMenuClick"]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dx40qu1dj80in"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="src/ExpReportCard.cs" id="ExpReportCardScript"]
|
||||||
|
|
||||||
|
[node name="ExpReportCard" type="Control" unique_id=223866085]
|
||||||
|
custom_minimum_size = Vector2(130, 130)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("ExpReportCardScript")
|
||||||
|
icon = NodePath("Panel/Icon")
|
||||||
|
nameLabel = NodePath("Panel/NameLabel")
|
||||||
|
expLabel = NodePath("Panel/ExpLabel")
|
||||||
|
|
||||||
|
[node name="Panel" type="VBoxContainer" parent="." unique_id=284918240]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Icon" type="TextureRect" parent="Panel" unique_id=149388562]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 5
|
||||||
|
|
||||||
|
[node name="NameLabel" type="Label" parent="Panel" unique_id=555712225]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ExpLabel" type="Label" parent="Panel" unique_id=1019918817]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(0.5, 1, 0.5, 1)
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
horizontal_alignment = 1
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[gd_scene format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="src/LootReportItem.cs" id="loot_script"]
|
||||||
|
|
||||||
|
[node name="LootReportItem" type="Control"]
|
||||||
|
custom_minimum_size = Vector2(80, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("loot_script")
|
||||||
|
icon = NodePath("Panel/Icon")
|
||||||
|
countLabel = NodePath("Panel/CountLabel")
|
||||||
|
|
||||||
|
[node name="Panel" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 2
|
||||||
|
alignment = 1
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="Icon" type="TextureRect" parent="Panel"]
|
||||||
|
custom_minimum_size = Vector2(64, 64)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
expand_mode = 1
|
||||||
|
stretch_mode = 5
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="CountLabel" type="Label" parent="Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
horizontal_alignment = 1
|
||||||
|
mouse_filter = 2
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cpfjgflxria5i"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="src/WingroundBehaviour.cs" id="1_win"]
|
||||||
|
|
||||||
|
[node name="Winground" type="Control" unique_id=1812297436 node_paths=PackedStringArray("lootTitle")]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_win")
|
||||||
|
lootTitle = NodePath("Content/LootTitle")
|
||||||
|
|
||||||
|
[node name="TransitionBg" type="TextureRect" parent="." unique_id=1411801503]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="Content" type="Control" parent="." unique_id=865412688]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="DimBg" type="ColorRect" parent="Content" unique_id=1956777181]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0.7)
|
||||||
|
|
||||||
|
[node name="CharExpContainer" type="HBoxContainer" parent="Content" unique_id=1416765042]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.03
|
||||||
|
anchor_top = 0.05
|
||||||
|
anchor_right = 0.42100003
|
||||||
|
anchor_bottom = 0.344
|
||||||
|
offset_right = -0.32006836
|
||||||
|
offset_bottom = -0.25601196
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 20
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="LootTitle" type="Label" parent="Content" unique_id=2075960163]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.65400004
|
||||||
|
anchor_top = 0.544
|
||||||
|
anchor_right = 0.95100003
|
||||||
|
anchor_bottom = 0.58400005
|
||||||
|
offset_left = -0.6800537
|
||||||
|
offset_top = -0.056030273
|
||||||
|
offset_right = -0.92004395
|
||||||
|
offset_bottom = -0.016052246
|
||||||
|
text = "ui/battle/victory/loot_title"
|
||||||
|
|
||||||
|
[node name="LootScrollContainer" type="ScrollContainer" parent="Content" unique_id=2098696418]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.65400004
|
||||||
|
anchor_top = 0.544
|
||||||
|
anchor_right = 0.96000004
|
||||||
|
anchor_bottom = 0.94200003
|
||||||
|
offset_left = -0.6800537
|
||||||
|
offset_top = -0.056030273
|
||||||
|
offset_right = -0.20007324
|
||||||
|
offset_bottom = 0.39196777
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
horizontal_scroll_mode = 3
|
||||||
|
|
||||||
|
[node name="LootContent" type="Control" parent="Content/LootScrollContainer" unique_id=969489928]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="CenterPanel" type="VBoxContainer" parent="Content" unique_id=385679072]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.31800002
|
||||||
|
anchor_top = 0.26000002
|
||||||
|
anchor_right = 0.68200004
|
||||||
|
anchor_bottom = 0.55200005
|
||||||
|
offset_left = -0.5600586
|
||||||
|
offset_top = -0.24002075
|
||||||
|
offset_right = 0.5599365
|
||||||
|
offset_bottom = -0.24804688
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="Content/CenterPanel" unique_id=583298800]
|
||||||
|
auto_translate_mode = 1
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
theme_override_colors/font_color = Color(0.3, 1, 0.3, 1)
|
||||||
|
theme_override_font_sizes/font_size = 48
|
||||||
|
text = "ui/battle/victory"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="Content/CenterPanel" unique_id=1854105230]
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ContinueButton" type="Button" parent="Content/CenterPanel" unique_id=1307713785]
|
||||||
|
auto_translate_mode = 1
|
||||||
|
custom_minimum_size = Vector2(0, 50)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "ui/battle/victory/return"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Content/CenterPanel/ContinueButton" to="." method="ToReturnScene"]
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Cthangover.Core.Battle;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
|
||||||
|
/// Engine.TimeScale to globally accelerate the entire game loop —
|
||||||
|
/// not just animations. Persists the choice to GameData.Settings
|
||||||
|
/// so it survives scene reloads. <see cref="ResetToNormal"/> restores 1x
|
||||||
|
/// without saving and is the part the core scene consumes through
|
||||||
|
/// <see cref="IBattleSpeedControl"/> when battle ends.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BattleSpeedControl : Control, IBattleSpeedControl
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, Button> _buttons = new();
|
||||||
|
private int _activeSpeed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the speed bar in the constructor. Godot never runs a
|
||||||
|
/// <c>_Ready</c> override declared in a mod assembly, so the buttons and the
|
||||||
|
/// saved-speed restore must happen here. Anchors are used instead of a
|
||||||
|
/// viewport-size lookup because the viewport is unavailable before the node
|
||||||
|
/// enters the tree.
|
||||||
|
/// </summary>
|
||||||
|
public BattleSpeedControl()
|
||||||
|
{
|
||||||
|
var speeds = new[] { 1, 4, 8 };
|
||||||
|
var hbox = new HBoxContainer { Name = "SpeedButtons" };
|
||||||
|
hbox.Alignment = BoxContainer.AlignmentMode.Begin;
|
||||||
|
|
||||||
|
foreach (var speed in speeds)
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Text = $"x{speed}",
|
||||||
|
CustomMinimumSize = new Vector2(48, 32)
|
||||||
|
};
|
||||||
|
btn.Pressed += () => OnSpeedSelected(speed);
|
||||||
|
hbox.AddChild(btn);
|
||||||
|
_buttons[speed] = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddChild(hbox);
|
||||||
|
|
||||||
|
AnchorLeft = 1f;
|
||||||
|
AnchorRight = 1f;
|
||||||
|
AnchorTop = 0f;
|
||||||
|
AnchorBottom = 0f;
|
||||||
|
OffsetLeft = -200f;
|
||||||
|
OffsetRight = -20f;
|
||||||
|
OffsetTop = 20f;
|
||||||
|
OffsetBottom = 52f;
|
||||||
|
|
||||||
|
ApplySavedSpeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the speed from <see cref="GameData.Settings.BattleSpeed"/>.
|
||||||
|
/// Called on <c>_Ready</c> to reapply the saved preference.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplySavedSpeed()
|
||||||
|
{
|
||||||
|
var speed = GameData.Instance.Settings.BattleSpeed;
|
||||||
|
if (speed < 1 || speed > 8)
|
||||||
|
speed = 1;
|
||||||
|
|
||||||
|
_activeSpeed = speed;
|
||||||
|
Engine.TimeScale = speed;
|
||||||
|
UpdateHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets <see cref="Engine.TimeScale"/> to 1.0x without
|
||||||
|
/// persisting to settings. Used when battle ends so the lobby
|
||||||
|
/// and menus run at normal speed regardless of the player's
|
||||||
|
/// in-battle preference.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetToNormal()
|
||||||
|
{
|
||||||
|
Engine.TimeScale = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpeedSelected(int speed)
|
||||||
|
{
|
||||||
|
_activeSpeed = speed;
|
||||||
|
Engine.TimeScale = speed;
|
||||||
|
|
||||||
|
var settings = GameData.Instance.Settings;
|
||||||
|
settings.BattleSpeed = speed;
|
||||||
|
settings.Save();
|
||||||
|
|
||||||
|
UpdateHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHighlight()
|
||||||
|
{
|
||||||
|
foreach (var kv in _buttons)
|
||||||
|
{
|
||||||
|
var isActive = kv.Key == _activeSpeed;
|
||||||
|
kv.Value.Modulate = isActive ? new Color(0.4f, 1f, 0.4f, 1f) : Colors.White;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Cthangover.Core.Audio;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI.Base.Lists;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defeat screen widget. Minimal compared to WingroundBehaviour —
|
||||||
|
/// only sets the deadground background texture (resolved from mods)
|
||||||
|
/// and offers a Main Menu button that clears BattleData and reloads
|
||||||
|
/// the menu scene. No loot, no EXP.
|
||||||
|
/// </summary>
|
||||||
|
public partial class DeadgroundBehaviour : TransitionWidget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Clears battle data and switches to the main menu scene.
|
||||||
|
/// Bound to the "Main Menu" button on the defeat screen.
|
||||||
|
/// </summary>
|
||||||
|
public void ToMainMenuClick()
|
||||||
|
{
|
||||||
|
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||||
|
sceneService?.SwitchToMenu();
|
||||||
|
GameData.Instance.Runtime.BattleData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plays the defeat sound, resolves the deadground background
|
||||||
|
/// texture from mods, and shows the transition widget.
|
||||||
|
/// </summary>
|
||||||
|
public override void Show()
|
||||||
|
{
|
||||||
|
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||||
|
|
||||||
|
var audioService = GetNode<AudioService>("/root/AudioService");
|
||||||
|
audioService?.PlaySound("battle/deadground", SoundType.UI);
|
||||||
|
|
||||||
|
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE", $"Deadground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||||
|
|
||||||
|
if (bg != null && bg.Texture == null)
|
||||||
|
{
|
||||||
|
var tex = Textures.Resolve("deadground");
|
||||||
|
GameLogger.Log("BATTLE", $"Deadground.Show: ResolveTexture('deadground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||||
|
bg.Texture = tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Show();
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE", $"Deadground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Single character EXP report in the victory screen. Displays the
|
||||||
|
/// character icon, localized name, and "+N" EXP value. Uses the same
|
||||||
|
/// ResolveMissingNodes pattern as LootReportItem for robustness
|
||||||
|
/// against missing scene links.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ExpReportCard : Control
|
||||||
|
{
|
||||||
|
[Export] private TextureRect icon;
|
||||||
|
[Export] private Label nameLabel;
|
||||||
|
[Export] private Label expLabel;
|
||||||
|
|
||||||
|
private void ResolveMissingNodes()
|
||||||
|
{
|
||||||
|
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||||
|
nameLabel ??= GetNodeOrNull<Label>("Panel/NameLabel");
|
||||||
|
expLabel ??= GetNodeOrNull<Label>("Panel/ExpLabel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the card with a character icon, localised name, and
|
||||||
|
/// "+N" EXP value. Calls <c>ResolveMissingNodes</c> to tolerate
|
||||||
|
/// incomplete scene wiring in the editor.
|
||||||
|
/// </summary>
|
||||||
|
public void Setup(Texture2D characterIcon, string characterName, int exp)
|
||||||
|
{
|
||||||
|
ResolveMissingNodes();
|
||||||
|
|
||||||
|
if (icon != null && characterIcon != null)
|
||||||
|
icon.Texture = characterIcon;
|
||||||
|
|
||||||
|
if (nameLabel != null)
|
||||||
|
nameLabel.Text = characterName;
|
||||||
|
|
||||||
|
if (expLabel != null)
|
||||||
|
expLabel.Text = "+" + exp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single loot cell in the victory grid. Shows icon with an optional
|
||||||
|
/// "xN" count label (hidden when count is 1). ResolveMissingNodes
|
||||||
|
/// supports both editor-assigned exports and runtime lookups so the
|
||||||
|
/// prefab works with incomplete scene wiring. TooltipText is set to
|
||||||
|
/// the localized item name for hover.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LootReportItem : Control
|
||||||
|
{
|
||||||
|
[Export] private TextureRect icon;
|
||||||
|
[Export] private Label countLabel;
|
||||||
|
|
||||||
|
private void ResolveMissingNodes()
|
||||||
|
{
|
||||||
|
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||||
|
countLabel ??= GetNodeOrNull<Label>("Panel/CountLabel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the loot cell: sets the item icon, a "xN" count
|
||||||
|
/// label (hidden when <paramref name="count"/> is 1), and the
|
||||||
|
/// localised name as the tooltip. Calls
|
||||||
|
/// <c>ResolveMissingNodes</c> to handle missing editor
|
||||||
|
/// assignments gracefully.
|
||||||
|
/// </summary>
|
||||||
|
public void Setup(Texture2D itemIcon, string localizedName, int count)
|
||||||
|
{
|
||||||
|
ResolveMissingNodes();
|
||||||
|
|
||||||
|
if (icon != null && itemIcon != null)
|
||||||
|
icon.Texture = itemIcon;
|
||||||
|
|
||||||
|
if (countLabel != null)
|
||||||
|
countLabel.Text = count > 1 ? "x" + count : "";
|
||||||
|
|
||||||
|
TooltipText = localizedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Floating damage-number overlay. Self-contained: creates its own Label
|
||||||
|
/// in _Ready, floats upward with random horizontal drift, fades alpha,
|
||||||
|
/// then QueueFrees itself. Static factory methods (SpawnDamage,
|
||||||
|
/// SpawnDefence) instantiate and parent it in one call — damage is red,
|
||||||
|
/// heal is green, defence is blue. MouseFilter is Ignore so clicks pass
|
||||||
|
/// through to cards underneath.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ShowDamageBehaviour : Control
|
||||||
|
{
|
||||||
|
private Label _label;
|
||||||
|
|
||||||
|
private float duration = 1.0f;
|
||||||
|
private float floatDistance = 80f;
|
||||||
|
private float driftRange = 30f;
|
||||||
|
private Vector2 startPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the label on first use. Godot does not dispatch <c>_Ready</c>
|
||||||
|
/// overrides declared in mod assemblies (only methods generated in the core
|
||||||
|
/// project run), so the label cannot be created in <c>_Ready</c> — it is
|
||||||
|
/// created here, right before the first animation starts.
|
||||||
|
/// </summary>
|
||||||
|
private void EnsureLabel()
|
||||||
|
{
|
||||||
|
if (_label != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MouseFilter = MouseFilterEnum.Ignore;
|
||||||
|
|
||||||
|
_label = new Label();
|
||||||
|
_label.SetAnchorsPreset(LayoutPreset.Center);
|
||||||
|
_label.HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
_label.VerticalAlignment = VerticalAlignment.Center;
|
||||||
|
_label.AddThemeFontSizeOverride("font_size", 28);
|
||||||
|
_label.AddThemeColorOverride("font_outline_color", new Color(0, 0, 0, 1));
|
||||||
|
_label.AddThemeConstantOverride("outline_size", 6);
|
||||||
|
AddChild(_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and parents a floating damage (or heal) number.
|
||||||
|
/// Damage is red, heal is green. The label floats upward with
|
||||||
|
/// a random horizontal drift and auto-destructs after the
|
||||||
|
/// tween completes. Skips values <= 0.
|
||||||
|
/// </summary>
|
||||||
|
public static void SpawnDamage(int amount, Node parent, Vector2 position, bool isHeal = false)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var color = isHeal
|
||||||
|
? new Color(0.3f, 1f, 0.3f, 1f)
|
||||||
|
: new Color(1f, 0.3f, 0.3f, 1f);
|
||||||
|
|
||||||
|
var hint = new ShowDamageBehaviour();
|
||||||
|
parent.AddChild(hint);
|
||||||
|
hint.Init(amount, position, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and parents a floating defence number (blue).
|
||||||
|
/// Same floating behaviour as <see cref="SpawnDamage"/>.
|
||||||
|
/// Skips values <= 0.
|
||||||
|
/// </summary>
|
||||||
|
public static void SpawnDefence(int amount, Node parent, Vector2 position)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var hint = new ShowDamageBehaviour();
|
||||||
|
parent.AddChild(hint);
|
||||||
|
hint.Init(amount, position, new Color(0.3f, 0.3f, 1f, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the label text, colour, and world position, then starts
|
||||||
|
/// the floating animation tween. Called by the static factory
|
||||||
|
/// methods after instantiation.
|
||||||
|
/// </summary>
|
||||||
|
public void Init(int amount, Vector2 worldPosition, Color? color = null)
|
||||||
|
{
|
||||||
|
EnsureLabel();
|
||||||
|
|
||||||
|
_label.Text = amount.ToString();
|
||||||
|
if (color.HasValue)
|
||||||
|
_label.Modulate = color.Value;
|
||||||
|
|
||||||
|
GlobalPosition = worldPosition;
|
||||||
|
startPosition = Position;
|
||||||
|
|
||||||
|
RunAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void RunAnimation()
|
||||||
|
{
|
||||||
|
float driftX = (float)GD.RandRange(-driftRange, driftRange);
|
||||||
|
Vector2 targetPos = startPosition + new Vector2(driftX, -floatDistance);
|
||||||
|
|
||||||
|
var tween = CreateTween();
|
||||||
|
tween.SetParallel(true);
|
||||||
|
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||||
|
tween.TweenProperty(this, "position", targetPos, duration);
|
||||||
|
|
||||||
|
var fadeTween = CreateTween();
|
||||||
|
fadeTween.TweenProperty(_label, "modulate:a", 0f, duration * 0.8f);
|
||||||
|
|
||||||
|
await ToSignal(tween, "finished");
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the turn-based battle view scene with the core so that
|
||||||
|
/// <see cref="GodotSceneService"/> can route the logical "battle" scene to
|
||||||
|
/// this mod's tscn. Runs at resources-ready — before any battle can be
|
||||||
|
/// triggered — and is the reason the core no longer hardcodes a battle path.
|
||||||
|
/// </summary>
|
||||||
|
public class TurnBattleSceneInitializer : IModInitializer
|
||||||
|
{
|
||||||
|
private const string BattleScenePath = "scenes/battle/battle.tscn";
|
||||||
|
|
||||||
|
/// <summary>No-op: registration only needs the resources-ready stage.</summary>
|
||||||
|
public void OnModLoaded(string modId) { }
|
||||||
|
|
||||||
|
/// <summary>Publishes the battle view scene path to <see cref="BattleSceneRegistry"/>.</summary>
|
||||||
|
public void OnModResourcesReady()
|
||||||
|
{
|
||||||
|
BattleSceneRegistry.Register(BattleScenePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Core.Audio;
|
||||||
|
using Cthangover.Core.Battle;
|
||||||
|
using Cthangover.Core.Factories.Impls;
|
||||||
|
using Cthangover.Core.Items;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Relationship;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI.Base.Lists;
|
||||||
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.TurnBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Victory screen widget (extends TransitionWidget for fade-in).
|
||||||
|
/// On Show it: awards tracked EXP to each surviving character in the
|
||||||
|
/// battle set, builds a 4-slot EXP report card row (with placeholders
|
||||||
|
/// for missing slots to keep layout stable), rolls loot from defeated
|
||||||
|
/// enemies using per-item probability/range, lays out loot in a grid
|
||||||
|
/// whose cell size is computed from the container width and
|
||||||
|
/// lootAspectHeight, adds items to inventory, and processes character
|
||||||
|
/// recruitment via RecruitBehaviourRegistry. A "return" button sets
|
||||||
|
/// the pending scene and reloads BaseScene — the actual destination
|
||||||
|
/// scene (town_entry or the quest's ReturnScene) is injected by
|
||||||
|
/// the scene manager.
|
||||||
|
/// </summary>
|
||||||
|
public partial class WingroundBehaviour : TransitionWidget
|
||||||
|
{
|
||||||
|
private static readonly ModScenePrefab ExpReportCardPrefab = new("scenes/battle/exp_report_card.tscn");
|
||||||
|
private static readonly ModScenePrefab LootReportItemPrefab = new("scenes/battle/loot_report_item.tscn");
|
||||||
|
|
||||||
|
[Export] private Label txtInfo;
|
||||||
|
[Export] private Label lootTitle;
|
||||||
|
[Export] private Container charExpContainer;
|
||||||
|
[Export] private Control lootContent;
|
||||||
|
[Export] private int lootColumnCount = 5;
|
||||||
|
[Export] private float lootAspectHeight = 1.2f;
|
||||||
|
[Export] private Vector2 lootCellPadding = new(4f, 4f);
|
||||||
|
[Export] private float lootBorderLeft = 8f;
|
||||||
|
[Export] private float lootBorderRight = 8f;
|
||||||
|
[Export] private float lootBorderTop = 8f;
|
||||||
|
[Export] private float lootBorderBottom = 8f;
|
||||||
|
|
||||||
|
private BattleData data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Victory screen entry point. Builds a <see cref="WingroundContext"/>
|
||||||
|
/// from <see cref="BattleSceneContext"/> data, notifies
|
||||||
|
/// <see cref="WingroundListenerRegistry"/> listeners at three
|
||||||
|
/// phases (before EXP, after loot, before show), awards EXP
|
||||||
|
/// from the context (which listeners may have mutated), and
|
||||||
|
/// commits loot from the context to inventory. Finally calls
|
||||||
|
/// <c>base.Show()</c> for the fade-in animation.
|
||||||
|
/// </summary>
|
||||||
|
public override void Show()
|
||||||
|
{
|
||||||
|
lootTitle.Text = TranslationServer.Translate("ui/battle/victory/loot_title");
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE", $"Winground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}, txtInfo={(txtInfo != null ? "found" : "NULL")}", LogLevel.Debug);
|
||||||
|
|
||||||
|
var audioService = GetNode<AudioService>("/root/AudioService");
|
||||||
|
audioService?.PlaySound("battle/winground", SoundType.UI);
|
||||||
|
|
||||||
|
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||||
|
GameLogger.Log("BATTLE", $"Winground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||||
|
|
||||||
|
if (bg != null && bg.Texture == null)
|
||||||
|
{
|
||||||
|
var tex = Textures.Resolve("winground");
|
||||||
|
GameLogger.Log("BATTLE", $"Winground.Show: ResolveTexture('winground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||||
|
bg.Texture = tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = GameData.Instance.Runtime.BattleData;
|
||||||
|
GameLogger.Log("BATTLE_UI", $"Winground.Show: BattleData={(data != null ? "found" : "NULL")}", LogLevel.Error);
|
||||||
|
|
||||||
|
var expTracking = BuildExpTracking();
|
||||||
|
var defeatedEnemies = BuildDefeatedEnemies();
|
||||||
|
|
||||||
|
var ctx = new WingroundContext
|
||||||
|
{
|
||||||
|
PlayerExpGained = expTracking,
|
||||||
|
DefeatedEnemies = defeatedEnemies,
|
||||||
|
BattleData = data
|
||||||
|
};
|
||||||
|
|
||||||
|
WingroundListenerRegistry.Instance.NotifyBeforeExpAward(ctx);
|
||||||
|
|
||||||
|
foreach (var characterType in GameData.Instance.Runtime.CharacterData.BattleSet)
|
||||||
|
{
|
||||||
|
if (!GameData.Instance.Runtime.CharacterData.Characters.TryGetValue(characterType, out var info))
|
||||||
|
{
|
||||||
|
GameLogger.Log("BATTLE_UI", $"Winground.Show: characterType='{characterType}' NOT FOUND in Characters dict", LogLevel.Error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var earnedExp = 0;
|
||||||
|
if (ctx.PlayerExpGained != null && info?.ID != null)
|
||||||
|
ctx.PlayerExpGained.TryGetValue(info.ID, out earnedExp);
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"Winground.Show: char='{characterType}' id='{info?.ID}' oldExp={info.Exp} earnedExp={earnedExp} newExp={info.Exp + earnedExp}", LogLevel.Debug);
|
||||||
|
|
||||||
|
info.Exp += earnedExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildCharExpDisplay(ctx.PlayerExpGained);
|
||||||
|
|
||||||
|
if (ctx.DefeatedEnemies != null && ctx.DefeatedEnemies.Count > 0)
|
||||||
|
{
|
||||||
|
var loot = GenerateLoot(ctx.DefeatedEnemies);
|
||||||
|
ctx.GeneratedLoot = loot;
|
||||||
|
|
||||||
|
WingroundListenerRegistry.Instance.NotifyAfterLootGenerated(ctx);
|
||||||
|
|
||||||
|
BuildLootDisplay(ctx.GeneratedLoot);
|
||||||
|
AddLootToInventory(ctx.GeneratedLoot);
|
||||||
|
|
||||||
|
ProcessRecruitment(ctx.DefeatedEnemies);
|
||||||
|
}
|
||||||
|
|
||||||
|
WingroundListenerRegistry.Instance.NotifyBeforeShow(ctx);
|
||||||
|
|
||||||
|
base.Show();
|
||||||
|
GameLogger.Log("BATTLE", $"Winground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> BuildExpTracking()
|
||||||
|
{
|
||||||
|
var source = BattleSceneContext.Instance?.PlayerExpGained;
|
||||||
|
if (source == null || source.Count == 0)
|
||||||
|
return new Dictionary<string, int>();
|
||||||
|
|
||||||
|
var copy = new Dictionary<string, int>();
|
||||||
|
foreach (var kvp in source)
|
||||||
|
copy[kvp.Key] = kvp.Value;
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"Winground.BuildExpTracking: copied {copy.Count} entries", LogLevel.Debug);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Character> BuildDefeatedEnemies()
|
||||||
|
{
|
||||||
|
var source = BattleSceneContext.Instance?.DefeatedEnemies;
|
||||||
|
if (source == null || source.Count == 0)
|
||||||
|
return new List<Character>();
|
||||||
|
|
||||||
|
var copy = new List<Character>(source);
|
||||||
|
GameLogger.Log("BATTLE_UI", $"Winground.BuildDefeatedEnemies: copied {copy.Count} entries", LogLevel.Debug);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int REPORT_SLOT_COUNT = 4;
|
||||||
|
|
||||||
|
private void BuildCharExpDisplay(Dictionary<string, int> expTracking)
|
||||||
|
{
|
||||||
|
var container = charExpContainer ?? GetNodeOrNull<Container>("Content/CharExpContainer");
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: charExpContainer export={(charExpContainer != null ? "found" : "NULL")}, GetNodeOrNull fallback={(container != null ? "found" : "NULL")}", LogLevel.Error);
|
||||||
|
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
GameLogger.Log("BATTLE_UI", "BuildCharExpDisplay: EARLY RETURN — container is null", LogLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var child in container.GetChildren())
|
||||||
|
child.QueueFree();
|
||||||
|
|
||||||
|
var charData = GameData.Instance.Runtime.CharacterData;
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: expTracking={(expTracking != null ? $"found ({expTracking.Count} entries)" : "NULL")}, BattleSet count={charData.BattleSet.Count}", LogLevel.Error);
|
||||||
|
|
||||||
|
var battleSetList = charData.BattleSet.ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < REPORT_SLOT_COUNT; i++)
|
||||||
|
{
|
||||||
|
if (i < battleSetList.Count)
|
||||||
|
{
|
||||||
|
var characterType = battleSetList[i];
|
||||||
|
if (!charData.Characters.TryGetValue(characterType, out var info))
|
||||||
|
{
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: characterType='{characterType}' NOT FOUND in Characters dict", LogLevel.Error);
|
||||||
|
AddPlaceholderSlot(container, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var earnedExp = 0;
|
||||||
|
expTracking?.TryGetValue(info.ID, out earnedExp);
|
||||||
|
|
||||||
|
var cardTemplate = CharacterFactory.Instance.Get(info.ID);
|
||||||
|
var name = cardTemplate?.Name != null
|
||||||
|
? TranslationServer.Translate(cardTemplate.Name).ToString()
|
||||||
|
: info.ID;
|
||||||
|
|
||||||
|
var reportCard = ExpReportCardPrefab.Instantiate<ExpReportCard>();
|
||||||
|
reportCard.Setup(cardTemplate?.Image, name, earnedExp);
|
||||||
|
container.AddChild(reportCard);
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: added reportCard for '{characterType}', id='{info.ID}', name='{name}', earnedExp={earnedExp}, cardTemplate={(cardTemplate != null ? "found" : "NULL")}, icon={(cardTemplate?.Image != null ? "has" : "null")}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddPlaceholderSlot(container, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: finished, container now has {container.GetChildCount()} children", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPlaceholderSlot(Container container, int slotIndex)
|
||||||
|
{
|
||||||
|
var placeholder = new Control();
|
||||||
|
placeholder.CustomMinimumSize = new Vector2(130, 130);
|
||||||
|
placeholder.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||||
|
placeholder.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||||
|
placeholder.MouseFilter = MouseFilterEnum.Ignore;
|
||||||
|
container.AddChild(placeholder);
|
||||||
|
|
||||||
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: added placeholder slot {slotIndex}", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Random random = new();
|
||||||
|
|
||||||
|
private List<ItemContainer> GenerateLoot(List<Character> enemies)
|
||||||
|
{
|
||||||
|
var loot = new List<ItemContainer>();
|
||||||
|
|
||||||
|
foreach (var enemy in enemies)
|
||||||
|
{
|
||||||
|
if (enemy?.Loot == null || enemy.Loot.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var entry in enemy.Loot)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.ItemId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var probability = Math.Clamp(entry.Probability, 0, 100);
|
||||||
|
if (probability <= 0)
|
||||||
|
continue;
|
||||||
|
if (probability < 100 && random.Next(1, 101) > probability)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var count = entry.MinCount == entry.MaxCount
|
||||||
|
? entry.MinCount
|
||||||
|
: random.Next(entry.MinCount, entry.MaxCount + 1);
|
||||||
|
|
||||||
|
if (count > 0)
|
||||||
|
TryAddLootItem(loot, entry.ItemId, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryAddLootItem(List<ItemContainer> loot, string itemId, int count)
|
||||||
|
{
|
||||||
|
var existing = loot.Find(c => c.Item?.ID == itemId);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.Count += count;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var item = ItemFactory.Instance.Get(itemId);
|
||||||
|
if (item != null)
|
||||||
|
loot.Add(new ItemContainer { Item = item, Count = count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLootDisplay(List<ItemContainer> loot)
|
||||||
|
{
|
||||||
|
var content = lootContent ?? GetNodeOrNull<Control>("Content/LootScrollContainer/LootContent");
|
||||||
|
if (content == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var child in content.GetChildren())
|
||||||
|
child.QueueFree();
|
||||||
|
|
||||||
|
if (loot.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var scrollContainer = content.GetParent() as ScrollContainer;
|
||||||
|
if (scrollContainer != null)
|
||||||
|
{
|
||||||
|
content.CustomMinimumSize = new Vector2(scrollContainer.Size.X, 0);
|
||||||
|
content.Size = new Vector2(scrollContainer.Size.X, content.Size.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveWidth = content.Size.X - lootBorderLeft - lootBorderRight;
|
||||||
|
var cellWidth = (effectiveWidth - (lootColumnCount - 1) * lootCellPadding.X) / lootColumnCount;
|
||||||
|
var cellHeight = cellWidth * lootAspectHeight;
|
||||||
|
|
||||||
|
var rows = Mathf.CeilToInt((float)loot.Count / lootColumnCount);
|
||||||
|
var totalHeight = lootBorderTop + lootBorderBottom + rows * cellHeight + (rows - 1) * lootCellPadding.Y;
|
||||||
|
content.CustomMinimumSize = new Vector2(content.CustomMinimumSize.X, totalHeight);
|
||||||
|
|
||||||
|
for (int i = 0; i < loot.Count; i++)
|
||||||
|
{
|
||||||
|
var itemContainer = loot[i];
|
||||||
|
var item = itemContainer.Item;
|
||||||
|
if (item == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var localizedName = TranslationServer.Translate(item.Name).ToString();
|
||||||
|
|
||||||
|
var reportItem = LootReportItemPrefab.Instantiate<LootReportItem>();
|
||||||
|
reportItem.Setup(item.Sprite, localizedName, itemContainer.Count);
|
||||||
|
|
||||||
|
var col = i % lootColumnCount;
|
||||||
|
var row = i / lootColumnCount;
|
||||||
|
reportItem.Position = new Vector2(
|
||||||
|
lootBorderLeft + col * (cellWidth + lootCellPadding.X),
|
||||||
|
lootBorderTop + row * (cellHeight + lootCellPadding.Y)
|
||||||
|
);
|
||||||
|
reportItem.Size = new Vector2(cellWidth, cellHeight);
|
||||||
|
|
||||||
|
content.AddChild(reportItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddLootToInventory(List<ItemContainer> loot)
|
||||||
|
{
|
||||||
|
var inventory = GameData.Instance.Runtime.Inventory;
|
||||||
|
foreach (var itemContainer in loot)
|
||||||
|
{
|
||||||
|
if (itemContainer.Item != null)
|
||||||
|
inventory.Add(itemContainer.Item, itemContainer.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessRecruitment(List<Character> enemies)
|
||||||
|
{
|
||||||
|
var runtime = GameData.Instance.Runtime;
|
||||||
|
var registry = RecruitBehaviourRegistry.Instance;
|
||||||
|
|
||||||
|
foreach (var enemy in enemies)
|
||||||
|
{
|
||||||
|
if (enemy == null || enemy.RecruitmentChance <= 0)
|
||||||
|
continue;
|
||||||
|
if (!registry.CanRecruit(enemy, runtime))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var roll = random.Next(1, 101);
|
||||||
|
if (roll > enemy.RecruitmentChance)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
runtime.RecruitingData.Add(enemy.ID, enemy.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToReturnScene()
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
GameLogger.Log("BATTLE", "battle data is null!", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.Quest != null && data?.NewTag != null)
|
||||||
|
data.Quest.AddTag(data.NewTag);
|
||||||
|
var returnScene = data?.ReturnScene ?? "town_entry";
|
||||||
|
GameData.Instance.Runtime.BattleData = null;
|
||||||
|
|
||||||
|
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||||
|
var sceneManager = GetNode<SceneManager>("/root/SceneManager");
|
||||||
|
if (sceneManager != null)
|
||||||
|
sceneManager.PendingSceneName = returnScene;
|
||||||
|
sceneService?.LoadShell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user