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]
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]

View File

@ -55,6 +55,6 @@ layout_mode = 2
theme_override_constants/margin_top = 10
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
text = "restart"

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ColdMint.scripts.camp;
using ColdMint.scripts.damage;
using ColdMint.scripts.debug;
@ -100,7 +101,7 @@ public partial class CharacterTemplate : CharacterBody2D
//角色创建后的初始血量
private int _initialHp;
protected int MaxHp;
public int MaxHp;
/// <summary>
/// <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>();
/// <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>
/// <para>Find the nearest item within the pick up area(Does not include items currently held)</para>
/// <para>在拾捡范围内查找距离最近的物品(不包括当前持有的物品)</para>
@ -433,7 +462,7 @@ public partial class CharacterTemplate : CharacterBody2D
/// <para>处理角色死亡的事件</para>
/// </summary>
/// <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
//如果攻击者不为空,且角色名不为空,那么打印角色死亡信息
@ -451,6 +480,7 @@ public partial class CharacterTemplate : CharacterBody2D
}
QueueFree();
return Task.CompletedTask;
}
/// <summary>

View File

@ -1,6 +1,8 @@
using System;
using System.Text;
using System.Threading.Tasks;
using ColdMint.scripts.damage;
using ColdMint.scripts.deathInfo;
using ColdMint.scripts.map.events;
using ColdMint.scripts.utils;
using ColdMint.scripts.weapon;
@ -153,6 +155,11 @@ public partial class Player : CharacterTemplate
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
//在平台检测射线与平台碰撞状态改变时
if (_platformDetectionRayCast2D != null && _platformDetectionRayCast2D.IsColliding() != _collidingWithPlatform)
@ -315,6 +322,11 @@ public partial class Player : CharacterTemplate
public override void _Process(double delta)
{
if (!Visible)
{
return;
}
AimTheCurrentItemAtAPoint(GetGlobalMousePosition());
var itemMarker2DPosition = Vector2.Zero;
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
{
DeathInfo = "\"白纸\"失手将自己杀死。"
};
EventManager.GameOverEvent(gameOverEvent);
return;
}
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)

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
{
private Label? _deathInfoLabel;
private Button? _restartButton;
public override void InitializeUi()
{
@ -19,11 +20,25 @@ public partial class GameOverLoaderMenuLoader : UiLoaderTemplate
public override void InitializeData()
{
_restartButton = GetNodeOrNull<Button>("CenterContainer/VBoxContainer/MarginContainer2/RestartButton");
_deathInfoLabel =
GetNode<Label>("CenterContainer/VBoxContainer/MarginContainer/CenterContainer2/DeathInfoLabel");
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)
{
if (_deathInfoLabel == null)

View File

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Text;
using ColdMint.scripts.camp;
using ColdMint.scripts.deathInfo;
using ColdMint.scripts.debug;
using ColdMint.scripts.map;
using ColdMint.scripts.map.roomInjectionProcessor;
@ -38,6 +39,7 @@ public partial class MainMenuLoader : UiLoaderTemplate
LogCat.MinLogLevel = LogCat.DisableAllLogLevel;
}
DeathInfoGenerator.RegisterDeathInfoHandler(new SelfDeathInfoHandler());
MapGenerator.RegisterRoomInjectionProcessor(new ChanceRoomInjectionProcessor());
MapGenerator.RegisterRoomInjectionProcessor(new TimeIntervalRoomInjectorProcessor());
//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();
_playerPackedScene = GD.Load<PackedScene>("res://prefab/entitys/Character.tscn");
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)
{
//An existing player instance will not be created.
//已经存在玩家实例,不再创建。
GameSceneNodeHolder.Player.Position = GlobalPosition;
GameSceneNodeHolder.Player.Revive(GameSceneNodeHolder.Player.MaxHp);
return;
}
SpawnPlayer();
}
/// <summary>
/// <para>Generate player instance</para>
/// <para>生成玩家实例</para>
/// </summary>
private void SpawnPlayer()
{
if (GameSceneNodeHolder.PlayerContainer == null)
{
return;
@ -57,9 +62,24 @@ public partial class PlayerSpawn : Marker2D
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()
{
base._ExitTree();
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)
{
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)
{
//我们在每次开火之前,检查武器的后坐力。
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;
return;
}
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>