Support for restarting the game.

支持重新开始游戏了。
This commit is contained in:
Cold-Mint 2024-06-04 22:23:06 +08:00
parent 74c85cd5a7
commit 2a836e32e6
Signed by: Cold-Mint
GPG Key ID: C5A9BF8A98E0CE99
19 changed files with 312 additions and 46 deletions

5
locals/DeathInfo.csv Normal file
View File

@ -0,0 +1,5 @@
id,zh,en,jp
#kill self
#自杀
death_info_self_1,{0}误伤了自己。,{0} accidentally shot himself.,{0}誤って自分を傷つけました。
death_info_self_2,{0}忘记了瞄准。,{0} forgot to aim.,{0}照準を忘れました。
1 id,zh,en,jp
2 #kill self
3 #自杀
4 death_info_self_1,{0}误伤了自己。,{0} accidentally shot himself.,{0}誤って自分を傷つけました。
5 death_info_self_2,{0}忘记了瞄准。,{0} forgot to aim.,{0}照準を忘れました。

View File

@ -0,0 +1,17 @@
[remap]
importer="csv_translation"
type="Translation"
uid="uid://dwx0hwuy0uqio"
[deps]
files=["res://locals/DeathInfo.zh.translation", "res://locals/DeathInfo.en.translation", "res://locals/DeathInfo.jp.translation"]
source_file="res://locals/DeathInfo.csv"
dest_files=["res://locals/DeathInfo.zh.translation", "res://locals/DeathInfo.en.translation", "res://locals/DeathInfo.jp.translation"]
[params]
compress=true
delimiter=0

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -146,7 +146,7 @@ hotbar_previous={
[internationalization] [internationalization]
locale/translations=PackedStringArray("res://locals/UI.en.translation", "res://locals/UI.zh.translation", "res://locals/Log.en.translation", "res://locals/Log.zh.translation", "res://locals/Weapon.en.translation", "res://locals/Weapon.zh.translation", "res://locals/InputMapping.en.translation", "res://locals/InputMapping.zh.translation", "res://locals/InputMapping.jp.translation", "res://locals/Log.jp.translation", "res://locals/UI.jp.translation", "res://locals/Weapon.jp.translation", "res://locals/Slogan.en.translation", "res://locals/Slogan.jp.translation", "res://locals/Slogan.zh.translation") locale/translations=PackedStringArray("res://locals/UI.en.translation", "res://locals/UI.zh.translation", "res://locals/Log.en.translation", "res://locals/Log.zh.translation", "res://locals/Weapon.en.translation", "res://locals/Weapon.zh.translation", "res://locals/InputMapping.en.translation", "res://locals/InputMapping.zh.translation", "res://locals/InputMapping.jp.translation", "res://locals/Log.jp.translation", "res://locals/UI.jp.translation", "res://locals/Weapon.jp.translation", "res://locals/Slogan.en.translation", "res://locals/Slogan.jp.translation", "res://locals/Slogan.zh.translation", "res://locals/DeathInfo.en.translation", "res://locals/DeathInfo.jp.translation", "res://locals/DeathInfo.zh.translation")
[layer_names] [layer_names]

View File

@ -55,6 +55,6 @@ layout_mode = 2
theme_override_constants/margin_top = 10 theme_override_constants/margin_top = 10
theme_override_constants/margin_bottom = 150 theme_override_constants/margin_bottom = 150
[node name="Button" type="Button" parent="CenterContainer/VBoxContainer/MarginContainer2"] [node name="RestartButton" type="Button" parent="CenterContainer/VBoxContainer/MarginContainer2"]
layout_mode = 2 layout_mode = 2
text = "restart" text = "restart"

View File

@ -11,8 +11,18 @@ public class EventManager
/// </summary> /// </summary>
public static Action<AiCharacterGenerateEvent>? AiCharacterGenerateEvent; public static Action<AiCharacterGenerateEvent>? AiCharacterGenerateEvent;
/// <summary>
/// <para>Game Over Event</para>
/// <para>游戏结束事件</para>
/// </summary>
public static Action<GameOverEvent>? GameOverEvent; public static Action<GameOverEvent>? GameOverEvent;
/// <summary>
/// <para>Events when the game is replayed</para>
/// <para>游戏重玩时的事件</para>
/// </summary>
public static Action<GameReplayEvent>? GameReplayEvent;
/// <summary> /// <summary>
/// <para>Map starts generating events</para> /// <para>Map starts generating events</para>
/// <para>地图开始生成的事件</para> /// <para>地图开始生成的事件</para>

View File

@ -45,8 +45,7 @@ public class AiPickNode : BehaviorTreeNodeTemplate
{ {
//If it's a weapon //If it's a weapon
//如果是武器 //如果是武器
var distance = weaponTemplate.GlobalPosition - Character.GlobalPosition; var distanceLength = weaponTemplate.GlobalPosition.DistanceTo(Character.GlobalPosition);
var distanceLength = distance.Length();
if (distanceLength < closestDistance) if (distanceLength < closestDistance)
{ {
closestDistance = distanceLength; closestDistance = distanceLength;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using ColdMint.scripts.camp; using ColdMint.scripts.camp;
using ColdMint.scripts.damage; using ColdMint.scripts.damage;
using ColdMint.scripts.debug; using ColdMint.scripts.debug;
@ -100,7 +101,7 @@ public partial class CharacterTemplate : CharacterBody2D
//角色创建后的初始血量 //角色创建后的初始血量
private int _initialHp; private int _initialHp;
protected int MaxHp; public int MaxHp;
/// <summary> /// <summary>
/// <para>The camp ID of the role</para> /// <para>The camp ID of the role</para>
@ -121,6 +122,34 @@ public partial class CharacterTemplate : CharacterBody2D
public Node[] PickingRangeBodies => PickingRangeBodiesList?.ToArray() ?? Array.Empty<Node>(); public Node[] PickingRangeBodies => PickingRangeBodiesList?.ToArray() ?? Array.Empty<Node>();
/// <summary>
/// <para>Resurrected character</para>
/// <para>复活角色</para>
/// </summary>
/// <remarks>
///<para>Sets the amount of Hp a character has after resurrection</para>
///<para>设置角色复活后拥有的Hp</para>
/// </remarks>
public void Revive(int newHp)
{
//If the new Hp is less than or equal to 0, there is no need to resurrect
//如果新的Hp小于等于0那么不需要复活
if (newHp <= 0)
{
return;
}
if (CurrentHp > 0)
{
//If the current Hp is greater than 0, there is no need to revive
//如果当前Hp大于0那么不需要复活
return;
}
CurrentHp = newHp;
Visible = true;
}
/// <summary> /// <summary>
/// <para>Find the nearest item within the pick up area(Does not include items currently held)</para> /// <para>Find the nearest item within the pick up area(Does not include items currently held)</para>
/// <para>在拾捡范围内查找距离最近的物品(不包括当前持有的物品)</para> /// <para>在拾捡范围内查找距离最近的物品(不包括当前持有的物品)</para>
@ -433,7 +462,7 @@ public partial class CharacterTemplate : CharacterBody2D
/// <para>处理角色死亡的事件</para> /// <para>处理角色死亡的事件</para>
/// </summary> /// </summary>
/// <param name="damageTemplate"></param> /// <param name="damageTemplate"></param>
protected virtual void OnDie(DamageTemplate damageTemplate) protected virtual Task OnDie(DamageTemplate damageTemplate)
{ {
//If the attacker is not empty and the role name is not empty, then the role death message is printed //If the attacker is not empty and the role name is not empty, then the role death message is printed
//如果攻击者不为空,且角色名不为空,那么打印角色死亡信息 //如果攻击者不为空,且角色名不为空,那么打印角色死亡信息
@ -451,6 +480,7 @@ public partial class CharacterTemplate : CharacterBody2D
} }
QueueFree(); QueueFree();
return Task.CompletedTask;
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Text; using System.Text;
using System.Threading.Tasks;
using ColdMint.scripts.damage; using ColdMint.scripts.damage;
using ColdMint.scripts.deathInfo;
using ColdMint.scripts.map.events; using ColdMint.scripts.map.events;
using ColdMint.scripts.utils; using ColdMint.scripts.utils;
using ColdMint.scripts.weapon; using ColdMint.scripts.weapon;
@ -153,6 +155,11 @@ public partial class Player : CharacterTemplate
protected override void HookPhysicsProcess(ref Vector2 velocity, double delta) protected override void HookPhysicsProcess(ref Vector2 velocity, double delta)
{ {
if (!Visible)
{
return;
}
//When the collision state between the platform detection ray and the platform changes //When the collision state between the platform detection ray and the platform changes
//在平台检测射线与平台碰撞状态改变时 //在平台检测射线与平台碰撞状态改变时
if (_platformDetectionRayCast2D != null && _platformDetectionRayCast2D.IsColliding() != _collidingWithPlatform) if (_platformDetectionRayCast2D != null && _platformDetectionRayCast2D.IsColliding() != _collidingWithPlatform)
@ -315,6 +322,11 @@ public partial class Player : CharacterTemplate
public override void _Process(double delta) public override void _Process(double delta)
{ {
if (!Visible)
{
return;
}
AimTheCurrentItemAtAPoint(GetGlobalMousePosition()); AimTheCurrentItemAtAPoint(GetGlobalMousePosition());
var itemMarker2DPosition = Vector2.Zero; var itemMarker2DPosition = Vector2.Zero;
if (ItemMarker2D != null) if (ItemMarker2D != null)
@ -366,18 +378,21 @@ public partial class Player : CharacterTemplate
} }
} }
protected override void OnDie(DamageTemplate damageTemplate) protected override async Task OnDie(DamageTemplate damageTemplate)
{ {
if (EventManager.GameOverEvent != null) Visible = false;
if (EventManager.GameOverEvent == null)
{ {
var gameOverEvent = new GameOverEvent return;
{
DeathInfo = "\"白纸\"失手将自己杀死。"
};
EventManager.GameOverEvent(gameOverEvent);
} }
Visible = false; var gameOverEvent = new GameOverEvent();
if (damageTemplate.Attacker != null)
{
gameOverEvent.DeathInfo = await DeathInfoGenerator.GenerateDeathInfo(this, damageTemplate.Attacker);
}
EventManager.GameOverEvent.Invoke(gameOverEvent);
} }
protected override void EnterThePickingRangeBody(Node node) protected override void EnterThePickingRangeBody(Node node)

View File

@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using ColdMint.scripts.character;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.deathInfo;
public static class DeathInfoGenerator
{
private static List<IDeathInfoHandler>? _deathInfoHandlers;
/// <summary>
/// <para>Register the death message handler</para>
/// <para>注册死亡信息处理器</para>
/// </summary>
/// <param name="deathInfoHandler"></param>
public static void RegisterDeathInfoHandler(IDeathInfoHandler deathInfoHandler)
{
_deathInfoHandlers ??= new List<IDeathInfoHandler>();
_deathInfoHandlers.Add(deathInfoHandler);
}
/// <summary>
/// <para>Unregister the death message handler</para>
/// <para>取消注册死亡信息处理器</para>
/// </summary>
/// <param name="deathInfoHandler"></param>
public static void UnregisterDeathInfoHandler(IDeathInfoHandler deathInfoHandler)
{
if (_deathInfoHandlers == null)
{
return;
}
_deathInfoHandlers.Remove(deathInfoHandler);
}
/// <summary>
/// <para>Generate death info</para>
/// <para>生成死亡信息</para>
/// </summary>
/// <param name="victim"></param>
/// <param name="killer"></param>
/// <returns></returns>
public static async Task<string> GenerateDeathInfo(Player victim, Node killer)
{
var victimName = victim.ReadOnlyCharacterName ?? victim.Name;
string killerName = killer.Name;
if (killer is CharacterTemplate characterTemplate)
{
killerName = characterTemplate.ReadOnlyCharacterName ?? killer.Name;
}
if (_deathInfoHandlers == null || _deathInfoHandlers.Count == 0)
{
return GenerateDefaultDeathInfo(victimName, killerName) ?? string.Empty;
}
foreach (var deathInfoHandler in _deathInfoHandlers)
{
var deathInfo = await deathInfoHandler.GenerateDeathInfo(victimName, killerName, victim, killer);
if (!string.IsNullOrEmpty(deathInfo))
{
return deathInfo;
}
}
return GenerateDefaultDeathInfo(victimName, killerName) ?? string.Empty;
}
/// <summary>
/// <para>Generate a default death message</para>
/// <para>生成默认的死亡信息</para>
/// </summary>
/// <param name="victimName"></param>
/// <param name="killerName"></param>
/// <returns></returns>
private static string? GenerateDefaultDeathInfo(string victimName, string killerName)
{
return TranslationServerUtils.TranslateWithFormat("death_info", victimName, killerName);
}
}

View File

@ -0,0 +1,35 @@
using System.Threading.Tasks;
using ColdMint.scripts.character;
using Godot;
namespace ColdMint.scripts.deathInfo;
/// <summary>
/// <para>Death information processor</para>
/// <para>死亡信息处理器</para>
/// </summary>
public interface IDeathInfoHandler
{
/// <summary>
/// <para>Generate death info</para>
/// <para>生成死亡信息</para>
/// </summary>
/// <param name="victimName">
///<para>victimName</para>
///<para>受害者名称</para>
/// </param>
/// <param name="killerName">
///<para>KillerName</para>
///<para>杀手名称</para>
/// </param>
/// <param name="victim">
/// <para>victim</para>
/// <para>受害者</para>
/// </param>
/// <param name="killer">
/// <para>Killer</para>
/// <para>杀手</para>
/// </param>
/// <returns></returns>
public Task<string?> GenerateDeathInfo(string victimName, string killerName, Player victim, Node killer);
}

View File

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using ColdMint.scripts.character;
using ColdMint.scripts.utils;
using Godot;
namespace ColdMint.scripts.deathInfo;
/// <summary>
/// <para>Deal with the message that your game failed due to accidental injury</para>
/// <para>处理自己误伤导致游戏失败的信息</para>
/// </summary>
public class SelfDeathInfoHandler : IDeathInfoHandler
{
private const string Prefix = "death_info_self_";
private const int Length = 2;
public Task<string?> GenerateDeathInfo(string victimName, string killerName, Player victim, Node killer)
{
if (victim != killer) return Task.FromResult<string?>(null);
var index = GD.Randi() % Length + 1;
return Task.FromResult(
TranslationServerUtils.TranslateWithFormat(Prefix + index, victimName, killerName));
}
}

View File

@ -11,6 +11,7 @@ namespace ColdMint.scripts.loader.uiLoader;
public partial class GameOverLoaderMenuLoader : UiLoaderTemplate public partial class GameOverLoaderMenuLoader : UiLoaderTemplate
{ {
private Label? _deathInfoLabel; private Label? _deathInfoLabel;
private Button? _restartButton;
public override void InitializeUi() public override void InitializeUi()
{ {
@ -19,11 +20,25 @@ public partial class GameOverLoaderMenuLoader : UiLoaderTemplate
public override void InitializeData() public override void InitializeData()
{ {
_restartButton = GetNodeOrNull<Button>("CenterContainer/VBoxContainer/MarginContainer2/RestartButton");
_deathInfoLabel = _deathInfoLabel =
GetNode<Label>("CenterContainer/VBoxContainer/MarginContainer/CenterContainer2/DeathInfoLabel"); GetNode<Label>("CenterContainer/VBoxContainer/MarginContainer/CenterContainer2/DeathInfoLabel");
EventManager.GameOverEvent += OnGameOver; EventManager.GameOverEvent += OnGameOver;
} }
public override void LoadUiActions()
{
if (_restartButton != null)
{
_restartButton.Pressed += () =>
{
var replayEvent = new GameReplayEvent();
EventManager.GameReplayEvent?.Invoke(replayEvent);
Visible = false;
};
}
}
private void OnGameOver(GameOverEvent gameOverEvent) private void OnGameOver(GameOverEvent gameOverEvent)
{ {
if (_deathInfoLabel == null) if (_deathInfoLabel == null)

View File

@ -2,6 +2,7 @@ using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using ColdMint.scripts.camp; using ColdMint.scripts.camp;
using ColdMint.scripts.deathInfo;
using ColdMint.scripts.debug; using ColdMint.scripts.debug;
using ColdMint.scripts.map; using ColdMint.scripts.map;
using ColdMint.scripts.map.roomInjectionProcessor; using ColdMint.scripts.map.roomInjectionProcessor;
@ -38,6 +39,7 @@ public partial class MainMenuLoader : UiLoaderTemplate
LogCat.MinLogLevel = LogCat.DisableAllLogLevel; LogCat.MinLogLevel = LogCat.DisableAllLogLevel;
} }
DeathInfoGenerator.RegisterDeathInfoHandler(new SelfDeathInfoHandler());
MapGenerator.RegisterRoomInjectionProcessor(new ChanceRoomInjectionProcessor()); MapGenerator.RegisterRoomInjectionProcessor(new ChanceRoomInjectionProcessor());
MapGenerator.RegisterRoomInjectionProcessor(new TimeIntervalRoomInjectorProcessor()); MapGenerator.RegisterRoomInjectionProcessor(new TimeIntervalRoomInjectorProcessor());
//Register the corresponding encoding provider to solve the problem of garbled Chinese path of the compressed package //Register the corresponding encoding provider to solve the problem of garbled Chinese path of the compressed package

View File

@ -18,21 +18,26 @@ public partial class PlayerSpawn : Marker2D
base._Ready(); base._Ready();
_playerPackedScene = GD.Load<PackedScene>("res://prefab/entitys/Character.tscn"); _playerPackedScene = GD.Load<PackedScene>("res://prefab/entitys/Character.tscn");
EventManager.MapGenerationCompleteEvent += MapGenerationCompleteEvent; EventManager.MapGenerationCompleteEvent += MapGenerationCompleteEvent;
EventManager.GameReplayEvent += GameReplayEvent;
} }
private void MapGenerationCompleteEvent(MapGenerationCompleteEvent mapGenerationCompleteEvent) private void GameReplayEvent(GameReplayEvent gameReplayEvent)
{ {
EventManager.MapGenerationCompleteEvent -= MapGenerationCompleteEvent;
//After the map is generated, create the player instance.
//当地图生成完成后,创建玩家实例。
if (GameSceneNodeHolder.Player != null) if (GameSceneNodeHolder.Player != null)
{ {
//An existing player instance will not be created.
//已经存在玩家实例,不再创建。
GameSceneNodeHolder.Player.Position = GlobalPosition; GameSceneNodeHolder.Player.Position = GlobalPosition;
GameSceneNodeHolder.Player.Revive(GameSceneNodeHolder.Player.MaxHp);
return; return;
} }
SpawnPlayer();
}
/// <summary>
/// <para>Generate player instance</para>
/// <para>生成玩家实例</para>
/// </summary>
private void SpawnPlayer()
{
if (GameSceneNodeHolder.PlayerContainer == null) if (GameSceneNodeHolder.PlayerContainer == null)
{ {
return; return;
@ -57,9 +62,24 @@ public partial class PlayerSpawn : Marker2D
LogCat.LogWithFormat("player_spawn_debug", player.ReadOnlyCharacterName, player.Position); LogCat.LogWithFormat("player_spawn_debug", player.ReadOnlyCharacterName, player.Position);
} }
private void MapGenerationCompleteEvent(MapGenerationCompleteEvent mapGenerationCompleteEvent)
{
//After the map is generated, create the player instance.
//当地图生成完成后,创建玩家实例。
if (GameSceneNodeHolder.Player != null)
{
//An existing player instance will not be created.
//已经存在玩家实例,不再创建。
return;
}
SpawnPlayer();
}
public override void _ExitTree() public override void _ExitTree()
{ {
base._ExitTree(); base._ExitTree();
EventManager.MapGenerationCompleteEvent -= MapGenerationCompleteEvent; EventManager.MapGenerationCompleteEvent -= MapGenerationCompleteEvent;
EventManager.GameReplayEvent -= GameReplayEvent;
} }
} }

View File

@ -0,0 +1,10 @@
namespace ColdMint.scripts.map.events;
/// <summary>
/// <para>Game replay event</para>
/// <para>游戏重玩事件</para>
/// </summary>
public class GameReplayEvent
{
}

View File

@ -177,34 +177,36 @@ public partial class WeaponTemplate : RigidBody2D, IItem
public void Fire(Node2D? owner, Vector2 enemyGlobalPosition) public void Fire(Node2D? owner, Vector2 enemyGlobalPosition)
{ {
var nowTime = DateTime.Now; var nowTime = DateTime.Now;
if (_lastFiringTime == null || nowTime - _lastFiringTime > _firingInterval) //If the present time minus the time of the last fire is less than the interval between fires, it means that the fire cannot be fired yet.
//如果现在时间减去上次开火时间小于开火间隔,说明还不能开火。
if (_lastFiringTime != null && nowTime - _lastFiringTime < _firingInterval)
{ {
if (owner is CharacterTemplate characterTemplate) return;
{
//我们在每次开火之前,检查武器的后坐力。
if (_recoil != Vector2.Zero)
{
//假设此武器拥有后坐力
var force = new Vector2();
var forceX = Math.Abs(_recoil.X);
if (Math.Abs(RotationDegrees) < 90)
{
//The weapon goes to the right and we apply a recoil to the left
//武器朝向右边我们向左施加后坐力
forceX = -forceX;
}
force.X = forceX * Config.CellSize;
force.Y = _recoil.Y * Config.CellSize;
characterTemplate.AddForce(force);
}
}
//If the time difference is greater than the firing interval, then fire
//如果可以时间差大于开火间隔,那么开火
DoFire(owner, enemyGlobalPosition);
_lastFiringTime = nowTime;
} }
if (owner is CharacterTemplate characterTemplate)
{
//We check the recoil of the weapon before each firing.
//我们在每次开火之前,检查武器的后坐力。
if (_recoil != Vector2.Zero)
{
var force = new Vector2();
var forceX = Math.Abs(_recoil.X);
if (Math.Abs(RotationDegrees) < 90)
{
//The weapon goes to the right and we apply a recoil to the left
//武器朝向右边我们向左施加后坐力
forceX = -forceX;
}
force.X = forceX * Config.CellSize;
force.Y = _recoil.Y * Config.CellSize;
characterTemplate.AddForce(force);
}
}
DoFire(owner, enemyGlobalPosition);
_lastFiringTime = nowTime;
} }
/// <summary> /// <summary>