diff --git a/prefab/entitys/DelivererOfDarkMagic.tscn b/prefab/entitys/DelivererOfDarkMagic.tscn
index 5d7f278..210854d 100644
--- a/prefab/entitys/DelivererOfDarkMagic.tscn
+++ b/prefab/entitys/DelivererOfDarkMagic.tscn
@@ -1,9 +1,10 @@
-[gd_scene load_steps=9 format=3 uid="uid://cj65pso40syj5"]
+[gd_scene load_steps=11 format=3 uid="uid://cj65pso40syj5"]
[ext_resource type="Script" path="res://scripts/character/AiCharacter.cs" id="1_ubaid"]
[ext_resource type="Texture2D" uid="uid://b1twcink38sh0" path="res://sprites/Player.png" id="2_eha68"]
[ext_resource type="Script" path="res://scripts/damage/DamageNumberNodeSpawn.cs" id="3_kiam3"]
[ext_resource type="PackedScene" uid="uid://sqqfrmikmk5v" path="res://prefab/ui/HealthBar.tscn" id="4_gt388"]
+[ext_resource type="Script" path="res://scripts/bubble/BubbleMarker.cs" id="5_y2fh5"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_bb8wt"]
radius = 20.0
@@ -24,7 +25,10 @@ animations = [{
}]
[sub_resource type="CircleShape2D" id="CircleShape2D_c61vr"]
-radius = 129.027
+radius = 82.2192
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_fowd5"]
+radius = 233.808
[node name="DelivererOfDarkMagic" type="CharacterBody2D"]
collision_layer = 64
@@ -80,3 +84,18 @@ shape = SubResource("CircleShape2D_c61vr")
[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."]
debug_enabled = true
+
+[node name="BubbleMarker" type="Marker2D" parent="."]
+position = Vector2(0, -79)
+script = ExtResource("5_y2fh5")
+
+[node name="VisibleOnScreenEnabler2D" type="VisibleOnScreenEnabler2D" parent="."]
+position = Vector2(0, 5.5)
+scale = Vector2(2.04, 3.05)
+
+[node name="ScoutArea2D" type="Area2D" parent="."]
+collision_layer = 0
+collision_mask = 68
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="ScoutArea2D"]
+shape = SubResource("CircleShape2D_fowd5")
diff --git a/prefab/ui/FloatLabel.tscn b/prefab/ui/FloatLabel.tscn
index a776c97..ce612b5 100644
--- a/prefab/ui/FloatLabel.tscn
+++ b/prefab/ui/FloatLabel.tscn
@@ -9,7 +9,6 @@ grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="."]
-z_index = 1
layout_mode = 0
offset_right = 40.0
offset_bottom = 23.0
diff --git a/prefab/ui/plaint.tscn b/prefab/ui/plaint.tscn
new file mode 100644
index 0000000..875cb78
--- /dev/null
+++ b/prefab/ui/plaint.tscn
@@ -0,0 +1,27 @@
+[gd_scene format=3 uid="uid://dmiyu1y726uo8"]
+
+[node name="Plaint" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -20.0
+offset_top = -36.0
+offset_right = 20.0
+offset_bottom = 36.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/font_color = Color(0.988235, 0.768627, 0.0980392, 1)
+theme_override_font_sizes/font_size = 48
+text = "!"
+horizontal_alignment = 1
diff --git a/prefab/ui/query.tscn b/prefab/ui/query.tscn
new file mode 100644
index 0000000..7c6ac5b
--- /dev/null
+++ b/prefab/ui/query.tscn
@@ -0,0 +1,26 @@
+[gd_scene format=3 uid="uid://bvpvdloxe4wdf"]
+
+[node name="Query" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -20.0
+offset_top = -36.0
+offset_right = 20.0
+offset_bottom = 36.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_font_sizes/font_size = 48
+text = "?"
+horizontal_alignment = 1
diff --git a/scripts/bubble/BubbleMarker.cs b/scripts/bubble/BubbleMarker.cs
new file mode 100644
index 0000000..65eee77
--- /dev/null
+++ b/scripts/bubble/BubbleMarker.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using ColdMint.scripts.utils;
+using Godot;
+
+namespace ColdMint.scripts.bubble;
+
+///
+/// BubbleMarker
+/// 气泡位置标记
+///
+public partial class BubbleMarker : Marker2D
+{
+ private readonly Dictionary _bubbleDictionary = [];
+
+ ///
+ /// Add bubbles
+ /// 添加气泡
+ ///
+ ///
+ ///
+ ///
+ public bool AddBubble(int id, Node2D node)
+ {
+ if (!_bubbleDictionary.TryAdd(id, node))
+ {
+ return false;
+ }
+
+ node.Hide();
+ NodeUtils.CallDeferredAddChild(this, node);
+ return true;
+ }
+
+ ///
+ /// DisplayBubble
+ /// 显示气泡
+ ///
+ ///
+ ///Display specific nodes above the creature as "bubbles", for example, question bubbles when an enemy finds the player.
+ ///在生物头顶显示特定的节点作为“气泡”,例如:当敌人发现玩家后将显示疑问气泡。
+ ///
+ ///
+ public void ShowBubble(int id)
+ {
+ if (!_bubbleDictionary.TryGetValue(id, out var value))
+ {
+ return;
+ }
+ value.Show();
+ }
+
+ ///
+ /// Hidden bubble
+ /// 隐藏气泡
+ ///
+ public void HideBubble(int id)
+ {
+ if (!_bubbleDictionary.TryGetValue(id, out var value))
+ {
+ return;
+ }
+ value.Hide();
+ }
+}
\ No newline at end of file
diff --git a/scripts/character/AiCharacter.cs b/scripts/character/AiCharacter.cs
index 638489b..e199b61 100644
--- a/scripts/character/AiCharacter.cs
+++ b/scripts/character/AiCharacter.cs
@@ -1,6 +1,9 @@
+using System;
using System.Collections.Generic;
+using ColdMint.scripts.bubble;
using ColdMint.scripts.camp;
using ColdMint.scripts.stateMachine;
+using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.character;
@@ -19,12 +22,28 @@ public sealed partial class AiCharacter : CharacterTemplate
private Vector2 _wallDetectionOrigin;
private Area2D? _attackArea;
+ ///
+ /// Reconnaissance area
+ /// 侦察区域
+ ///
+ ///
+ ///Most of the time, when the enemy enters the reconnaissance area, the character will issue a "question mark" and try to move slowly towards the event point.
+ ///大多数情况下,当敌人进入侦察区域后,角色会发出“疑问(问号)”,并尝试向事件点缓慢移动。
+ ///
+ private Area2D? _scoutArea;
+
///
/// All enemies within striking distance
/// 在攻击范围内的所有敌人
///
private List? _enemyInTheAttackRange;
+ ///
+ /// Scout all enemies within range
+ /// 在侦察范围内所有的敌人
+ ///
+ private List? _enemyInTheScoutRange;
+
///
/// Obstacle detection ray during attack
@@ -37,6 +56,8 @@ public sealed partial class AiCharacter : CharacterTemplate
private RayCast2D? _attackObstacleDetection;
+ private VisibleOnScreenEnabler2D? _screenEnabler2D;
+
///
/// Navigation agent
/// 导航代理
@@ -49,12 +70,69 @@ public sealed partial class AiCharacter : CharacterTemplate
public RayCast2D? AttackObstacleDetection => _attackObstacleDetection;
+
+ ///
+ /// Exclamation bubble Id
+ /// 感叹气泡Id
+ ///
+ private const int plaintBubbleId = 0;
+
+ ///
+ /// Query bubble Id
+ /// 疑问气泡Id
+ ///
+ private const int queryBubbleId = 1;
+
+ ///
+ /// BubbleMarker
+ /// 气泡标记
+ ///
+ ///
+ ///Subsequent production of dialogue bubbles can be put into the parent class for players to use.
+ ///后续制作对话泡时可进其放到父类,供玩家使用。
+ ///
+ private BubbleMarker? _bubbleMarker;
+
public override void _Ready()
{
base._Ready();
+
_enemyInTheAttackRange = new List();
+ _enemyInTheScoutRange = new List();
+ _screenEnabler2D = GetNode("VisibleOnScreenEnabler2D");
+ _screenEnabler2D.ScreenEntered += () =>
+ {
+ //When the character enters the screen.
+ //当角色进入屏幕。
+ ProcessMode = ProcessModeEnum.Disabled;
+ };
+ _screenEnabler2D.ScreenExited += () =>
+ {
+ //When the character leaves the screen.
+ //当角色离开屏幕。
+ ProcessMode = ProcessModeEnum.Inherit;
+ };
+ _bubbleMarker = GetNode("BubbleMarker");
+ if (_bubbleMarker != null)
+ {
+ using var plaintScene = GD.Load("res://prefab/ui/plaint.tscn");
+ var plaint = NodeUtils.InstantiatePackedScene(plaintScene);
+ if (plaint != null)
+ {
+ _bubbleMarker.AddBubble(plaintBubbleId, plaint);
+ }
+
+ using var queryScene = GD.Load("res://prefab/ui/query.tscn");
+ var query = NodeUtils.InstantiatePackedScene(queryScene);
+ if (query != null)
+ {
+ _bubbleMarker.AddBubble(queryBubbleId, query);
+ }
+ }
+
_wallDetection = GetNode("WallDetection");
_attackArea = GetNode("AttackArea2D");
+ _scoutArea = GetNode("ScoutArea2D");
NavigationAgent2D = GetNode("NavigationAgent2D");
if (ItemMarker2D != null)
{
@@ -73,6 +151,14 @@ public sealed partial class AiCharacter : CharacterTemplate
_attackArea.BodyExited += ExitTheAttackArea;
}
+ if (_scoutArea != null)
+ {
+ _scoutArea.Monitoring = true;
+ _scoutArea.Monitorable = false;
+ _scoutArea.BodyEntered += EnterTheScoutArea;
+ _scoutArea.BodyExited += ExitTheScoutArea;
+ }
+
_wallDetectionOrigin = _wallDetection.TargetPosition;
StateMachine = new PatrolStateMachine();
StateMachine.Context = new StateContext
@@ -87,34 +173,78 @@ public sealed partial class AiCharacter : CharacterTemplate
}
///
- /// EnemyDetected
- /// 是否发现敌人
+ /// Display exclamation marks
+ /// 显示感叹号
+ ///
+ public void DispladyPlaint()
+ {
+ _bubbleMarker?.ShowBubble(plaintBubbleId);
+ }
+
+ public void HidePlaint()
+ {
+ _bubbleMarker?.HideBubble(plaintBubbleId);
+ }
+
+ ///
+ /// Displady Query
+ /// 显示疑问
+ ///
+ public void DispladyQuery()
+ {
+ _bubbleMarker?.ShowBubble(queryBubbleId);
+ }
+
+ public void HiddenQuery()
+ {
+ _bubbleMarker?.HideBubble(queryBubbleId);
+ }
+
+ ///
+ /// Whether the enemy has been detected in the reconnaissance area
+ /// 侦察范围是否发现敌人
///
///
///Have you spotted the enemy?
///是否发现敌人
///
- public bool EnemyDetected()
+ public bool ScoutEnemyDetected()
{
- if (_enemyInTheAttackRange == null)
+ if (_enemyInTheScoutRange == null)
{
return false;
}
- return _enemyInTheAttackRange.Count > 0;
+ return _enemyInTheScoutRange.Count > 0;
}
///
- /// Get the first enemy to enter range
- /// 获取第一个进入范围的敌人
+ /// Get the first enemy in range
+ /// 获取第一个进入侦察范围的敌人
///
///
- public CharacterTemplate? GetFirstEnemy()
+ public CharacterTemplate? GetFirstEnemyInScoutArea()
+ {
+ if (_enemyInTheScoutRange == null || _enemyInTheScoutRange.Count == 0)
+ {
+ return null;
+ }
+
+ return _enemyInTheScoutRange[0];
+ }
+
+ ///
+ /// Get the first enemy within striking range
+ /// 获取第一个进入攻击范围的敌人
+ ///
+ ///
+ public CharacterTemplate? GetFirstEnemyInAttackArea()
{
if (_enemyInTheAttackRange == null || _enemyInTheAttackRange.Count == 0)
{
return null;
}
+
return _enemyInTheAttackRange[0];
}
@@ -125,36 +255,95 @@ public sealed partial class AiCharacter : CharacterTemplate
{
var nextPathPosition = NavigationAgent2D.GetNextPathPosition();
var direction = (nextPathPosition - GlobalPosition).Normalized();
- velocity = direction * Config.CellSize * Speed;
+ velocity = direction * Config.CellSize * Speed * ProtectedSpeedScale;
}
}
- private void EnterTheAttackArea(Node node)
+ ///
+ /// When the node enters the reconnaissance area
+ /// 当节点进入侦察区域后
+ ///
+ ///
+ private void EnterTheScoutArea(Node node)
+ {
+ CanCauseHarmNode(node, (canCause, characterTemplate) =>
+ {
+ if (canCause && characterTemplate != null)
+ {
+ _enemyInTheScoutRange?.Add(characterTemplate);
+ }
+ });
+ }
+
+ ///
+ /// When the node exits the reconnaissance area
+ /// 当节点退出侦察区域后
+ ///
+ ///
+ private void ExitTheScoutArea(Node node)
{
if (node == this)
{
- //The target can't be yourself.
- //攻击目标不能是自己。
return;
}
if (node is CharacterTemplate characterTemplate)
{
- //Determine if damage can be done between factions
- //判断阵营间是否可造成伤害
- var camp = CampManager.GetCamp(CampId);
- var enemyCamp = CampManager.GetCamp(characterTemplate.CampId);
- if (enemyCamp != null && camp != null)
- {
- var canCause = CampManager.CanCauseHarm(camp, enemyCamp);
- if (canCause)
- {
- _enemyInTheAttackRange?.Add(characterTemplate);
- }
- }
+ _enemyInTheScoutRange?.Remove(characterTemplate);
}
}
+ ///
+ /// When a node enters the attack zone
+ /// 当节点进入攻击区域后
+ ///
+ ///
+ private void EnterTheAttackArea(Node node)
+ {
+ CanCauseHarmNode(node, (canCause, characterTemplate) =>
+ {
+ if (canCause && characterTemplate != null)
+ {
+ _enemyInTheAttackRange?.Add(characterTemplate);
+ }
+ });
+ }
+
+ ///
+ /// CanCauseHarmNode
+ /// 是否可伤害某个节点
+ ///
+ ///
+ ///
+ private void CanCauseHarmNode(Node node, Action action)
+ {
+ if (node == this)
+ {
+ //The target can't be yourself.
+ //攻击目标不能是自己。
+ action.Invoke(false, null);
+ return;
+ }
+
+ if (node is not CharacterTemplate characterTemplate)
+ {
+ action.Invoke(false, null);
+ return;
+ }
+
+ //Determine if damage can be done between factions
+ //判断阵营间是否可造成伤害
+ var camp = CampManager.GetCamp(CampId);
+ var enemyCamp = CampManager.GetCamp(characterTemplate.CampId);
+ if (enemyCamp != null && camp != null)
+ {
+ action.Invoke(CampManager.CanCauseHarm(camp, enemyCamp), characterTemplate);
+ return;
+ }
+
+ action.Invoke(false, characterTemplate);
+ }
+
private void ExitTheAttackArea(Node node)
{
if (node == this)
@@ -194,6 +383,12 @@ public sealed partial class AiCharacter : CharacterTemplate
_attackArea.BodyExited -= ExitTheAttackArea;
}
+ if (_scoutArea != null)
+ {
+ _scoutArea.BodyEntered -= EnterTheScoutArea;
+ _scoutArea.BodyExited -= ExitTheScoutArea;
+ }
+
if (StateMachine != null)
{
StateMachine.Stop();
diff --git a/scripts/character/CharacterTemplate.cs b/scripts/character/CharacterTemplate.cs
index 573fff6..92e5df1 100644
--- a/scripts/character/CharacterTemplate.cs
+++ b/scripts/character/CharacterTemplate.cs
@@ -38,6 +38,36 @@ public partial class CharacterTemplate : CharacterBody2D
///
protected const float Speed = 5f;
+ ///
+ /// Speed multiplier
+ /// 速度乘数
+ ///
+ protected float ProtectedSpeedScale = 1f;
+
+ ///
+ /// Speed multiplier
+ /// 速度乘数
+ ///
+ ///Set to 0.5 to move at 50% of the normal speed.
+ ///设置为0.5则以正常速度的50%移动。
+ ///
+ ///
+ public float SpeedScale
+ {
+ get => ProtectedSpeedScale;
+ set
+ {
+ if (value > 1)
+ {
+ ProtectedSpeedScale = 1;
+ }
+ else
+ {
+ ProtectedSpeedScale = value;
+ }
+ }
+ }
+
protected const float JumpVelocity = -240;
//物品被扔出后多长时间恢复与地面和平台的碰撞(单位:秒)
diff --git a/scripts/character/Player.cs b/scripts/character/Player.cs
index 6e0068d..7548cc4 100644
--- a/scripts/character/Player.cs
+++ b/scripts/character/Player.cs
@@ -210,7 +210,7 @@ public partial class Player : CharacterTemplate
//Moving left and right
//左右移动
var axis = Input.GetAxis("ui_left", "ui_right");
- velocity.X = axis * Speed * Config.CellSize;
+ velocity.X = axis * Speed * Config.CellSize * ProtectedSpeedScale;
//Use items
//使用物品
diff --git a/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs b/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs
index 0874edd..96f68dc 100644
--- a/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs
+++ b/scripts/stateMachine/StateProcessor/ChaseStateProcessor.cs
@@ -17,21 +17,24 @@ public class ChaseStateProcessor : StateProcessorTemplate
return;
}
- //Get the first enemy to enter the attack range.
- //获取第一次进入攻击范围的敌人。
- var enemy = aiCharacter.GetFirstEnemy();
+ //Get the first enemy to enter the reconnaissance range.
+ //获取第一次进入侦察范围的敌人。
+ var enemy = aiCharacter.GetFirstEnemyInScoutArea();
if (enemy == null)
{
//No more enemies. Return to previous status.
//没有敌人了,返回上一个状态。
+ aiCharacter.HiddenQuery();
+ aiCharacter.SetTargetPosition(aiCharacter.GlobalPosition);
LogCat.Log("chase_no_enemy", label: LogCat.LogLabel.ChaseStateProcessor);
context.CurrentState = context.PreviousState;
}
else
{
- //Chase the enemy.
- //追击敌人。
+ //Set the position of the enemy entering the range to the position we are going to.
+ //将进入范围的敌人位置设置为我们要前往的位置。
aiCharacter.SetTargetPosition(enemy.GlobalPosition);
+ aiCharacter.DispladyQuery();
}
}
diff --git a/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs b/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs
index 3b40f4b..3d7c9cd 100644
--- a/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs
+++ b/scripts/stateMachine/StateProcessor/PatrolStateProcessor.cs
@@ -41,8 +41,10 @@ public class PatrolStateProcessor : StateProcessorTemplate
return;
}
- if (aiCharacter.EnemyDetected())
+ if (aiCharacter.ScoutEnemyDetected())
{
+ //Seeing that the enemy had entered the reconnaissance area, we gave chase immediately.
+ //发现敌人进入侦察范围,我们立即追击。
context.CurrentState = State.Chase;
LogCat.Log("patrol_enemy_detected", label: LogCat.LogLabel.PatrolStateProcessor);
return;
@@ -50,12 +52,16 @@ public class PatrolStateProcessor : StateProcessorTemplate
if (Points == null || Points.Length == 0)
{
+ //There are no patrol points.
+ //没有巡逻点。
LogCat.LogError("no_points", label: LogCat.LogLabel.PatrolStateProcessor);
return;
}
if (_originPosition == null)
{
+ //If there is no origin, then wait for the character to collide with the ground to set it as the origin.
+ //如果没有原点,那么等待角色与地面碰撞时将其设置为原点。
if (!aiCharacter.IsOnFloor())
{
LogCat.LogWarning("patrol_not_on_floor", LogCat.LogLabel.PatrolStateProcessor);
@@ -69,8 +75,10 @@ public class PatrolStateProcessor : StateProcessorTemplate
var point = _originPosition + Points[_index];
var distance = aiCharacter.GlobalPosition.DistanceTo(point.Value);
- if (distance < 10)
+ if (distance < 5)
{
+ //No need to actually come to the patrol point, we just need a distance to get close.
+ //无需真正的来到巡逻点,我们只需要一个距离接近了就可以了。
LogCat.LogWithFormat("patrol_arrival_point", LogCat.LogLabel.PatrolStateProcessor, point);
_index++;
if (_index >= Points.Length)