2025-11-02 11:43:21 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using Duckov;
|
2025-11-05 21:34:21 +08:00
|
|
|
|
using HitFeedback.Api;
|
2025-11-02 11:43:21 +08:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Networking;
|
|
|
|
|
|
using UnityEngine.SceneManagement;
|
|
|
|
|
|
using Object = UnityEngine.Object;
|
|
|
|
|
|
using Random = UnityEngine.Random;
|
|
|
|
|
|
|
2025-11-01 23:26:25 +08:00
|
|
|
|
namespace HitFeedback
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
public class ModBehaviour : Duckov.Modding.ModBehaviour
|
2025-11-01 23:26:25 +08:00
|
|
|
|
{
|
2025-11-02 11:43:21 +08:00
|
|
|
|
public const string AudioFolderName = "audio";
|
2025-11-04 17:05:34 +08:00
|
|
|
|
public const string ConfigFileName = "config.ini";
|
2025-11-02 11:43:21 +08:00
|
|
|
|
public string audioFolderPath;
|
2025-11-04 17:05:34 +08:00
|
|
|
|
public string configFilePath;
|
2025-11-02 11:43:21 +08:00
|
|
|
|
|
2025-11-05 21:34:21 +08:00
|
|
|
|
public Dictionary<string, float> audioProbability = new Dictionary<string, float>();
|
|
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
public Health health;
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
public Config config = new Config();
|
|
|
|
|
|
|
2025-11-04 17:05:34 +08:00
|
|
|
|
public float totalWeight;
|
2025-11-02 11:43:21 +08:00
|
|
|
|
|
2025-11-05 21:34:21 +08:00
|
|
|
|
public const string MOD_SETTING_NAME = "受击反馈";
|
|
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
private void Update()
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
if (Input.GetKeyDown(config.hotKey))
|
2025-11-02 11:43:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
PlayRandomAudioClip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnAfterSetup()
|
|
|
|
|
|
{
|
|
|
|
|
|
LevelManager.OnLevelInitialized += OnSceneLoaded;
|
2025-11-05 21:34:21 +08:00
|
|
|
|
audioFolderPath = Path.Combine(info.path, AudioFolderName);
|
|
|
|
|
|
configFilePath = Path.Combine(info.path, ConfigFileName);
|
2025-11-02 11:43:21 +08:00
|
|
|
|
FindWavFiles();
|
2025-11-04 17:05:34 +08:00
|
|
|
|
config.LoadConfig(configFilePath);
|
|
|
|
|
|
ApplyConfig();
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
InitializeSetting();
|
|
|
|
|
|
UpdateTotalWeight();
|
2025-11-02 11:43:21 +08:00
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
protected override void OnBeforeDeactivate()
|
|
|
|
|
|
{
|
|
|
|
|
|
LevelManager.OnLevelInitialized -= OnSceneLoaded;
|
2025-11-04 17:05:34 +08:00
|
|
|
|
SaveConfig();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveConfig();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 21:34:21 +08:00
|
|
|
|
private void UpdateTotalWeight()
|
|
|
|
|
|
{
|
|
|
|
|
|
totalWeight = 0;
|
|
|
|
|
|
foreach (var f in audioProbability)
|
|
|
|
|
|
{
|
|
|
|
|
|
totalWeight += f.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 17:05:34 +08:00
|
|
|
|
private void ApplyConfig()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var f in config.probability)
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
if (audioProbability.ContainsKey(f.Key))
|
2025-11-04 17:05:34 +08:00
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
audioProbability[f.Key] = f.Value;
|
2025-11-04 17:05:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SaveConfig()
|
|
|
|
|
|
{
|
|
|
|
|
|
config.probability.Clear();
|
2025-11-05 21:34:21 +08:00
|
|
|
|
foreach (var f in audioProbability)
|
2025-11-04 17:05:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
config.probability.Add(f.Key, f.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
config.SaveConfig(configFilePath);
|
2025-11-02 11:43:21 +08:00
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
private void FindWavFiles()
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
audioProbability.Clear();
|
2025-11-02 11:43:21 +08:00
|
|
|
|
if (!Directory.Exists(audioFolderPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
var audioFiles = new List<string>();
|
|
|
|
|
|
string[] wavFiles = Directory.GetFiles(audioFolderPath, "*.wav", SearchOption.TopDirectoryOnly);
|
|
|
|
|
|
audioFiles.AddRange(wavFiles);
|
|
|
|
|
|
string[] mp3Files = Directory.GetFiles(audioFolderPath, "*.mp3", SearchOption.TopDirectoryOnly);
|
|
|
|
|
|
audioFiles.AddRange(mp3Files);
|
|
|
|
|
|
string[] oggFiles = Directory.GetFiles(audioFolderPath, "*.ogg", SearchOption.TopDirectoryOnly);
|
|
|
|
|
|
audioFiles.AddRange(oggFiles);
|
|
|
|
|
|
if (audioFiles.Count > 0)
|
2025-11-02 11:43:21 +08:00
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
foreach (var filePath in audioFiles)
|
2025-11-02 11:43:21 +08:00
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
audioProbability.Add(Path.GetFileName(filePath), 1);
|
2025-11-02 11:43:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (UnauthorizedAccessException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"Error: Access to '{audioFolderPath}' is denied. {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (DirectoryNotFoundException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"Error: Directory '{audioFolderPath}' not found. {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"An unexpected error occurred: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnSceneLoaded()
|
|
|
|
|
|
{
|
|
|
|
|
|
TryAddListener();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void TryAddListener()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (health)
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
health.OnHurtEvent.RemoveListener(OnHurtEvent);
|
|
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
health = CharacterMainControl.Main?.Health;
|
|
|
|
|
|
if (health)
|
|
|
|
|
|
{
|
|
|
|
|
|
health.OnHurtEvent.AddListener(OnHurtEvent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnHurtEvent(DamageInfo damageInfo)
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
if (config.ShouldPlayAudioFeedback(damageInfo))
|
|
|
|
|
|
PlayRandomAudioClip();
|
2025-11-02 11:43:21 +08:00
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
2025-11-02 11:43:21 +08:00
|
|
|
|
public void PlayRandomAudioClip()
|
|
|
|
|
|
{
|
2025-11-05 21:34:21 +08:00
|
|
|
|
if (audioProbability.Count > 0)
|
2025-11-02 11:43:21 +08:00
|
|
|
|
{
|
2025-11-04 17:05:34 +08:00
|
|
|
|
var randomIndex = Random.Range(0, totalWeight);
|
2025-11-05 21:34:21 +08:00
|
|
|
|
foreach (var f in audioProbability)
|
2025-11-04 17:05:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
randomIndex -= f.Value;
|
|
|
|
|
|
if (randomIndex <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
AudioManager.PostCustomSFX(Path.Combine(audioFolderPath, f.Key));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-02 11:43:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("Mod Feedback: No audio clips loaded to play.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-05 21:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
public void InitializeSetting()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!Api.ModConfigAPI.Initialize())
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var audio in audioProbability)
|
|
|
|
|
|
{
|
|
|
|
|
|
ModConfigAPI.SafeAddInputWithSlider(MOD_SETTING_NAME, audio.Key, $"音频\"{audio.Key}\"播放概率",
|
|
|
|
|
|
typeof(float), audio.Value, new Vector2(0, 100));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (DamageFeature value in Enum.GetValues(typeof(DamageFeature)))
|
|
|
|
|
|
{
|
|
|
|
|
|
ModConfigAPI.SafeAddBoolDropdownList(MOD_SETTING_NAME, value.ToString(),
|
|
|
|
|
|
$"受到{ToSingleFeatureChineseString(value)}时触发",
|
|
|
|
|
|
config.audioDamageFeatures.Contains(value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var hotkeyOptions = GenerateCommonKeyCodeOptions();
|
|
|
|
|
|
ModConfigAPI.SafeAddDropdownList(
|
|
|
|
|
|
MOD_SETTING_NAME,
|
|
|
|
|
|
"hotKey",
|
|
|
|
|
|
"主动触发的热键",
|
|
|
|
|
|
hotkeyOptions,
|
|
|
|
|
|
typeof(int),
|
|
|
|
|
|
config.hotKey
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ModConfigAPI.SafeAddOnOptionsChangedDelegate(OnConfigChange);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void OnConfigChange(string key)
|
|
|
|
|
|
{
|
|
|
|
|
|
key = key[(MOD_SETTING_NAME.Length + 1)..];
|
|
|
|
|
|
if (key == "hotKey")
|
|
|
|
|
|
{
|
|
|
|
|
|
config.hotKey = (KeyCode)ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, (int)(config.hotKey));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (audioProbability.ContainsKey(key))
|
|
|
|
|
|
{
|
|
|
|
|
|
var value = ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, audioProbability[key]);
|
|
|
|
|
|
audioProbability[key] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Enum.TryParse(key, out DamageFeature damageInfo))
|
|
|
|
|
|
{
|
|
|
|
|
|
var current=config.audioDamageFeatures.Contains(damageInfo);
|
|
|
|
|
|
if (ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, current))
|
|
|
|
|
|
{
|
|
|
|
|
|
config.audioDamageFeatures.Add(damageInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (current)
|
|
|
|
|
|
{
|
|
|
|
|
|
config.audioDamageFeatures.Remove(damageInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UpdateTotalWeight();
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成包含常用 KeyCode 的 SortedDictionary。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>一个 SortedDictionary,键是 KeyCode 的字符串表示,值是 KeyCode 枚举本身。</returns>
|
|
|
|
|
|
private static SortedDictionary<string, object> GenerateCommonKeyCodeOptions()
|
|
|
|
|
|
{
|
|
|
|
|
|
var options = new SortedDictionary<string, object>();
|
|
|
|
|
|
// 字母键
|
|
|
|
|
|
for (var c = 'A'; c <= 'Z'; c++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), c.ToString());
|
|
|
|
|
|
options.Add(c.ToString(), keyCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数字键(主键盘)
|
|
|
|
|
|
for (var i = 0; i <= 9; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "Alpha" + i.ToString());
|
|
|
|
|
|
options.Add(i.ToString(), keyCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数字键盘
|
|
|
|
|
|
for (var i = 0; i <= 9; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "Keypad" + i.ToString());
|
|
|
|
|
|
options.Add($"Num_{i}", keyCode); // 加前缀区分主键盘数字
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 功能键
|
|
|
|
|
|
for (var i = 1; i <= 12; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "F" + i.ToString());
|
|
|
|
|
|
options.Add($"F{i}", keyCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 常用控制键
|
|
|
|
|
|
options.Add("空格", (int)KeyCode.Space);
|
|
|
|
|
|
options.Add("回车", (int)KeyCode.Return);
|
|
|
|
|
|
options.Add("Esc", (int)KeyCode.Escape);
|
|
|
|
|
|
options.Add("Shift (左)", (int)KeyCode.LeftShift);
|
|
|
|
|
|
options.Add("Shift (右)", (int)KeyCode.RightShift);
|
|
|
|
|
|
options.Add("Ctrl (左)", (int)KeyCode.LeftControl);
|
|
|
|
|
|
options.Add("Ctrl (右)", (int)KeyCode.RightControl);
|
|
|
|
|
|
options.Add("Alt (左)", (int)KeyCode.LeftAlt);
|
|
|
|
|
|
options.Add("Alt (右)", (int)KeyCode.RightAlt);
|
|
|
|
|
|
options.Add("Tab", (int)KeyCode.Tab);
|
|
|
|
|
|
options.Add("Backspace", (int)KeyCode.Backspace);
|
|
|
|
|
|
options.Add("Delete", (int)KeyCode.Delete);
|
|
|
|
|
|
options.Add("Home", (int)KeyCode.Home);
|
|
|
|
|
|
options.Add("End", (int)KeyCode.End);
|
|
|
|
|
|
options.Add("PageUp", (int)KeyCode.PageUp);
|
|
|
|
|
|
options.Add("PageDown", (int)KeyCode.PageDown);
|
|
|
|
|
|
options.Add("插入", (int)KeyCode.Insert);
|
|
|
|
|
|
// 方向键
|
|
|
|
|
|
options.Add("向上", (int)KeyCode.UpArrow);
|
|
|
|
|
|
options.Add("向下", (int)KeyCode.DownArrow);
|
|
|
|
|
|
options.Add("向左", (int)KeyCode.LeftArrow);
|
|
|
|
|
|
options.Add("向右", (int)KeyCode.RightArrow);
|
|
|
|
|
|
// 鼠标按键
|
|
|
|
|
|
options.Add("鼠标左键", (int)KeyCode.Mouse0);
|
|
|
|
|
|
options.Add("鼠标右键", (int)KeyCode.Mouse1);
|
|
|
|
|
|
options.Add("鼠标中键", (int)KeyCode.Mouse2);
|
|
|
|
|
|
// 其他一些常用键
|
|
|
|
|
|
options.Add("~", (int)KeyCode.BackQuote);
|
|
|
|
|
|
options.Add("-", (int)KeyCode.Minus);
|
|
|
|
|
|
options.Add("=", (int)KeyCode.Equals);
|
|
|
|
|
|
options.Add("[", (int)KeyCode.LeftBracket);
|
|
|
|
|
|
options.Add("]", (int)KeyCode.RightBracket);
|
|
|
|
|
|
options.Add("\\", (int)KeyCode.Backslash);
|
|
|
|
|
|
options.Add(";", (int)KeyCode.Semicolon);
|
|
|
|
|
|
options.Add("'", (int)KeyCode.Quote);
|
|
|
|
|
|
options.Add(",", (int)KeyCode.Comma);
|
|
|
|
|
|
options.Add(".", (int)KeyCode.Period);
|
|
|
|
|
|
options.Add("/", (int)KeyCode.Slash);
|
|
|
|
|
|
return options;
|
|
|
|
|
|
}
|
|
|
|
|
|
private static string ToSingleFeatureChineseString(DamageFeature feature)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (feature)
|
|
|
|
|
|
{
|
|
|
|
|
|
case DamageFeature.Undefined:
|
|
|
|
|
|
return "未指定特性";
|
|
|
|
|
|
case DamageFeature.NormalDamage:
|
|
|
|
|
|
return "普通伤害";
|
|
|
|
|
|
case DamageFeature.RealDamage:
|
|
|
|
|
|
return "真实伤害";
|
|
|
|
|
|
case DamageFeature.BuffOrEffectDamage:
|
|
|
|
|
|
return "增益/效果伤害";
|
|
|
|
|
|
case DamageFeature.ArmorIgnoringDamage:
|
|
|
|
|
|
return "无视护甲伤害";
|
|
|
|
|
|
case DamageFeature.CriticalDamage:
|
|
|
|
|
|
return "暴击伤害";
|
|
|
|
|
|
case DamageFeature.ArmorPiercingDamage:
|
|
|
|
|
|
return "护甲穿透伤害";
|
|
|
|
|
|
case DamageFeature.ExplosionDamage:
|
|
|
|
|
|
return "爆炸伤害";
|
|
|
|
|
|
case DamageFeature.ArmorBreakingDamage:
|
|
|
|
|
|
return "护甲破坏伤害";
|
|
|
|
|
|
case DamageFeature.ElementalDamage:
|
|
|
|
|
|
return "元素伤害";
|
|
|
|
|
|
case DamageFeature.OnHitBuffApply:
|
|
|
|
|
|
return "命中附加增益";
|
|
|
|
|
|
case DamageFeature.OnHitBleed:
|
|
|
|
|
|
return "命中附加流血";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "未知特性"; // 处理未定义或将来添加的特性
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-01 23:26:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|