diff --git a/Helpers/ColorHelper.cs b/Helpers/ColorHelper.cs new file mode 100644 index 000000000..bbe650ee1 --- /dev/null +++ b/Helpers/ColorHelper.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace TownOfHost; + +public static class ColorHelper +{ + /// 蛍光マーカーのような色合いの透過色に変換する + /// 最大明度にするかどうか.黒っぽい色を黒っぽいままにしたい場合はfalse + public static Color ToMarkingColor(this Color color, bool bright = true) + { + Color.RGBToHSV(color, out var h, out _, out var v); + var markingColor = Color.HSVToRGB(h, MarkerSat, bright ? MarkerVal : v).SetAlpha(MarkerAlpha); + return markingColor; + } + + /// マーカー色のS値 = 彩度 + private const float MarkerSat = 1f; + /// マーカー色のV値 = 明度 + private const float MarkerVal = 1f; + /// マーカー色のアルファ = 不透明度 + private const float MarkerAlpha = 0.2f; +} diff --git a/Helpers/CustomRolesHelper.cs b/Helpers/CustomRolesHelper.cs index 93fd68afa..42a64f6e0 100644 --- a/Helpers/CustomRolesHelper.cs +++ b/Helpers/CustomRolesHelper.cs @@ -22,10 +22,7 @@ public static bool IsMadmate(this CustomRoles role) var roleInfo = role.GetRoleInfo(); if (roleInfo != null) return roleInfo.CustomRoleType == CustomRoleTypes.Madmate; - return - role is - CustomRoles.SKMadmate or - CustomRoles.MSchrodingerCat; + return role == CustomRoles.SKMadmate; } public static bool IsImpostorTeam(this CustomRoles role) => role.IsImpostor() || role.IsMadmate(); public static bool IsNeutral(this CustomRoles role) @@ -33,13 +30,7 @@ public static bool IsNeutral(this CustomRoles role) var roleInfo = role.GetRoleInfo(); if (roleInfo != null) return roleInfo.CustomRoleType == CustomRoleTypes.Neutral; - return - role is - CustomRoles.SchrodingerCat or - CustomRoles.EgoSchrodingerCat or - CustomRoles.JSchrodingerCat or - CustomRoles.HASTroll or - CustomRoles.HASFox; + return role is CustomRoles.HASTroll or CustomRoles.HASFox; } public static bool IsCrewmate(this CustomRoles role) => role.GetRoleInfo()?.CustomRoleType == CustomRoleTypes.Crewmate || (!role.IsImpostorTeam() && !role.IsNeutral()); public static bool IsVanilla(this CustomRoles role) @@ -52,15 +43,6 @@ CustomRoles.GuardianAngel or CustomRoles.Impostor or CustomRoles.Shapeshifter; } - public static bool IsKilledSchrodingerCat(this CustomRoles role) - { - return role is - CustomRoles.SchrodingerCat or - CustomRoles.MSchrodingerCat or - CustomRoles.CSchrodingerCat or - CustomRoles.EgoSchrodingerCat or - CustomRoles.JSchrodingerCat; - } public static CustomRoleTypes GetCustomRoleTypes(this CustomRoles role) { diff --git a/Helpers/StringHelper.cs b/Helpers/StringHelper.cs new file mode 100644 index 000000000..d41d20da5 --- /dev/null +++ b/Helpers/StringHelper.cs @@ -0,0 +1,25 @@ +using System.Text; +using UnityEngine; + +namespace TownOfHost; + +public static class StringHelper +{ + public static readonly Encoding shiftJIS = CodePagesEncodingProvider.Instance.GetEncoding("Shift_JIS"); + + /// 蛍光マーカーのような装飾をする + /// 文字列 + /// 元の色 自動で半透明の蛍光色に変換される + /// 最大明度にするかどうか.黒っぽい色を黒っぽいままにしたい場合はfalse + /// マーキング済文字列 + public static string Mark(this string self, Color color, bool bright = true) + { + var markingColor = color.ToMarkingColor(bright); + var markingColorCode = ColorUtility.ToHtmlStringRGBA(markingColor); + return $"{self}"; + } + /// + /// SJISでのバイト数を計算する + /// + public static int GetByteCount(this string self) => shiftJIS.GetByteCount(self); +} diff --git a/Modules/AdminProvider.cs b/Modules/AdminProvider.cs new file mode 100644 index 000000000..a7dbe8d19 --- /dev/null +++ b/Modules/AdminProvider.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using TownOfHost.Roles.Core; +using UnityEngine; + +namespace TownOfHost.Modules; + +public static class AdminProvider +{ + // ref: MapCountOverlay.Update + /// + /// 実行された時点でのアドミン情報を取得する + /// + /// Key: 部屋のSystemType, Value: で,Key順にソートされた辞書 + public static SortedDictionary CalculateAdmin() + { + SortedDictionary allAdmins = new(); + // 既にカウントされた人のPlayerIdを格納する + // これに追加しようとしたときにfalseが返ってきたらカウントしないようにすることで,各プレイヤーが1回しかカウントされないようになっている + HashSet countedPlayers = new(15); + // 検出された当たり判定の格納用に使い回す配列 変換時の負荷を回避するためIl2CppReferenceArrayで扱う + Il2CppReferenceArray colliders = new(45); + // ref: MapCountOverlay.Awake + ContactFilter2D filter = new() + { + useLayerMask = true, + layerMask = Constants.LivingPlayersOnlyMask, + useTriggers = true, + }; + + // 各部屋の人数カウント処理 + foreach (var room in ShipStatus.Instance.AllRooms) + { + var roomId = room.RoomId; + // 通路か当たり判定がないなら何もしない + if (roomId == SystemTypes.Hallway || room.roomArea == null) + { + continue; + } + // 検出された当たり判定の数 検出された当たり判定はここでcollidersに格納される + var numColliders = room.roomArea.OverlapCollider(filter, colliders); + // 実際にアドミンで表示される,死体も含めた全部の数 + var totalPlayers = 0; + // 死体の数 + var numDeadBodies = 0; + // インポスターの数 + var numImpostors = 0; + + // 検出された各当たり判定への処理 + for (var i = 0; i < numColliders; i++) + { + var collider = colliders[i]; + // おにくの場合 + if (collider.CompareTag("DeadBody")) + { + var deadBody = collider.GetComponent(); + if (deadBody != null && countedPlayers.Add(deadBody.ParentId)) + { + totalPlayers++; + numDeadBodies++; + // インポスターの死体だった場合 + if (Utils.GetPlayerById(deadBody.ParentId)?.Is(CustomRoleTypes.Impostor) == true) + { + numImpostors++; + } + } + } + // 生きてる場合 + else if (!collider.isTrigger) + { + var playerControl = collider.GetComponent(); + if (playerControl.IsAlive() && countedPlayers.Add(playerControl.PlayerId)) + { + totalPlayers++; + // インポスターだった場合 + if (playerControl.Is(CustomRoleTypes.Impostor)) + { + numImpostors++; + } + } + } + } + + allAdmins[roomId] = new() + { + Room = roomId, + TotalPlayers = totalPlayers, + NumDeadBodies = numDeadBodies, + NumImpostors = numImpostors, + }; + } + return allAdmins; + } + + public readonly record struct AdminEntry + { + /// 対象の部屋 + public SystemTypes Room { get; init; } + /// 部屋の中にいるプレイヤーの合計 + public int TotalPlayers { get; init; } + /// 部屋の中にある死体の数 + public int NumDeadBodies { get; init; } + /// 部屋の中にインポスターがいるかどうか + public int NumImpostors { get; init; } + } +} diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index 75db6e4e9..ff6419124 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using AmongUs.GameOptions; using Hazel; using TownOfHost.Attributes; @@ -15,32 +14,8 @@ public static class AntiBlackout /// ///追放処理を上書きするかどうか /// - public static bool OverrideExiledPlayer => IsRequired && (IsSingleImpostor || Diff_CrewImp == 1); - /// - ///インポスターが一人しか存在しない設定かどうか - /// - public static bool IsSingleImpostor => Main.RealOptionsData != null ? Main.RealOptionsData.GetInt(Int32OptionNames.NumImpostors) == 1 : Main.NormalOptions.NumImpostors == 1; - /// - ///AntiBlackout内の処理が必要であるかどうか - /// - public static bool IsRequired => Options.NoGameEnd.GetBool() || Jackal.RoleInfo.IsEnable; - /// - ///インポスター以外の人数とインポスターの人数の差 - /// - public static int Diff_CrewImp - { - get - { - int numImpostors = 0; - int numCrewmates = 0; - foreach (var pc in Main.AllPlayerControls) - { - if (pc.Data.Role.IsImpostor) numImpostors++; - else numCrewmates++; - } - return numCrewmates - numImpostors; - } - } + public static bool OverrideExiledPlayer => Options.NoGameEnd.GetBool() || Jackal.RoleInfo.IsEnable; + public static bool IsCached { get; private set; } = false; private static Dictionary isDeadCache = new(); private readonly static LogHandler logger = Logger.Handler("AntiBlackout"); diff --git a/Modules/DebugModeManager.cs b/Modules/DebugModeManager.cs index 2d82d5fff..c74efd8bd 100644 --- a/Modules/DebugModeManager.cs +++ b/Modules/DebugModeManager.cs @@ -26,7 +26,14 @@ public static void SetupCustomOption() { EnableDebugMode = BooleanOptionItem.Create(2, "EnableDebugMode", false, TabGroup.MainSettings, true) .SetColor(Color.green) - .SetHidden(!AmDebugger); + .SetHidden(!AmDebugger) + .RegisterUpdateValueEvent((obj, args) => + { + if (DestroyableSingleton.InstanceExists && Main.NormalOptions.NumImpostors == 0 && AmongUsClient.Instance.AmHost && !EnableDebugMode.GetBool()) + { + Main.NormalOptions.NumImpostors = 1; + } + }); } } } \ No newline at end of file diff --git a/Modules/DoorsReset.cs b/Modules/DoorsReset.cs new file mode 100644 index 000000000..c377f273e --- /dev/null +++ b/Modules/DoorsReset.cs @@ -0,0 +1,95 @@ +using TownOfHost.Attributes; + +namespace TownOfHost.Modules; + +public static class DoorsReset +{ + private static bool isEnabled = false; + private static ResetMode mode; + private static DoorsSystemType DoorsSystem => ShipStatus.Instance.Systems.TryGetValue(SystemTypes.Doors, out var system) ? system.TryCast() : null; + private static readonly LogHandler logger = Logger.Handler(nameof(DoorsReset)); + + [GameModuleInitializer] + public static void Initialize() + { + // AirshipとPolus以外は非対応 + if ((MapNames)Main.NormalOptions.MapId is not (MapNames.Airship or MapNames.Polus)) + { + isEnabled = false; + return; + } + isEnabled = Options.ResetDoorsEveryTurns.GetBool(); + mode = (ResetMode)Options.DoorsResetMode.GetValue(); + logger.Info($"初期化: [ {isEnabled}, {mode} ]"); + } + + /// 設定に応じてドア状況をリセット + public static void ResetDoors() + { + if (!isEnabled || DoorsSystem == null) + { + return; + } + logger.Info("リセット"); + + switch (mode) + { + case ResetMode.AllOpen: OpenAllDoors(); break; + case ResetMode.AllClosed: CloseAllDoors(); break; + case ResetMode.RandomByDoor: OpenOrCloseAllDoorsRandomly(); break; + default: logger.Warn($"無効なモード: {mode}"); break; + } + } + /// マップ上の全ドアを開放 + private static void OpenAllDoors() + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + SetDoorOpenState(door, true); + } + DoorsSystem.IsDirty = true; + } + /// マップ上の全ドアを閉鎖 + private static void CloseAllDoors() + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + SetDoorOpenState(door, false); + } + DoorsSystem.IsDirty = true; + } + /// マップ上の全ドアをランダムに開閉 + private static void OpenOrCloseAllDoorsRandomly() + { + foreach (var door in ShipStatus.Instance.AllDoors) + { + var isOpen = IRandom.Instance.Next(2) > 0; + SetDoorOpenState(door, isOpen); + } + DoorsSystem.IsDirty = true; + } + + /// ドアの開閉状況を設定する.サボタージュで閉められないドアに対しては何もしない + /// 対象のドア + /// 開けるならtrue,閉めるならfalse + private static void SetDoorOpenState(PlainDoor door, bool isOpen) + { + if (IsValidDoor(door)) + { + door.SetDoorway(isOpen); + } + } + /// リセット対象のドアかどうか判定する + /// リセット対象ならtrue + private static bool IsValidDoor(PlainDoor door) + { + // エアシラウンジトイレとPolus除染室のドアは対象外 + if (door.Room is SystemTypes.Lounge or SystemTypes.Decontamination) + { + return false; + } + return true; + } + + public enum ResetMode { AllOpen, AllClosed, RandomByDoor, } +} diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs index 874f7eed3..0f499ae88 100644 --- a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -7,7 +7,6 @@ using Mathf = UnityEngine.Mathf; using TownOfHost.Roles.Core; -using TownOfHost.Roles.Neutral; namespace TownOfHost.Modules { @@ -97,15 +96,6 @@ public override IGameOptions BuildGameOptions() var roleClass = player.GetRoleClass(); roleClass?.ApplyGameOptions(opt); - switch (role) - { - case CustomRoles.EgoSchrodingerCat: - opt.SetVision(true); - break; - case CustomRoles.JSchrodingerCat: - ((Jackal)roleClass).ApplyGameOptions(opt); - break; - } foreach (var subRole in player.GetCustomSubRoles()) { switch (subRole) diff --git a/Modules/GameState.cs b/Modules/GameState.cs index f768d3d71..15497a438 100644 --- a/Modules/GameState.cs +++ b/Modules/GameState.cs @@ -19,6 +19,16 @@ public class PlayerState public CustomDeathReason DeathReason { get; set; } public TaskState taskState; public bool IsBlackOut { get; set; } + private bool _canUseMovingPlatform = true; + public bool CanUseMovingPlatform + { + get => _canUseMovingPlatform; + set + { + Logger.Info($"ID: {PlayerId} の昇降機可用性を {value} に設定", nameof(PlayerState)); + _canUseMovingPlatform = value; + } + } public (DateTime, byte) RealKiller; public PlainShipRoom LastRoom; public Dictionary TargetColorData; @@ -174,6 +184,8 @@ public void Update(PlayerControl player) CompletedTasksCount = Math.Min(AllTasksCount, CompletedTasksCount); Logger.Info($"{player.GetNameWithRole()}: TaskCounts = {CompletedTasksCount}/{AllTasksCount}", "TaskState.Update"); } + public bool HasCompletedEnoughCountOfTasks(int count) => + IsTaskFinished || CompletedTasksCount >= count; } public class PlayerVersion { diff --git a/Modules/MeetingVoteManager.cs b/Modules/MeetingVoteManager.cs index d0c7f9a7e..06e701ac1 100644 --- a/Modules/MeetingVoteManager.cs +++ b/Modules/MeetingVoteManager.cs @@ -51,12 +51,13 @@ public void ClearAndExile(byte voter, byte exiled) EndMeeting(false); } /// - /// 投票を追加します + /// 投票を行います.投票者が既に投票している場合は票を上書きします /// /// 投票者 /// 投票先 /// 票数 - public void AddVote(byte voter, byte voteFor, int numVotes = 1) + /// 投票者自身の投票操作による自発的な投票かどうか + public void SetVote(byte voter, byte voteFor, int numVotes = 1, bool isIntentional = true) { if (!allVotes.TryGetValue(voter, out var vote)) { @@ -71,7 +72,7 @@ public void AddVote(byte voter, byte voteFor, int numVotes = 1) bool doVote = true; foreach (var role in CustomRoleManager.AllActiveRoles.Values) { - var (roleVoteFor, roleNumVotes, roleDoVote) = role.OnVote(voter, voteFor); + var (roleVoteFor, roleNumVotes, roleDoVote) = role.ModifyVote(voter, voteFor, isIntentional); if (roleVoteFor.HasValue) { logger.Info($"{role.Player.GetNameWithRole()} が {Utils.GetPlayerById(voter).GetNameWithRole()} の投票先を {GetVoteName(roleVoteFor.Value)} に変更します"); @@ -181,7 +182,7 @@ public VoteResult CountVotes(bool applyVoteMode) return new VoteResult(votes); } /// - /// スキップモードと無投票モードに応じて,投票を変更したりプレイヤーを死亡させたりします + /// スキップモードと無投票モードに応じて,投票を上書きしたりプレイヤーを死亡させたりします /// private void ApplySkipAndNoVoteMode() { @@ -205,11 +206,11 @@ private void ApplySkipAndNoVoteMode() logger.Info($"無投票のため {voterName} を自殺させます"); break; case VoteMode.SelfVote: - vote.ChangeVoteTarget(vote.Voter); + SetVote(vote.Voter, vote.Voter, isIntentional: false); logger.Info($"無投票のため {voterName} に自投票させます"); break; case VoteMode.Skip: - vote.ChangeVoteTarget(Skip); + SetVote(vote.Voter, Skip, isIntentional: false); logger.Info($"無投票のため {voterName} にスキップさせます"); break; } @@ -224,7 +225,7 @@ private void ApplySkipAndNoVoteMode() logger.Info($"スキップしたため {voterName} を自殺させます"); break; case VoteMode.SelfVote: - vote.ChangeVoteTarget(vote.Voter); + SetVote(vote.Voter, vote.Voter, isIntentional: false); logger.Info($"スキップしたため {voterName} に自投票させます"); break; } @@ -263,11 +264,6 @@ public void DoVote(byte voteTo, int numVotes) VotedFor = voteTo; NumVotes = numVotes; } - public void ChangeVoteTarget(byte voteTarget) - { - logger.Info($"{Utils.GetPlayerById(Voter).GetNameWithRole()}の投票を{GetVoteName(VotedFor)}から{GetVoteName(voteTarget)}に変更"); - VotedFor = voteTarget; - } } public readonly struct VoteResult diff --git a/Modules/ModUpdater.cs b/Modules/ModUpdater.cs index 2585bd0b3..079fafe13 100644 --- a/Modules/ModUpdater.cs +++ b/Modules/ModUpdater.cs @@ -177,7 +177,11 @@ private static void ShowPopup(string message, bool showButton = false) button.gameObject.SetActive(showButton); button.GetComponentInChildren().TargetText = StringNames.QuitLabel; button.GetComponent().OnClick = new(); - button.GetComponent().OnClick.AddListener((Action)(() => Application.Quit())); + button.GetComponent().OnClick.AddListener((Action)(() => + { + Application.OpenURL("https://github.com/tukasa0001/TownOfHost/releases/latest"); + Application.Quit(); + })); } } } diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index cbf65c538..64f9dfc0e 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -5,6 +5,7 @@ using HarmonyLib; using UnityEngine; +using TownOfHost.Modules; using TownOfHost.Roles; using TownOfHost.Roles.Core; using TownOfHost.Roles.AddOns.Common; @@ -37,8 +38,6 @@ public static void WaitOptionsLoad() taskOptionsLoad.Wait(); Logger.Info("Options.Load End", "Options"); } - // オプションId - public const int PresetId = 0; // プリセット private static readonly string[] presets = @@ -188,16 +187,23 @@ public static CustomGameMode CurrentGameMode public static OptionItem PolusReactorTimeLimit; public static OptionItem AirshipReactorTimeLimit; + // サボタージュのクールダウン変更 + public static OptionItem ModifySabotageCooldown; + public static OptionItem SabotageCooldown; + // 停電の特殊設定 public static OptionItem LightsOutSpecialSettings; public static OptionItem DisableAirshipViewingDeckLightsPanel; public static OptionItem DisableAirshipGapRoomLightsPanel; public static OptionItem DisableAirshipCargoLightsPanel; + public static OptionItem BlockDisturbancesToSwitches; // マップ改造 public static OptionItem MapModification; public static OptionItem AirShipVariableElectrical; public static OptionItem DisableAirshipMovingPlatform; + public static OptionItem ResetDoorsEveryTurns; + public static OptionItem DoorsResetMode; // その他 public static OptionItem FixFirstKillCooldown; @@ -260,6 +266,8 @@ public static int GetRoleChance(CustomRoles role) public static void Load() { if (IsLoaded) return; + OptionSaver.Initialize(); + // プリセット _ = PresetOptionItem.Create(0, TabGroup.MainSettings) .SetColor(new Color32(204, 204, 0, 255)) @@ -286,7 +294,7 @@ public static void Load() // Impostor sortedRoleInfo.Where(role => role.CustomRoleType == CustomRoleTypes.Impostor).Do(info => { - SetupRoleOptions(info.ConfigId, info.Tab, info.RoleName); + SetupRoleOptions(info); info.OptionCreator?.Invoke(); }); @@ -296,10 +304,10 @@ public static void Load() CanMakeMadmateCount = IntegerOptionItem.Create(5012, "CanMakeMadmateCount", new(0, 15, 1), 0, TabGroup.ImpostorRoles, false) .SetValueFormat(OptionFormat.Times); - // Madmate - sortedRoleInfo.Where(role => role.CustomRoleType == CustomRoleTypes.Madmate).Do(info => + // Madmate, Crewmate, Neutral + sortedRoleInfo.Where(role => role.CustomRoleType != CustomRoleTypes.Impostor).Do(info => { - SetupRoleOptions(info.ConfigId, info.Tab, info.RoleName); + SetupRoleOptions(info); info.OptionCreator?.Invoke(); }); // Madmate Common Options @@ -315,30 +323,9 @@ public static void Load() .SetValueFormat(OptionFormat.Seconds); MadmateVentMaxTime = FloatOptionItem.Create(15214, "MadmateVentMaxTime", new(0f, 180f, 5f), 0f, TabGroup.ImpostorRoles, false) .SetValueFormat(OptionFormat.Seconds); - // Crewmate - sortedRoleInfo.Where(role => role.CustomRoleType == CustomRoleTypes.Crewmate).Do(info => - { - SetupRoleOptions(info.ConfigId, info.Tab, info.RoleName); - info.OptionCreator?.Invoke(); - }); - - // Neutral - sortedRoleInfo.Where(role => role.CustomRoleType == CustomRoleTypes.Neutral).Do(info => - { - switch (info.RoleName) - { - case CustomRoles.Jackal: //ジャッカルは1人固定 - SetupSingleRoleOptions(info.ConfigId, info.Tab, info.RoleName, 1); - break; - default: - SetupRoleOptions(info.ConfigId, info.Tab, info.RoleName); - break; - } - info.OptionCreator?.Invoke(); - }); - SetupLoversRoleOptionsToggle(50300); // Add-Ons + SetupRoleOptions(50300, TabGroup.Addons, CustomRoles.Lovers, assignCountRule: new(2, 2, 2)); LastImpostor.SetupCustomOption(); Watcher.SetupCustomOption(); Workhorse.SetupCustomOption(); @@ -350,8 +337,8 @@ public static void Load() .SetGameMode(CustomGameMode.Standard); // HideAndSeek - SetupRoleOptions(100000, TabGroup.MainSettings, CustomRoles.HASFox, CustomGameMode.HideAndSeek); - SetupRoleOptions(100100, TabGroup.MainSettings, CustomRoles.HASTroll, CustomGameMode.HideAndSeek); + SetupRoleOptions(100000, TabGroup.MainSettings, CustomRoles.HASFox, customGameMode: CustomGameMode.HideAndSeek); + SetupRoleOptions(100100, TabGroup.MainSettings, CustomRoles.HASTroll, customGameMode: CustomGameMode.HideAndSeek); AllowCloseDoors = BooleanOptionItem.Create(101000, "AllowCloseDoors", false, TabGroup.MainSettings, false) .SetHeader(true) .SetGameMode(CustomGameMode.HideAndSeek); @@ -374,6 +361,13 @@ public static void Load() .SetValueFormat(OptionFormat.Seconds) .SetGameMode(CustomGameMode.Standard); + // サボタージュのクールダウン変更 + ModifySabotageCooldown = BooleanOptionItem.Create(100810, "ModifySabotageCooldown", false, TabGroup.MainSettings, false) + .SetGameMode(CustomGameMode.Standard); + SabotageCooldown = FloatOptionItem.Create(100811, "SabotageCooldown", new(1f, 60f, 1f), 30f, TabGroup.MainSettings, false).SetParent(ModifySabotageCooldown) + .SetValueFormat(OptionFormat.Seconds) + .SetGameMode(CustomGameMode.Standard); + // 停電の特殊設定 LightsOutSpecialSettings = BooleanOptionItem.Create(101500, "LightsOutSpecialSettings", false, TabGroup.MainSettings, false) .SetGameMode(CustomGameMode.Standard); @@ -383,12 +377,16 @@ public static void Load() .SetGameMode(CustomGameMode.Standard); DisableAirshipCargoLightsPanel = BooleanOptionItem.Create(101513, "DisableAirshipCargoLightsPanel", false, TabGroup.MainSettings, false).SetParent(LightsOutSpecialSettings) .SetGameMode(CustomGameMode.Standard); + BlockDisturbancesToSwitches = BooleanOptionItem.Create(101514, "BlockDisturbancesToSwitches", false, TabGroup.MainSettings, false).SetParent(LightsOutSpecialSettings) + .SetGameMode(CustomGameMode.Standard); // マップ改造 MapModification = BooleanOptionItem.Create(102000, "MapModification", false, TabGroup.MainSettings, false) .SetHeader(true); AirShipVariableElectrical = BooleanOptionItem.Create(101600, "AirShipVariableElectrical", false, TabGroup.MainSettings, false).SetParent(MapModification); DisableAirshipMovingPlatform = BooleanOptionItem.Create(101700, "DisableAirshipMovingPlatform", false, TabGroup.MainSettings, false).SetParent(MapModification); + ResetDoorsEveryTurns = BooleanOptionItem.Create(101800, "ResetDoorsEveryTurns", false, TabGroup.MainSettings, false).SetParent(MapModification); + DoorsResetMode = StringOptionItem.Create(101810, "DoorsResetMode", EnumHelper.GetAllNames(), 0, TabGroup.MainSettings, false).SetParent(ResetDoorsEveryTurns); // タスク無効化 DisableTasks = BooleanOptionItem.Create(100300, "DisableTasks", false, TabGroup.MainSettings, false) @@ -577,53 +575,31 @@ public static void Load() DebugModeManager.SetupCustomOption(); + OptionSaver.Load(); + IsLoaded = true; } - public static void SetupRoleOptions(int id, TabGroup tab, CustomRoles role, CustomGameMode customGameMode = CustomGameMode.Standard) + public static void SetupRoleOptions(SimpleRoleInfo info) => + SetupRoleOptions(info.ConfigId, info.Tab, info.RoleName, info.AssignCountRule); + public static void SetupRoleOptions(int id, TabGroup tab, CustomRoles role, IntegerValueRule assignCountRule = null, CustomGameMode customGameMode = CustomGameMode.Standard) { if (role.IsVanilla()) return; + assignCountRule ??= new(1, 15, 1); - var spawnOption = IntegerOptionItem.Create(id, role.ToString(), new(0, 100, 10), 0, tab, false).SetColor(Utils.GetRoleColor(role)) + var spawnOption = IntegerOptionItem.Create(id, role.ToString(), new(0, 100, 10), 0, tab, false) + .SetColor(Utils.GetRoleColor(role)) .SetValueFormat(OptionFormat.Percent) .SetHeader(true) .SetGameMode(customGameMode) as IntegerOptionItem; - var countOption = IntegerOptionItem.Create(id + 1, "Maximum", new(1, 15, 1), 1, tab, false).SetParent(spawnOption) + var countOption = IntegerOptionItem.Create(id + 1, "Maximum", assignCountRule, assignCountRule.Step, tab, false) + .SetParent(spawnOption) .SetValueFormat(OptionFormat.Players) .SetGameMode(customGameMode); CustomRoleSpawnChances.Add(role, spawnOption); CustomRoleCounts.Add(role, countOption); } - private static void SetupLoversRoleOptionsToggle(int id, CustomGameMode customGameMode = CustomGameMode.Standard) - { - var role = CustomRoles.Lovers; - var spawnOption = IntegerOptionItem.Create(id, role.ToString(), new(0, 100, 10), 0, TabGroup.Addons, false).SetColor(Utils.GetRoleColor(role)) - .SetValueFormat(OptionFormat.Percent) - .SetHeader(true) - .SetGameMode(customGameMode) as IntegerOptionItem; - - var countOption = IntegerOptionItem.Create(id + 1, "NumberOfLovers", new(2, 2, 1), 2, TabGroup.Addons, false).SetParent(spawnOption) - .SetHidden(true) - .SetGameMode(customGameMode); - - CustomRoleSpawnChances.Add(role, spawnOption); - CustomRoleCounts.Add(role, countOption); - } - public static void SetupSingleRoleOptions(int id, TabGroup tab, CustomRoles role, int count, CustomGameMode customGameMode = CustomGameMode.Standard) - { - var spawnOption = IntegerOptionItem.Create(id, role.ToString(), new(0, 100, 10), 0, tab, false).SetColor(Utils.GetRoleColor(role)) - .SetValueFormat(OptionFormat.Percent) - .SetHeader(true) - .SetGameMode(customGameMode) as IntegerOptionItem; - // 初期値,最大値,最小値が同じで、stepが0のどうやっても変えることができない個数オプション - var countOption = IntegerOptionItem.Create(id + 1, "Maximum", new(count, count, count), count, tab, false).SetParent(spawnOption) - .SetHidden(true) - .SetGameMode(customGameMode); - - CustomRoleSpawnChances.Add(role, spawnOption); - CustomRoleCounts.Add(role, countOption); - } public class OverrideTasksData { public static Dictionary AllData = new(); diff --git a/Modules/OptionItem/OptionItem.cs b/Modules/OptionItem/OptionItem.cs index 737488dbd..2a1f9482d 100644 --- a/Modules/OptionItem/OptionItem.cs +++ b/Modules/OptionItem/OptionItem.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using BepInEx.Configuration; +using TownOfHost.Modules; using UnityEngine; namespace TownOfHost @@ -10,7 +10,9 @@ public abstract class OptionItem { #region static public static IReadOnlyList AllOptions => _allOptions; - private static List _allOptions = new(); + private static List _allOptions = new(1024); + public static IReadOnlyDictionary FastOptions => _fastOptions; + private static Dictionary _fastOptions = new(1024); public static int CurrentPreset { get; set; } #endregion @@ -39,22 +41,18 @@ public abstract class OptionItem private Dictionary _replacementDictionary; // 設定値情報 (オプションの値に関わる情報) + public int[] AllValues { get; private set; } = new int[NumPresets]; public int CurrentValue { get => GetValue(); set => SetValue(value); } + public int SingleValue { get; private set; } // 親子情報 public OptionItem Parent { get; private set; } public List Children; - // 内部情報 (外部から参照することを想定していない情報) - public ConfigEntry CurrentEntry => - IsSingleValue ? singleEntry : AllConfigEntries[CurrentPreset]; - private ConfigEntry[] AllConfigEntries; - private ConfigEntry singleEntry; - public OptionBehaviour OptionBehaviour; // イベント @@ -83,30 +81,32 @@ public OptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSi // オブジェクト初期化 Children = new(); - // ConfigEntry初期化 - AllConfigEntries = new ConfigEntry[5]; - if (Id == 0) + // デフォルト値に設定 + if (Id == PresetId) { - singleEntry = Main.Instance.Config.Bind("Current Preset", id.ToString(), DefaultValue); - CurrentPreset = singleEntry.Value; + SingleValue = DefaultValue; + CurrentPreset = SingleValue; } else if (IsSingleValue) { - singleEntry = Main.Instance.Config.Bind("SingleEntryOptions", id.ToString(), DefaultValue); + SingleValue = DefaultValue; } else { - for (int i = 0; i < 5; i++) + for (int i = 0; i < NumPresets; i++) { - AllConfigEntries[i] = Main.Instance.Config.Bind($"Preset{i + 1}", id.ToString(), DefaultValue); + AllValues[i] = DefaultValue; } } - if (AllOptions.Any(op => op.Id == id)) + if (_fastOptions.TryAdd(id, this)) + { + _allOptions.Add(this); + } + else { Logger.Error($"ID:{id}が重複しています", "OptionItem"); } - _allOptions.Add(this); } // Setter @@ -155,7 +155,7 @@ public virtual string GetString() { return ApplyFormat(CurrentValue.ToString()); } - public virtual int GetValue() => CurrentEntry.Value; + public virtual int GetValue() => IsSingleValue ? SingleValue : AllValues[CurrentPreset]; // 旧IsHidden関数 public virtual bool IsHiddenOn(CustomGameMode mode) @@ -179,10 +179,17 @@ public virtual void Refresh() opt.oldValue = opt.Value = CurrentValue; } } - public virtual void SetValue(int value, bool doSync = true) + public void SetValue(int afterValue, bool doSave, bool doSync = true) { - int beforeValue = CurrentEntry.Value; - int afterValue = CurrentEntry.Value = value; + int beforeValue = CurrentValue; + if (IsSingleValue) + { + SingleValue = afterValue; + } + else + { + AllValues[CurrentPreset] = afterValue; + } CallUpdateValueEvent(beforeValue, afterValue); Refresh(); @@ -190,6 +197,18 @@ public virtual void SetValue(int value, bool doSync = true) { SyncAllOptions(); } + if (doSave) + { + OptionSaver.Save(); + } + } + public virtual void SetValue(int afterValue, bool doSync = true) + { + SetValue(afterValue, true, doSync); + } + public void SetAllValues(int[] values) // プリセット読み込み専用 + { + AllValues = values; } // 演算子オーバーロード @@ -201,7 +220,7 @@ public virtual void SetValue(int value, bool doSync = true) // 全体操作用 public static void SwitchPreset(int newPreset) { - CurrentPreset = Math.Clamp(newPreset, 0, 4); + CurrentPreset = Math.Clamp(newPreset, 0, NumPresets - 1); foreach (var op in AllOptions) op.Refresh(); @@ -244,6 +263,9 @@ public UpdateValueEventArgs(int beforeValue, int currentValue) BeforeValue = beforeValue; } } + + public const int NumPresets = 5; + public const int PresetId = 0; } public enum TabGroup diff --git a/Modules/OptionItem/PresetOptionItem.cs b/Modules/OptionItem/PresetOptionItem.cs index 20003cfa9..9b62ff297 100644 --- a/Modules/OptionItem/PresetOptionItem.cs +++ b/Modules/OptionItem/PresetOptionItem.cs @@ -9,7 +9,7 @@ public class PresetOptionItem : OptionItem public PresetOptionItem(int defaultValue, TabGroup tab) : base(0, "Preset", defaultValue, tab, true) { - Rule = (0, 4, 1); + Rule = (0, NumPresets - 1, 1); } public static PresetOptionItem Create(int defaultValue, TabGroup tab) { diff --git a/Modules/OptionItem/ValueRule.cs b/Modules/OptionItem/ValueRule.cs index 9deeabcee..f44910618 100644 --- a/Modules/OptionItem/ValueRule.cs +++ b/Modules/OptionItem/ValueRule.cs @@ -69,7 +69,11 @@ public override int RepeatIndex(int value) } public override float GetValueByIndex(int index) - => RepeatIndex(index) * Step + MinValue; + { + //丸め誤差対策でdecimal型にして計算し、float型にして返す + decimal ss = RepeatIndex(index) * (decimal)Step + (decimal)MinValue; + return (float)ss; + } public override int GetNearestIndex(float num) { diff --git a/Modules/OptionSaver.cs b/Modules/OptionSaver.cs new file mode 100644 index 000000000..d27c529b7 --- /dev/null +++ b/Modules/OptionSaver.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace TownOfHost.Modules; + +public static class OptionSaver +{ + private static readonly DirectoryInfo SaveDataDirectoryInfo = new("./TOH_DATA/SaveData/"); + private static readonly FileInfo OptionSaverFileInfo = new($"{SaveDataDirectoryInfo.FullName}/Options.json"); + private static readonly LogHandler logger = Logger.Handler(nameof(OptionSaver)); + + public static void Initialize() + { + if (!SaveDataDirectoryInfo.Exists) + { + SaveDataDirectoryInfo.Create(); + SaveDataDirectoryInfo.Attributes |= FileAttributes.Hidden; + } + if (!OptionSaverFileInfo.Exists) + { + OptionSaverFileInfo.Create().Dispose(); + } + } + /// 現在のオプションからjsonシリアライズ用のオブジェクトを生成 + private static SerializableOptionsData GenerateOptionsData() + { + Dictionary singleOptions = new(); + Dictionary presetOptions = new(); + foreach (var option in OptionItem.AllOptions) + { + if (option.IsSingleValue) + { + if (!singleOptions.TryAdd(option.Id, option.SingleValue)) + { + logger.Warn($"SingleOptionのID {option.Id} が重複"); + } + } + else if (!presetOptions.TryAdd(option.Id, option.AllValues)) + { + logger.Warn($"プリセットオプションのID {option.Id} が重複"); + } + } + return new SerializableOptionsData + { + Version = Version, + SingleOptions = singleOptions, + PresetOptions = presetOptions, + }; + } + /// デシリアライズされたオブジェクトを読み込み,オプション値を設定 + private static void LoadOptionsData(SerializableOptionsData serializableOptionsData) + { + if (serializableOptionsData.Version != Version) + { + // 今後バージョン間の移行方法を用意する場合,ここでバージョンごとの変換メソッドに振り分ける + logger.Info($"読み込まれたオプションのバージョン {serializableOptionsData.Version} が現在のバージョン {Version} と一致しないためデフォルト値で上書きします"); + Save(); + return; + } + Dictionary singleOptions = serializableOptionsData.SingleOptions; + Dictionary presetOptions = serializableOptionsData.PresetOptions; + foreach (var singleOption in singleOptions) + { + var id = singleOption.Key; + var value = singleOption.Value; + if (OptionItem.FastOptions.TryGetValue(id, out var optionItem)) + { + optionItem.SetValue(value, doSave: false); + } + } + foreach (var presetOption in presetOptions) + { + var id = presetOption.Key; + var values = presetOption.Value; + if (OptionItem.FastOptions.TryGetValue(id, out var optionItem)) + { + optionItem.SetAllValues(values); + } + } + } + /// 現在のオプションをjsonファイルに保存 + public static void Save() + { + var jsonString = JsonSerializer.Serialize(GenerateOptionsData(), new JsonSerializerOptions { WriteIndented = true, }); + File.WriteAllText(OptionSaverFileInfo.FullName, jsonString); + } + /// jsonファイルからオプションを読み込み + public static void Load() + { + var jsonString = File.ReadAllText(OptionSaverFileInfo.FullName); + // 空なら読み込まず,デフォルト値をセーブする + if (jsonString.Length <= 0) + { + logger.Info("オプションデータが空のためデフォルト値を保存"); + Save(); + return; + } + LoadOptionsData(JsonSerializer.Deserialize(jsonString)); + } + + /// json保存に適したオプションデータ + public class SerializableOptionsData + { + public int Version { get; init; } + /// プリセット外のオプション + public Dictionary SingleOptions { get; init; } + /// プリセット内のオプション + public Dictionary PresetOptions { get; init; } + } + + /// オプションの形式に互換性のない変更(プリセット数変更など)を加えるときはここの数字を上げる + public static readonly int Version = 0; +} diff --git a/Modules/OptionShower.cs b/Modules/OptionShower.cs index f4337e58e..932352681 100644 --- a/Modules/OptionShower.cs +++ b/Modules/OptionShower.cs @@ -3,6 +3,7 @@ using System.Text; using UnityEngine; +using TownOfHost.Roles; using TownOfHost.Roles.Core; using static TownOfHost.Translator; @@ -27,6 +28,7 @@ public static string GetText() }; //ゲームモードの表示 sb.Append($"{Options.GameMode.GetName()}: {Options.GameMode.GetString()}\n\n"); + sb.AppendFormat("{0}: {1}\n\n", RoleAssignManager.OptionAssignMode.GetName(), RoleAssignManager.OptionAssignMode.GetString()); if (Options.HideGameSettings.GetBool() && !AmongUsClient.Instance.AmHost) { sb.Append($"{GetString("Message.HideGameSettings")}"); @@ -47,6 +49,11 @@ public static string GetText() } //有効な役職と詳細設定一覧 pages.Add(""); + if (RoleAssignManager.OptionAssignMode.GetBool()) + { + ShowChildren(RoleAssignManager.OptionAssignMode, ref sb, Color.white); + sb.Append('\n'); + } nameAndValue(Options.EnableGM); foreach (var kvp in Options.CustomRoleSpawnChances) { @@ -105,8 +112,11 @@ private static void ShowChildren(OptionItem option, ref StringBuilder sb, Color foreach (var opt in option.Children.Select((v, i) => new { Value = v, Index = i + 1 })) { if (opt.Value.Name == "Maximum") continue; //Maximumの項目は飛ばす - sb.Append(string.Concat(Enumerable.Repeat(Utils.ColorString(color, "┃"), deep - 1))); - sb.Append(Utils.ColorString(color, opt.Index == option.Children.Count ? "┗ " : "┣ ")); + if (deep > 0) + { + sb.Append(string.Concat(Enumerable.Repeat(Utils.ColorString(color, "┃"), deep - 1))); + sb.Append(Utils.ColorString(color, opt.Index == option.Children.Count ? "┗ " : "┣ ")); + } sb.Append($"{opt.Value.GetName()}: {opt.Value.GetString()}\n"); if (opt.Value.GetBool()) ShowChildren(opt.Value, ref sb, color, deep + 1); } diff --git a/Modules/RPC.cs b/Modules/RPC.cs index dfaa61f5c..d7cd370f4 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -30,7 +30,13 @@ public enum CustomRPC SetCurrentDousingTarget, SetEvilTrackerTarget, SetRealKiller, - SyncPuppet + SyncPuppet, + SetSchrodingerCatTeam, + StealthDarken, + EvilHackerCreateMurderNotify, + PenguinSync, + MareSync, + SyncPlagueDoctor, } public enum Sounds { diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 1b76aeaa1..9bac46fa2 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -13,10 +13,10 @@ using UnityEngine; using TownOfHost.Modules; +using TownOfHost.Roles; using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; using TownOfHost.Roles.Impostor; -using TownOfHost.Roles.Neutral; using TownOfHost.Roles.AddOns.Common; using TownOfHost.Roles.AddOns.Impostor; using TownOfHost.Roles.AddOns.Crewmate; @@ -207,10 +207,10 @@ private static string GetDisplayRoleName(PlayerControl seer, PlayerControl seen var (roleColor, roleText) = GetTrueRoleNameData(seen.PlayerId); //seen側による変更 - seen.GetRoleClass()?.OverrideRoleNameAsSeen(seer, ref enabled, ref roleColor, ref roleText); + seen.GetRoleClass()?.OverrideDisplayRoleNameAsSeen(seer, ref enabled, ref roleColor, ref roleText); //seer側による変更 - seer.GetRoleClass()?.OverrideRoleNameAsSeer(seen, ref enabled, ref roleColor, ref roleText); + seer.GetRoleClass()?.OverrideDisplayRoleNameAsSeer(seen, ref enabled, ref roleColor, ref roleText); return enabled ? ColorString(roleColor, roleText) : ""; } @@ -277,7 +277,9 @@ public static string GetSubRoleMarks(List subRolesList) private static (Color color, string text) GetTrueRoleNameData(byte playerId, bool showSubRoleMarks = true) { var state = PlayerState.GetByPlayerId(playerId); - return GetRoleNameData(state.MainRole, state.SubRoles, showSubRoleMarks); + var (color, text) = GetRoleNameData(state.MainRole, state.SubRoles, showSubRoleMarks); + CustomRoleManager.GetByPlayerId(playerId)?.OverrideTrueRoleName(ref color, ref text); + return (color, text); } /// /// 対象のRoleNameを全て正確に表示 @@ -401,7 +403,7 @@ public static bool HasTasks(GameData.PlayerInfo p, bool ForRecompute = true) hasTasks = false; break; default: - if (role.IsImpostor() || role.IsKilledSchrodingerCat()) hasTasks = false; + if (role.IsImpostor()) hasTasks = false; break; } @@ -508,7 +510,7 @@ public static void ShowActiveSettings(byte PlayerId = byte.MaxValue) SendMessage(GetString("Message.HideGameSettings"), PlayerId); return; } - var sb = new StringBuilder(); + var sb = new StringBuilder().AppendFormat("", ActiveSettingsLineHeight); if (Options.CurrentGameMode == CustomGameMode.HideAndSeek) { sb.Append(GetString("Roles")).Append(':'); @@ -520,14 +522,18 @@ public static void ShowActiveSettings(byte PlayerId = byte.MaxValue) } else { - sb.Append(GetString("Settings")).Append(':'); + sb.AppendFormat("", ActiveSettingsSize); + sb.Append("").Append(GetString("Settings")).Append('\n').Append(""); + sb.AppendFormat("\n【{0}: {1}】\n", RoleAssignManager.OptionAssignMode.GetName(true), RoleAssignManager.OptionAssignMode.GetString()); + if (RoleAssignManager.OptionAssignMode.GetBool()) + { + ShowChildrenSettings(RoleAssignManager.OptionAssignMode, ref sb); + } foreach (var role in Options.CustomRoleCounts) { if (!role.Key.IsEnable()) continue; sb.Append($"\n【{GetRoleName(role.Key)}×{role.Key.GetCount()}】\n"); ShowChildrenSettings(Options.CustomRoleSpawnChances[role.Key], ref sb); - var text = sb.ToString(); - sb.Clear().Append(text.RemoveHtmlTags()); } foreach (var opt in OptionItem.AllOptions.Where(x => x.GetBool() && x.Parent == null && x.Id >= 80000 && !x.IsHiddenOn(Options.CurrentGameMode))) { @@ -536,11 +542,9 @@ public static void ShowActiveSettings(byte PlayerId = byte.MaxValue) else sb.Append($"\n【{opt.GetName(true)}】\n"); ShowChildrenSettings(opt, ref sb); - var text = sb.ToString(); - sb.Clear().Append(text.RemoveHtmlTags()); } } - SendMessage(sb.ToString(), PlayerId); + SendMessage(sb.ToString(), PlayerId, removeTags: false); } public static void CopyCurrentSettings() { @@ -580,14 +584,16 @@ public static void ShowActiveRoles(byte PlayerId = byte.MaxValue) SendMessage(GetString("Message.HideGameSettings"), PlayerId); return; } - var sb = new StringBuilder(GetString("Roles")).Append(':'); - sb.AppendFormat("\n{0}:{1}", GetRoleName(CustomRoles.GM), Options.EnableGM.GetString().RemoveHtmlTags()); + var sb = new StringBuilder().AppendFormat("", ActiveSettingsLineHeight); + sb.AppendFormat("", ActiveSettingsSize); + sb.Append("").Append(GetString("Roles")).Append('\n').Append(""); + sb.AppendFormat("\n{0}:{1}", GetRoleName(CustomRoles.GM), Options.EnableGM.GetString()); foreach (CustomRoles role in CustomRolesHelper.AllRoles) { if (role is CustomRoles.HASFox or CustomRoles.HASTroll) continue; if (role.IsEnable()) sb.AppendFormat("\n{0}:{1}x{2}", GetRoleName(role), $"{role.GetChance()}%", role.GetCount()); } - SendMessage(sb.ToString(), PlayerId); + SendMessage(sb.ToString(), PlayerId, removeTags: false); } public static void ShowChildrenSettings(OptionItem option, ref StringBuilder sb, int deep = 0) { @@ -605,7 +611,7 @@ public static void ShowChildrenSettings(OptionItem option, ref StringBuilder sb, sb.Append(string.Concat(Enumerable.Repeat("┃", Mathf.Max(deep - 1, 0)))); sb.Append(opt.Index == option.Children.Count ? "┗ " : "┣ "); } - sb.Append($"{opt.Value.GetName(true)}: {opt.Value.GetString()}\n"); + sb.Append($"{opt.Value.GetName(true).RemoveHtmlTags()}: {opt.Value.GetString()}\n"); if (opt.Value.GetBool()) ShowChildrenSettings(opt.Value, ref sb, deep + 1); } } @@ -617,20 +623,25 @@ public static void ShowLastResult(byte PlayerId = byte.MaxValue) return; } var sb = new StringBuilder(); + var winnerColor = ((CustomRoles)CustomWinnerHolder.WinnerTeam).GetRoleInfo()?.RoleColor ?? Palette.DisabledGrey; - sb.Append(GetString("LastResult")).Append(':'); + sb.Append(""""""); + sb.Append("").Append(GetString("LastResult")).Append(""); + sb.Append('\n').Append(SetEverythingUpPatch.LastWinsText.Mark(winnerColor, false)); + sb.Append(""); + + sb.Append("\n"); List cloneRoles = new(PlayerState.AllPlayerStates.Keys); - sb.Append($"\n{SetEverythingUpPatch.LastWinsText}\n"); foreach (var id in Main.winnerList) { - sb.Append($"\n★ ").Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + sb.Append($"\n★ ".Color(winnerColor)).Append(SummaryTexts(id, true)); cloneRoles.Remove(id); } foreach (var id in cloneRoles) { - sb.Append($"\n  ").Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + sb.Append($"\n  ").Append(SummaryTexts(id, true)); } - SendMessage(sb.ToString(), PlayerId); + SendMessage(sb.ToString(), PlayerId, removeTags: false); } public static void ShowKillLog(byte PlayerId = byte.MaxValue) { @@ -639,7 +650,7 @@ public static void ShowKillLog(byte PlayerId = byte.MaxValue) SendMessage(GetString("CantUse.killlog"), PlayerId); return; } - SendMessage(EndGamePatch.KillLog, PlayerId); + SendMessage(EndGamePatch.KillLog, PlayerId, removeTags: false); } public static string GetSubRolesText(byte id, bool disableColor = false) { @@ -673,11 +684,11 @@ public static void ShowHelp() + $"\n/dump - {GetString("Command.dump")}" ); } - public static void SendMessage(string text, byte sendTo = byte.MaxValue, string title = "") + public static void SendMessage(string text, byte sendTo = byte.MaxValue, string title = "", bool removeTags = true) { if (!AmongUsClient.Instance.AmHost) return; if (title == "") title = "" + GetString("DefaultSystemMessageTitle") + ""; - Main.MessagesToSend.Add((text.RemoveHtmlTags(), sendTo, title)); + Main.MessagesToSend.Add((removeTags ? text.RemoveHtmlTags() : text, sendTo, title)); } public static void ApplySuffix() { @@ -805,8 +816,6 @@ public static void NotifyRoles(bool isForMeeting = false, PlayerControl SpecifyS string SelfRoleName = enabled ? $"{text}" : ""; string SelfDeathReason = seer.KnowDeathReason(seer) ? $"({ColorString(GetRoleColor(CustomRoles.Doctor), GetVitalText(seer.PlayerId))})" : ""; string SelfName = $"{ColorString(seer.GetRoleColor(), SeerRealName)}{SelfDeathReason}{SelfMark}"; - if (Arsonist.IsDouseDone(seer)) - SelfName = $"\r\n{ColorString(seer.GetRoleColor(), GetString("EnterVentToWin"))}"; SelfName = SelfRoleName + "\r\n" + SelfName; SelfName += SelfSuffix.ToString() == "" ? "" : "\r\n " + SelfSuffix.ToString(); if (!isForMeeting) SelfName += "\r\n"; @@ -914,6 +923,7 @@ public static void AfterMeetingTasks() roleClass.AfterMeetingTasks(); if (Options.AirShipVariableElectrical.GetBool()) AirShipElectricalDoors.Initialize(); + DoorsReset.ResetDoors(); } public static void ChangeInt(ref int ChangeTo, int input, int max) @@ -970,13 +980,33 @@ public static void OpenDirectory(string path) }; Process.Start(startInfo); } - public static string SummaryTexts(byte id, bool disableColor = true) + public static string SummaryTexts(byte id, bool isForChat) { - var RolePos = TranslationController.Instance.currentLanguage.languageID == SupportedLangs.English ? 47 : 37; - string summary = $"{ColorString(Main.PlayerColors[id], Main.AllPlayerNames[id])}{GetProgressText(id)} {GetVitalText(id)} {GetTrueRoleName(id, false)}{GetSubRolesText(id)}"; - return disableColor ? summary.RemoveHtmlTags() : summary; + // 全プレイヤー中最長の名前の長さからプレイヤー名の後の水平位置を計算する + // 1em ≒ 半角2文字 + // 空白は0.5emとする + // SJISではアルファベットは1バイト,日本語は基本的に2バイト + var longestNameByteCount = Main.AllPlayerNames.Values.Select(name => name.GetByteCount()).OrderByDescending(byteCount => byteCount).FirstOrDefault(); + //最大11.5emとする(★+日本語10文字分+半角空白) + var pos = Math.Min(((float)longestNameByteCount / 2) + 1.5f /* ★+末尾の半角空白 */ , 11.5f); + + var builder = new StringBuilder(); + builder.Append(isForChat ? Main.AllPlayerNames[id] : ColorString(Main.PlayerColors[id], Main.AllPlayerNames[id])); + builder.AppendFormat("", pos).Append(isForChat ? GetProgressText(id).RemoveColorTags() : GetProgressText(id)).Append(""); + // "(00/00) " = 4em + pos += 4f; + builder.AppendFormat("", pos).Append(GetVitalText(id)).Append(""); + // "Lover's Suicide " = 8em + // "回線切断 " = 4.5em + pos += DestroyableSingleton.Instance.currentLanguage.languageID == SupportedLangs.English ? 8f : 4.5f; + builder.AppendFormat("", pos); + builder.Append(isForChat ? GetTrueRoleName(id, false).RemoveColorTags() : GetTrueRoleName(id, false)); + builder.Append(isForChat ? GetSubRolesText(id).RemoveColorTags() : GetSubRolesText(id)); + builder.Append(""); + return builder.ToString(); } public static string RemoveHtmlTags(this string str) => Regex.Replace(str, "<[^>]*?>", ""); + public static string RemoveColorTags(this string str) => Regex.Replace(str, "", ""); public static void FlashColor(Color color, float duration = 1f) { var hud = DestroyableSingleton.Instance; @@ -1073,5 +1103,8 @@ public static bool TryCast(this Il2CppObjectBase obj, out T casted) public static bool IsAllAlive => PlayerState.AllPlayerStates.Values.All(state => state.CountType == CountTypes.OutOfGame || !state.IsDead); public static int PlayersCount(CountTypes countTypes) => PlayerState.AllPlayerStates.Values.Count(state => state.CountType == countTypes); public static int AlivePlayersCount(CountTypes countTypes) => Main.AllAlivePlayerControls.Count(pc => pc.Is(countTypes)); + + private const string ActiveSettingsSize = "70%"; + private const string ActiveSettingsLineHeight = "55%"; } } \ No newline at end of file diff --git a/Patches/ChatCommandPatch.cs b/Patches/ChatCommandPatch.cs index c883b18f8..aaad9f9b4 100644 --- a/Patches/ChatCommandPatch.cs +++ b/Patches/ChatCommandPatch.cs @@ -19,7 +19,16 @@ class ChatCommands public static bool Prefix(ChatController __instance) { - if (__instance.freeChatField.textArea.text == "") return false; + // クイックチャットなら横流し + if (__instance.quickChatField.Visible) + { + return true; + } + // 入力欄に何も書かれてなければブロック + if (__instance.freeChatField.textArea.text == "") + { + return false; + } __instance.timeSinceLastMessage = 3f; var text = __instance.freeChatField.textArea.text; if (ChatHistory.Count == 0 || ChatHistory[^1] != text) ChatHistory.Add(text); diff --git a/Patches/CheckGameEndPatch.cs b/Patches/CheckGameEndPatch.cs index cc3248cfe..bf58d2e87 100644 --- a/Patches/CheckGameEndPatch.cs +++ b/Patches/CheckGameEndPatch.cs @@ -205,7 +205,6 @@ public bool CheckGameEndByLivingPlayers(out GameOverReason reason) reason = GameOverReason.ImpostorByKill; CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Jackal); CustomWinnerHolder.WinnerRoles.Add(CustomRoles.Jackal); - CustomWinnerHolder.WinnerRoles.Add(CustomRoles.JSchrodingerCat); } else if (Jackal == 0 && Imp == 0) //クルー勝利 { diff --git a/Patches/CredentialsPatch.cs b/Patches/CredentialsPatch.cs index 57d43afcc..b89234ddb 100644 --- a/Patches/CredentialsPatch.cs +++ b/Patches/CredentialsPatch.cs @@ -6,6 +6,7 @@ using TownOfHost.Modules; using TownOfHost.Roles.Core; +using TownOfHost.Templates; using static TownOfHost.Translator; namespace TownOfHost @@ -53,15 +54,18 @@ class VersionShowerStartPatch static TextMeshPro SpecialEventText; static void Postfix(VersionShower __instance) { + TMPTemplate.SetBase(__instance.text); Main.credentialsText = $"{Main.ModName} v{Main.PluginVersion}"; #if DEBUG Main.credentialsText += $"\r\n{ThisAssembly.Git.Branch}({ThisAssembly.Git.Commit})"; #endif - var credentials = Object.Instantiate(__instance.text); - credentials.text = Main.credentialsText; - credentials.alignment = TextAlignmentOptions.Right; + var credentials = TMPTemplate.Create( + "TOHCredentialsText", + Main.credentialsText, + fontSize: 2f, + alignment: TextAlignmentOptions.Right, + setActive: true); credentials.transform.position = new Vector3(1f, 2.65f, -2f); - credentials.fontSize = credentials.fontSizeMax = credentials.fontSizeMin = 2f; ErrorText.Create(__instance.text); if (Main.hasArgumentException && ErrorText.Instance != null) @@ -73,17 +77,20 @@ static void Postfix(VersionShower __instance) if (SpecialEventText == null && TohLogo != null) { - SpecialEventText = Object.Instantiate(__instance.text, TohLogo.transform); + SpecialEventText = TMPTemplate.Create( + "SpecialEventText", + "", + Color.white, + alignment: TextAlignmentOptions.Center, + parent: TohLogo.transform); SpecialEventText.name = "SpecialEventText"; - SpecialEventText.text = ""; - SpecialEventText.color = Color.white; SpecialEventText.fontSizeMin = 3f; - SpecialEventText.alignment = TextAlignmentOptions.Center; SpecialEventText.transform.localPosition = new Vector3(0f, 0.8f, 0f); } if (SpecialEventText != null) { SpecialEventText.enabled = TitleLogoPatch.amongUsLogo != null; + SpecialEventText.gameObject.SetActive(true); } if (Main.IsInitialRelease) { diff --git a/Patches/ExilePatch.cs b/Patches/ExilePatch.cs index c955e4fc4..6714dd467 100644 --- a/Patches/ExilePatch.cs +++ b/Patches/ExilePatch.cs @@ -65,7 +65,6 @@ static void WrapUpPostfix(GameData.PlayerInfo exiled) { roleClass.OnExileWrapUp(exiled, ref DecidedWinner); } - SchrodingerCat.ChangeTeam(exiled.Object); if (CustomWinnerHolder.WinnerTeam != CustomWinner.Terrorist) PlayerState.GetByPlayerId(exiled.PlayerId).SetDead(); } diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs index 055ed4cc7..1f9c50360 100644 --- a/Patches/GameOptionsMenuPatch.cs +++ b/Patches/GameOptionsMenuPatch.cs @@ -39,6 +39,12 @@ public static void Postfix(GameOptionsMenu __instance) case StringNames.GameKillCooldown: ob.Cast().ValidRange = new FloatRange(0, 180); break; + case StringNames.GameNumImpostors: + if (DebugModeManager.IsDebugMode) + { + ob.Cast().ValidRange.min = 0; + } + break; default: break; } @@ -245,7 +251,7 @@ public static bool Prefix(StringOption __instance) var option = OptionItem.AllOptions.FirstOrDefault(opt => opt.OptionBehaviour == __instance); if (option == null) return true; - option.SetValue(option.CurrentValue + 1); + option.SetValue(option.CurrentValue + (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift) ? 5 : 1)); return false; } } @@ -258,7 +264,7 @@ public static bool Prefix(StringOption __instance) var option = OptionItem.AllOptions.FirstOrDefault(opt => opt.OptionBehaviour == __instance); if (option == null) return true; - option.SetValue(option.CurrentValue - 1); + option.SetValue(option.CurrentValue - (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift) ? 5 : 1)); return false; } } diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index 0ca86567f..3e8cb8d6e 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -84,7 +84,7 @@ public static void Postfix(HudManager __instance) LowerInfoText.fontSizeMax = 2.0f; } - LowerInfoText.text = roleClass?.GetLowerText(player, isForHud: true) ?? ""; + LowerInfoText.text = roleClass?.GetLowerText(player, isForMeeting: GameStates.IsMeeting, isForHud: true) ?? ""; LowerInfoText.enabled = LowerInfoText.text != ""; if (!AmongUsClient.Instance.IsGameStarted && AmongUsClient.Instance.NetworkMode != NetworkModes.FreePlay) diff --git a/Patches/LadderPatch.cs b/Patches/LadderPatch.cs index 5e5404467..93a3a6633 100644 --- a/Patches/LadderPatch.cs +++ b/Patches/LadderPatch.cs @@ -58,7 +58,7 @@ public static void FixedUpdate(PlayerControl player) var state = PlayerState.GetByPlayerId(player.PlayerId); state.DeathReason = CustomDeathReason.Fall; state.SetDead(); - }, 0.05f, "LadderFallTask"); + }, 0.30f, "LadderFallTask"); } } } diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 76c91f8d0..bb5dbcba5 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Text; using HarmonyLib; @@ -7,6 +8,8 @@ using TownOfHost.Modules; using TownOfHost.Roles; using TownOfHost.Roles.Core; +using TownOfHost.Roles.Neutral; +using TownOfHost.Roles.Core.Interfaces; using static TownOfHost.Translator; namespace TownOfHost; @@ -27,9 +30,19 @@ public static bool Prefix() [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.CastVote))] public static class CastVotePatch { - public static void Prefix([HarmonyArgument(0)] byte srcPlayerId /* 投票した人 */ , [HarmonyArgument(1)] byte suspectPlayerId /* 投票された人 */ ) + public static bool Prefix(MeetingHud __instance, [HarmonyArgument(0)] byte srcPlayerId /* 投票した人 */ , [HarmonyArgument(1)] byte suspectPlayerId /* 投票された人 */ ) { - MeetingVoteManager.Instance.AddVote(srcPlayerId, suspectPlayerId); + var voter = Utils.GetPlayerById(srcPlayerId); + var voted = Utils.GetPlayerById(suspectPlayerId); + if (voter.GetRoleClass()?.CheckVoteAsVoter(voted) == false) + { + __instance.RpcClearVote(voter.GetClientId()); + Logger.Info($"{voter.GetNameWithRole()} は投票しない", nameof(CastVotePatch)); + return false; + } + + MeetingVoteManager.Instance?.SetVote(srcPlayerId, suspectPlayerId); + return true; } } [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] @@ -93,15 +106,16 @@ public static void Postfix(MeetingHud __instance) { _ = new LateTask(() => { - foreach (var seer in Main.AllPlayerControls) + foreach (var seen in Main.AllPlayerControls) { - foreach (var target in Main.AllPlayerControls) + var seenName = seen.GetRealName(isMeeting: true); + var coloredName = Utils.ColorString(seen.GetRoleColor(), seenName); + foreach (var seer in Main.AllPlayerControls) { - var seerName = seer.GetRealName(isMeeting: true); - var coloredName = Utils.ColorString(seer.GetRoleColor(), seerName); - seer.RpcSetNamePrivate( - seer == target ? coloredName : seerName, - true); + seen.RpcSetNamePrivate( + seer == seen ? coloredName : seenName, + true, + seer); } } ChatUpdatePatch.DoBlockChat = false; @@ -220,17 +234,33 @@ private static void RevengeOnExile(byte playerId, CustomDeathReason deathReason) private static PlayerControl PickRevengeTarget(PlayerControl exiledplayer, CustomDeathReason deathReason)//道連れ先選定 { List TargetList = new(); - foreach (var candidate in Main.AllAlivePlayerControls) + if (exiledplayer.GetRoleClass() is INekomata nekomata) { - if (candidate == exiledplayer || Main.AfterMeetingDeathPlayers.ContainsKey(candidate.PlayerId)) continue; - switch (exiledplayer.GetCustomRole()) + // 道連れしない状態ならnull + if (!nekomata.DoRevenge(deathReason)) { - //ここに道連れ役職を追加 - default: - if (exiledplayer.Is(CustomRoleTypes.Madmate) && deathReason == CustomDeathReason.Vote && Options.MadmateRevengeCrewmate.GetBool() //黒猫オプション - && !candidate.Is(CustomRoleTypes.Impostor)) - TargetList.Add(candidate); - break; + return null; + } + TargetList = Main.AllAlivePlayerControls.Where(candidate => candidate != exiledplayer && !Main.AfterMeetingDeathPlayers.ContainsKey(candidate.PlayerId) && nekomata.IsCandidate(candidate)).ToList(); + } + else + { + var isMadmate = + exiledplayer.Is(CustomRoleTypes.Madmate) || + // マッド属性化時に削除 + (exiledplayer.GetRoleClass() is SchrodingerCat schrodingerCat && schrodingerCat.AmMadmate); + foreach (var candidate in Main.AllAlivePlayerControls) + { + if (candidate == exiledplayer || Main.AfterMeetingDeathPlayers.ContainsKey(candidate.PlayerId)) continue; + switch (exiledplayer.GetCustomRole()) + { + // ここにINekomata未適用の道連れ役職を追加 + default: + if (isMadmate && deathReason == CustomDeathReason.Vote && Options.MadmateRevengeCrewmate.GetBool() //黒猫オプション + && !candidate.Is(CustomRoleTypes.Impostor)) + TargetList.Add(candidate); + break; + } } } if (TargetList == null || TargetList.Count == 0) return null; diff --git a/Patches/MovingPlatformBehaviourPatch.cs b/Patches/MovingPlatformBehaviourPatch.cs index 148926cf3..86fb9e574 100644 --- a/Patches/MovingPlatformBehaviourPatch.cs +++ b/Patches/MovingPlatformBehaviourPatch.cs @@ -29,7 +29,15 @@ public static bool GetIsDirtyPrefix(ref bool __result) return true; } [HarmonyPatch(nameof(MovingPlatformBehaviour.Use), typeof(PlayerControl)), HarmonyPrefix] - public static bool UsePrefix() => !isDisabled; + public static bool UsePrefix([HarmonyArgument(0)] PlayerControl player) + { + // プレイヤーがぬーん使用不可状態のときに使用をブロック + if (!PlayerState.GetByPlayerId(player.PlayerId).CanUseMovingPlatform) + { + return false; + } + return !isDisabled; + } [HarmonyPatch(nameof(MovingPlatformBehaviour.SetSide)), HarmonyPrefix] public static bool SetSidePrefix() => !isDisabled; } diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index 6f9cc5bb2..ffb1d2ac9 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -7,6 +7,7 @@ using TownOfHost.Modules; using TownOfHost.Roles.Core; +using TownOfHost.Templates; using static TownOfHost.Translator; namespace TownOfHost @@ -25,18 +26,19 @@ public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] ref En if (!GameStates.IsModHost) return; SummaryText = new(); foreach (var id in PlayerState.AllPlayerStates.Keys) - SummaryText[id] = Utils.SummaryTexts(id, disableColor: false); + SummaryText[id] = Utils.SummaryTexts(id, false); - var sb = new StringBuilder(GetString("KillLog") + ":"); + var sb = new StringBuilder(GetString("KillLog")); + sb.Append(""); foreach (var kvp in PlayerState.AllPlayerStates.OrderBy(x => x.Value.RealKiller.Item1.Ticks)) { var date = kvp.Value.RealKiller.Item1; if (date == DateTime.MinValue) continue; var killerId = kvp.Value.GetRealKiller(); var targetId = kvp.Key; - sb.Append($"\n{date:T} {Main.AllPlayerNames[targetId]}({Utils.GetTrueRoleName(targetId, false)}{Utils.GetSubRolesText(targetId)}) [{Utils.GetVitalText(kvp.Key)}]"); + sb.Append($"\n{date:T} {Main.AllPlayerNames[targetId]}({Utils.GetTrueRoleName(targetId, false)}{Utils.GetSubRolesText(targetId)}) [{Utils.GetVitalText(kvp.Key)}]".RemoveHtmlTags()); if (killerId != byte.MaxValue && killerId != targetId) - sb.Append($"\n\t\t⇐ {Main.AllPlayerNames[killerId]}({Utils.GetTrueRoleName(killerId, false)}{Utils.GetSubRolesText(killerId)})"); + sb.Append($"\n\t\t⇐ {Main.AllPlayerNames[killerId]}({Utils.GetTrueRoleName(killerId, false)}{Utils.GetSubRolesText(killerId)})".RemoveHtmlTags()); } KillLog = sb.ToString(); @@ -197,9 +199,6 @@ public static void Postfix(EndGameManager __instance) //####################################### var Pos = Camera.main.ViewportToWorldPoint(new Vector3(0f, 1f, Camera.main.nearClipPlane)); - var RoleSummaryObject = UnityEngine.Object.Instantiate(__instance.WinText.gameObject); - RoleSummaryObject.transform.position = new Vector3(__instance.Navigation.ExitButton.transform.position.x + 0.1f, Pos.y - 0.1f, -15f); - RoleSummaryObject.transform.localScale = new Vector3(1f, 1f, 1f); StringBuilder sb = new($"{GetString("RoleSummaryText")}"); List cloneRoles = new(PlayerState.AllPlayerStates.Keys); @@ -212,15 +211,15 @@ public static void Postfix(EndGameManager __instance) { sb.Append($"\n  ").Append(EndGamePatch.SummaryText[id]); } - var RoleSummary = RoleSummaryObject.GetComponent(); - RoleSummary.alignment = TMPro.TextAlignmentOptions.TopLeft; - RoleSummary.color = Color.white; - RoleSummary.outlineWidth *= 1.2f; - RoleSummary.fontSizeMin = RoleSummary.fontSizeMax = RoleSummary.fontSize = 1.25f; - - var RoleSummaryRectTransform = RoleSummary.GetComponent(); - RoleSummaryRectTransform.anchoredPosition = new Vector2(Pos.x + 3.5f, Pos.y - 0.1f); - RoleSummary.text = sb.ToString(); + var RoleSummary = TMPTemplate.Create( + "RoleSummaryText", + sb.ToString(), + Color.white, + 1.25f, + TMPro.TextAlignmentOptions.TopLeft, + setActive: true); + RoleSummary.transform.position = new Vector3(__instance.Navigation.ExitButton.transform.position.x + -0.05f, Pos.y - 0.13f, -15f); + RoleSummary.transform.localScale = new Vector3(1f, 1f, 1f); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/Patches/PlayerContorolPatch.cs b/Patches/PlayerContorolPatch.cs index ee6abf314..1b3a8fc8f 100644 --- a/Patches/PlayerContorolPatch.cs +++ b/Patches/PlayerContorolPatch.cs @@ -11,9 +11,7 @@ using TownOfHost.Roles; using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; -using TownOfHost.Roles.Neutral; using TownOfHost.Roles.AddOns.Crewmate; -using static TownOfHost.Translator; namespace TownOfHost { @@ -385,14 +383,6 @@ public static void Postfix(PlayerControl __instance) //名前変更 RealName = target.GetRealName(); - //名前色変更処理 - //自分自身の名前の色を変更 - if (target.AmOwner && AmongUsClient.Instance.IsGameStarted) - { //targetが自分自身 - if (target.Is(CustomRoles.Arsonist) && Arsonist.IsDouseDone(target)) - RealName = Utils.ColorString(Utils.GetRoleColor(CustomRoles.Arsonist), GetString("EnterVentToWin")); - } - //NameColorManager準拠の処理 RealName = RealName.ApplyNameColorData(seer, target, false); @@ -569,11 +559,8 @@ public static bool Prefix(PlayerControl __instance) { ret = roleClass.OnCompleteTask(); } - else - { - ret = Workhorse.OnCompleteTask(pc); - var isTaskFinish = taskState.IsTaskFinished; - } + //属性クラスの扱いを決定するまで仮置き + ret &= Workhorse.OnCompleteTask(pc); Utils.NotifyRoles(); return ret; } @@ -650,4 +637,16 @@ public static bool Prefix(PlayerControl __instance, ref RoleTypes roleType) return true; } } -} \ No newline at end of file + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Die))] + public static class PlayerControlDiePatch + { + public static void Postfix(PlayerControl __instance) + { + if (AmongUsClient.Instance.AmHost) + { + // 死者の最終位置にペットが残るバグ対応 + __instance.RpcSetPet(""); + } + } + } +} diff --git a/Patches/RandomSpawnPatch.cs b/Patches/RandomSpawnPatch.cs index d182ff8ad..aa3b12ae7 100644 --- a/Patches/RandomSpawnPatch.cs +++ b/Patches/RandomSpawnPatch.cs @@ -6,6 +6,7 @@ using UnityEngine; using TownOfHost.Roles.Core; +using TownOfHost.Roles.Impostor; namespace TownOfHost { @@ -34,6 +35,11 @@ public static void Postfix(CustomNetworkTransform __instance, [HarmonyArgument(0 if (NumOfTP[player.PlayerId] == 2) { if (Main.NormalOptions.MapId != 4) return; //マップがエアシップじゃなかったらreturn + if (player.Is(CustomRoles.Penguin)) + { + var penguin = player.GetRoleClass() as Penguin; + penguin?.OnSpawnAirship(); + } player.RpcResetAbilityCooldown(); if (Options.FixFirstKillCooldown.GetBool() && !MeetingStates.MeetingCalled) player.SetKillCooldown(Main.AllPlayerKillCooldown[player.PlayerId]); if (!Options.RandomSpawn.GetBool()) return; //ランダムスポーンが無効ならreturn diff --git a/Patches/SabotageSystemPatch.cs b/Patches/SabotageSystemPatch.cs index d867a23ad..d674aebbc 100644 --- a/Patches/SabotageSystemPatch.cs +++ b/Patches/SabotageSystemPatch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using TownOfHost.Attributes; namespace TownOfHost { @@ -33,6 +34,36 @@ public static void Prefix(HeliSabotageSystem __instance) __instance.Countdown = Options.AirshipReactorTimeLimit.GetFloat(); } } + [HarmonyPatch(typeof(SwitchSystem), nameof(SwitchSystem.RepairDamage))] + public static class SwitchSystemRepairDamagePatch + { + public static bool Prefix(SwitchSystem __instance, [HarmonyArgument(1)] byte amount) + { + if (!AmongUsClient.Instance.AmHost) + { + return true; + } + + // サボタージュによる破壊ではない && 配電盤を下げられなくするオプションがオン + if (!amount.HasBit(SwitchSystem.DamageSystem) && Options.BlockDisturbancesToSwitches.GetBool()) + { + // amount分だけ1を左にずらす + // 各桁が各ツマミに対応する + // 一番左のツマミが操作されたら(amount: 0) 00001 + // 一番右のツマミが操作されたら(amount: 4) 10000 + // ref: SwitchSystem.RepairDamage, SwitchMinigame.FixedUpdate + var switchedKnob = (byte)(0b_00001 << amount); + // ExpectedSwitches: すべてONになっているときのスイッチの上下状態 + // ActualSwitches: 実際のスイッチの上下状態 + // 操作されたツマミについて,ExpectedとActualで同じならそのツマミは既に直ってる + if ((__instance.ActualSwitches & switchedKnob) == (__instance.ExpectedSwitches & switchedKnob)) + { + return false; + } + } + return true; + } + } [HarmonyPatch(typeof(ElectricTask), nameof(ElectricTask.Initialize))] public static class ElectricTaskInitializePatch { @@ -53,4 +84,29 @@ public static void Postfix() Utils.NotifyRoles(ForceLoop: true); } } + + // サボタージュを発生させたときに呼び出されるメソッド + [HarmonyPatch(typeof(SabotageSystemType), nameof(SabotageSystemType.RepairDamage))] + public static class SabotageSystemTypeRepairDamagePatch + { + private static bool isCooldownModificationEnabled; + private static float modifiedCooldownSec; + + [GameModuleInitializer] + public static void Initialize() + { + isCooldownModificationEnabled = Options.ModifySabotageCooldown.GetBool(); + modifiedCooldownSec = Options.SabotageCooldown.GetFloat(); + } + + public static void Postfix(SabotageSystemType __instance) + { + if (!isCooldownModificationEnabled || !AmongUsClient.Instance.AmHost) + { + return; + } + __instance.Timer = modifiedCooldownSec; + __instance.IsDirty = true; + } + } } \ No newline at end of file diff --git a/Patches/ShipStatusPatch.cs b/Patches/ShipStatusPatch.cs index 0d7c0485e..5437cba81 100644 --- a/Patches/ShipStatusPatch.cs +++ b/Patches/ShipStatusPatch.cs @@ -5,6 +5,7 @@ using UnityEngine; using TownOfHost.Roles.Core; +using TownOfHost.Roles.Neutral; namespace TownOfHost { @@ -123,7 +124,11 @@ public static bool OnSabotage(PlayerControl player, SystemTypes systemType, byte return true; } - if (player.Is(CustomRoleTypes.Madmate)) + var isMadmate = + player.Is(CustomRoleTypes.Madmate) || + // マッド属性化時に削除 + (player.GetRoleClass() is SchrodingerCat schrodingerCat && schrodingerCat.AmMadmate); + if (isMadmate) { if (systemType == SystemTypes.Comms) { diff --git a/Patches/onGameStartedPatch.cs b/Patches/onGameStartedPatch.cs index f62a03ad6..cd0a09be3 100644 --- a/Patches/onGameStartedPatch.cs +++ b/Patches/onGameStartedPatch.cs @@ -150,9 +150,13 @@ public static void Prefix() PlayerControl.LocalPlayer.Data.IsDead = true; } Dictionary<(byte, byte), RoleTypes> rolesMap = new(); - AssignDesyncRole(CustomRoles.Sheriff, AllPlayers, senders, rolesMap, BaseRole: RoleTypes.Impostor); - AssignDesyncRole(CustomRoles.Arsonist, AllPlayers, senders, rolesMap, BaseRole: RoleTypes.Impostor); - AssignDesyncRole(CustomRoles.Jackal, AllPlayers, senders, rolesMap, BaseRole: RoleTypes.Impostor); + foreach (var (role, info) in CustomRoleManager.AllRolesInfo) + { + if (info.RequireResetCam) + { + AssignDesyncRole(role, AllPlayers, senders, rolesMap, BaseRole: info.BaseRoleType.Invoke()); + } + } MakeDesyncSender(senders, rolesMap); } //以下、バニラ側の役職割り当てが入る @@ -247,7 +251,7 @@ public static void Postfix() foreach (var role in CustomRolesHelper.AllRoles.Where(x => x < CustomRoles.NotAssigned)) { if (role.IsVanilla()) continue; - if (role is CustomRoles.Sheriff or CustomRoles.Arsonist or CustomRoles.Jackal) continue; + if (CustomRoleManager.GetRoleInfo(role) is SimpleRoleInfo info && info.RequireResetCam) continue; var baseRoleTypes = role.GetRoleTypes() switch { RoleTypes.Impostor => Impostors, @@ -441,7 +445,7 @@ public static int GetRoleTypesCount(RoleTypes roleTypes) int count = 0; foreach (var role in CustomRolesHelper.AllRoles.Where(x => x < CustomRoles.NotAssigned)) { - if (role is CustomRoles.Sheriff or CustomRoles.Arsonist or CustomRoles.Jackal) continue; + if (CustomRoleManager.GetRoleInfo(role) is SimpleRoleInfo info && info.RequireResetCam) continue; if (role == CustomRoles.Egoist && Main.NormalOptions.GetInt(Int32OptionNames.NumImpostors) <= 1) continue; if (role.GetRoleTypes() == roleTypes) count += role.GetRealCount(); diff --git a/README-EN.md b/README-EN.md index 3ad891a49..8ca1b58bb 100644 --- a/README-EN.md +++ b/README-EN.md @@ -15,7 +15,7 @@ This mod is not affiliated with Among Us or Innersloth LLC, and the content cont ## Releases -AmongUs Version: **2023.7.11** +AmongUs Version: **2023.7.12** **Latest Version: [Here](https://github.com/tukasa0001/TownOfHost/releases/latest)** @@ -46,15 +46,16 @@ Note that if a player other than the host plays with this mod installed, the fol ### Hotkeys #### Host Only -| HotKey | Function | Usable Scene | -| ------------------- | -------------------------------- | ------------- | -| `Shift`+`L`+`Enter` | Force End Game | In Game | -| `Shift`+`M`+`Enter` | Skip meeting to end | In Game | -| `Ctrl`+`N` | Show active settings | Lobby&In Game | -| `Ctrl`+`Shift`+`N` | Show active settings description | Lobby&In Game | -| `C` | Cancel game start | In Countdown | -| `Shift` | Start the game immediately | In Countdown | -| `Ctrl`+`RMB` | Execute clicked player | In Meeting | +| HotKey | Function | Usable Scene | +| ------------------- | ------------------------------------------- | ---------------------------------- | +| `Shift`+`L`+`Enter` | Force End Game | In Game | +| `Shift`+`M`+`Enter` | Skip meeting to end | In Game | +| `Ctrl`+`N` | Show active settings | Lobby&In Game | +| `Ctrl`+`Shift`+`N` | Show active settings description | Lobby&In Game | +| `C` | Cancel game start | In Countdown | +| `Shift` | Start the game immediately | In Countdown | +| `Shift` | Increase the option value change step to 5x | During Setting Up Options In Lobby | +| `Ctrl`+`RMB` | Execute clicked player | In Meeting | #### MOD Client Only | HotKey | Function | Usable Scene | @@ -215,25 +216,83 @@ File outputs by `Alt`+`L` are stored in `./TOH_DATA/OptionOutputs` as `Preset{pr ## Roles -| Impostors | Crewmates | Neutrals | Add-Ons | Others | -| ----------------------------------- | --------------------------------- | --------------------------------- | ----------------------------- | --------- | -| [BountyHunter](#BountyHunter) | [Bait](#Bait) | [Arsonist](#Arsonist) | [LastImpostor](#LastImpostor) | [GM](#GM) | -| [EvilTracker](#EvilTracker) | [Dictator](#Dictator) | [Egoist](#Egoist) | [Lovers](#Lovers) | | -| [FireWorks](#FireWorks) | [Doctor](#Doctor) | [Executioner](#Executioner) | [Watcher](#Watcher) | | -| [Mare](#Mare) | [Lighter](#Lighter) | [Jackal](#Jackal) | [Workhorse](#Workhorse) | | -| [Puppeteer](#Puppeteer) | [Mayor](#Mayor) | [Jester](#Jester) | | | -| [SerialKiller](#SerialKiller) | [SabotageMaster](#SabotageMaster) | [Opportunist](#Opportunist) | | | -| [ShapeMaster](#ShapeMaster) | [Seer](#Seer) | [Terrorist](#Terrorist) | | | -| [Sniper](#Sniper) | [Sheriff](#Sheriff) | [SchrodingerCat](#SchrodingerCat) | | | -| [TimeThief](#TimeThief) | [Snitch](#Snitch) | | | | -| [Vampire](#Vampire) | [SpeedBooster](#SpeedBooster) | | | | -| [Warlock](#Warlock) | [Beartrap](#Beartrap) | | | | -| [Witch](#Witch) | [TimeManager](#TimeManager) | | | | -| [Mafia](#Mafia) | | | | | -| [Madmate](#Madmate) | | | | | -| [MadGuardian](#MadGuardian) | | | | | -| [MadSnitch](#MadSnitch) | | | | | -| [SidekickMadmate](#SidekickMadmate) | | | | | +
+Impostors + +- [BountyHunter](#bountyhunter) +- [EvilHacker](#evilhacker) +- [EvilTracker](#eviltracker) +- [FireWorks](#fireworks) +- [Insider](#insider) +- [Mare](#mare) +- [Neko-Kabocha](#neko-kabocha) +- [Penguin](#penguin) +- [Puppeteer](#puppeteer) +- [SerialKiller](#serialkiller) +- [ShapeMaster](#shapemaster) +- [Sniper](#sniper) +- [Stealth](#stealth) +- [TimeThief](#timethief) +- [Vampire](#vampire) +- [Warlock](#warlock) +- [Witch](#witch) +- [Mafia](#mafia) +- [Madmate](#madmate-1) +- [MadGuardian](#madguardian) +- [MadSnitch](#madsnitch) +- [SidekickMadmate](#sidekickmadmate) + +
+ +
+Crewmates + +- [Bait](#bait) +- [Dictator](#dictator) +- [Doctor](#doctor) +- [Lighter](#lighter) +- [Mayor](#mayor) +- [SabotageMaster](#sabotagemaster) +- [Seer](#seer) +- [Sheriff](#sheriff) +- [Snitch](#snitch) +- [SpeedBooster](#speedbooster) +- [Beartrap](#beartrap) +- [TimeManager](#timemanager) + +
+ +
+Neutrals + +- [Arsonist](#arsonist) +- [Egoist](#egoist) +- [Executioner](#executioner) +- [Jackal](#jackal) +- [Jester](#jester) +- [Opportunist](#opportunist) +- [PlagueDoctor](#plaguedoctor) +- [Terrorist](#terrorist) +- [SchrodingerCat](#schrodingercat) + +
+ +
+Add-Ons + +- [LastImpostor](#lastimpostor) +- [Lovers](#lovers) +- [Watcher](#watcher) +- [Workhorse](#workhorse) + +
+ +
+Others + +- [GM](#gm) + +
### GM @@ -261,6 +320,28 @@ The target swaps after a configurable amount of time.
| Kill Cooldown After Killing Others(s) | | Show arrow pointing to target | +### EvilHacker + +Create by HYZE + +Team : Impostors +Basis : Impostor + +At every meeting beginning, the EvilHacker gets the admin information at the time in the chat. +Rooms with impostor(s) are marked with a `★`. +Rooms with dead-body(ies) are marked with the number of bodies. +They can see the kill-flash when other impostors killing. +If the option "Can See The Murder Location" is On, the room where the murder occurred is notified below their name for 10 sec. + +#### Game Options + +| Name | +| ----------------------------------------- | +| Can See The Location of Dead-bodies | +| Can See The Location of Other Impostors | +| Can See The Kill-flash for Impostor Kills | +| Can See The Murder Location | + ### EvilTracker Team : Impostors
@@ -274,7 +355,7 @@ Depending on option, they can also see kill flash when other impostor kills.
- When they Shapeshift to impostor or dead player (unavailable for target), they stay able to select target. - Shapeshift cooldown is fixed to __"5s"__ (can select target) or to __"255s"__ (cannot). - Shapeshift duration is fixed to __"1s"__, which means EvilTrackers can hardly pretend to be someone else. -- EvilTrackers can Assign [SidekickMadmate](#SidekickMadmate) by Shapeshift. +- EvilTrackers can Assign [SidekickMadmate](#sidekickmadmate) by Shapeshift. #### Game Options @@ -303,6 +384,27 @@ Even if they mistakenly bomb themselves, killing everyone results in Impostor wi | FireWorks Max Count | | FireWorks Radius | +### Insider + +Create and idea by Masami
+ +Team : Impostors
+Basis : Impostor
+ +The Insider can get information about roles of other players.
+They can see roles whose they killed.
+They can also see roles and abilities of all Impostors.
+Killing specified times tells them Madmates as well.
+ +#### Game Options + +| Name | +| ---------------------------- | +| Can See Impostor Abilities | +| Can See All Ghost's Roles | +| Can See Madmates | +| ┗ Kill Count To See Madmates | + ### Mare Create by Kihi, しゅー, そうくん, ゆりの
@@ -321,6 +423,39 @@ While lights are out they can move faster, but everyone sees their name in red.< | Mare Player Add Speed In Lights Out | | Mare Kill Cooldown In Lights Out | +### Neko-Kabocha + +Create by HYZE + +Team : Impostors +Basis : Impostor + +The Neko-Kabocha kills back their killer. +They make a revenge when exiled if the "Revenge When Exiled" option is on. + +#### Game Options + +| Name | +| ---------------------- | +| Impostors Get Revenged | +| Madmates Get Revenged | +| Revenge When Exiled | + +### Penguin + +Team : Impostors
+Basis : Shapeshifter
+ +Penguin can drag target to anywhere by kill button.
+The target can be killed when the timer reaches zero or by kill button again.
+ +#### Game Options + +| Name | +| -------------------------------------- | +| Dragging Time | +| Kill if meeting starts during dragging | + ### Puppeteer Team : Impostors
@@ -391,6 +526,23 @@ If it is a one shot assist, it will disappear immediately.
| Sniper Aim Assist | | Sniper One shot Assist | +### Stealth + +Create by HYZE +Idea by Supeeee + +Team : Impostors +Basis : Impostor + +When the Stealth kills, players in the same room are blinded for a short while. + +#### Game Options + +| Name | +| -------------------------------- | +| Exclude Impostors From Blindness | +| Blindness Duration | + ### TimeThief Created by integral, しゅー, そうくん, ゆりの
@@ -416,7 +568,7 @@ Team : Impostors
Basis : Impostor
When the vampire kills, the kill is delayed (the bitten player will die in a set time based on settings or when the next meeting is called).
-If the vampire bites [Bait](#Bait), the player will die immediately and a self-report will be forced.
+If the vampire bites [Bait](#bait), the player will die immediately and a self-report will be forced.
#### Game Options @@ -512,16 +664,17 @@ Basis : Crewmate or Engineer
Count : Crew
The MadSnitches belong to team Impostors, one type of Madmates.
-They can see who is the Impostor after finishing all their tasks.
+They can see who is the Impostor after finishing specified count of tasks.
Depending on option, they can use vents.
#### Game Options -| Name | -| ------------------------ | -| MadSnitch Can Use Vent | -| Also Exposed To Impostor | -| MadSnitch Tasks | +| Name | +| --------------------------- | +| MadSnitch Can Use Vent | +| Also Exposed To Impostor | +| Tasks Until Boost Activated | +| MadSnitch Tasks | ### SidekickMadmate @@ -575,14 +728,17 @@ By closing the chat, the doctor can see the dead players cause of death next to Team : Crewmates
Basis : Crewmate
-After finishing all the task, The lighters have their vision expanded and ignore lights out.
+By completing a certain number of tasks, or by the progress of your tasks, their vision increases. +After finishing all their tasks, they are no longer affected by the reduction in vision of lights outs. #### Game Options | Name | | ----------------------------- | -| Lighter Expanded Vision | -| Lighter Gains Impostor Vision | +| Max Vision | +| Ignore Fix Lights Effect | +| Ability Activation Condition | +| ┗ Tasks Until Boost Activated | ### Mayor @@ -661,15 +817,15 @@ Killing Crewmates will result in suicide.
| Sheriff Shot Limit | | Sheriff Can Kill Madmates | | Sheriff Can Kill Neutrals | -| ┣ Sheriff Can Kill [Jester](#Jester) | -| ┣ Sheriff Can Kill [Terrorist](#Terrorist) | -| ┣ Sheriff Can Kill [Opportunist](#Opportunist) | -| ┣ Sheriff Can Kill [Arsonist](#Arsonist) | -| ┣ Sheriff Can Kill [Egoist](#Egoist) | -| ┣ Sheriff Can Kill [SchrodingerCat](#SchrodingerCat) In Team Egoist | -| ┣ Sheriff Can Kill [Executioner](#Executioner) | -| ┣ Sheriff Can Kill [Jackal](#Jackal) | -| ┗ Sheriff Can Kill [SchrodingerCat](#SchrodingerCat) In Team Jackal | +| ┣ Sheriff Can Kill [Jester](#jester) | +| ┣ Sheriff Can Kill [Terrorist](#terrorist) | +| ┣ Sheriff Can Kill [Opportunist](#opportunist) | +| ┣ Sheriff Can Kill [Arsonist](#arsonist) | +| ┣ Sheriff Can Kill [Egoist](#egoist) | +| ┣ Sheriff Can Kill [SchrodingerCat](#schrodingercat) In Team Egoist | +| ┣ Sheriff Can Kill [Executioner](#executioner) | +| ┣ Sheriff Can Kill [Jackal](#jackal) | +| ┗ Sheriff Can Kill [SchrodingerCat](#schrodingercat) In Team Jackal | ### Snitch @@ -873,6 +1029,33 @@ Victory Conditions : Remain alive until the game end
Regardless of the games outcome, Opportunist wins an additional victory if they survive to the end of the match.
+### PlagueDoctor + +Create by こう。
+ +Team : Neutral(Solo)
+Basis : Impostor
+Count : Crew
+Victory Conditions : Infect All the living players. + +The PlagueDoctor can create the first infected player by using the kill button.
+If PlagueDoctor has not created an infected player, make the killler an infected person.
+Any player in close to an infected player becomes the next infected player with accumulated time.
+Infection status is not reset.
+The PlagueDoctor win when all survivors become infected. Do not care dead or alive.
+ +### Game Options + +| Name | +| ------------------- | +| Infect Count | +| Infect When Killed | +| Infect Time | +| Infect Distance | +| Infect Invalid Time | +| Can Infect Self | +| Can Infect in Vent | + ### SchrodingerCat Team : Neutral(Other)
@@ -1064,6 +1247,26 @@ The time limit for some sabotage can be modified. | ┣ Polus Reactor TimeLimit | | ┗ Airship Reactor TimeLimit | +## Sabotage Cooldown Control + +Modifies cooldown time for sabotages. + +| Name | +| ------------------------- | +| Sabotage Cooldown Control | +| ┗ Sabotage Cooldown | + +## Fix Lights Special Settings + +Additional options for lights outs. + +| Name | | +| ------------------------------------------ | ------------------------------------- | +| Disable Viewing Deck Lights Panel(Airship) | | +| Disable Gap Room Lights Panel(Airship) | | +| Disable Cargo Lights Panel(Airship) | | +| Block Disturbances To Switches | Make switches unable to be turned off | + ## Map Modifications ### AirShip Variable Electrical @@ -1082,6 +1285,15 @@ Disable the moving platform in Airship. | -------------------------------- | | Disable Moving Platform(Airship) | +### Reset Doors After Meeting(Airship/Polus) + +After meetings, all door openings are reset to the specified state. + +| Name | | +| ---------------------------------------- | ---------------------------------------------- | +| Reset Doors After Meeting(Airship/Polus) | | +| ┗ Reset Mode | Select from All Open/All Closed/Random By Door | + ## Mode ### DisableTasks @@ -1281,13 +1493,15 @@ If the client language is English, this option is meaningless unless `Force Japa ## Credits -More tips to modding and [BountyHunter](#BountyHunter),[Mafia](#Mafia),[Vampire](#Vampire),[Witch](#Witch),[Bait](#Bait),[Mayor](#Mayor),[Sheriff](#Sheriff),[Snitch](#Snitch),[Lighter](#Lighter),[Seer](#Seer),[Jackal](#jackal) idea by [The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles)
-[Opportunist](#Opportunist),[Watcher](#Watcher) original idea by [The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
-[SchrodingerCat](#SchrodingerCat),[EvilTracker](#EvilTracker) idea by [The Other Roles: GM Haoming Edition](https://github.com/haoming37/TheOtherRoles-GM-Haoming)
-[Doctor](#Doctor) and [Sniper](#Sniper) original idea by [Nebula on the Ship](https://github.com/Dolly1016/Nebula)
-[Jester](#Jester) and [Madmate](#Madmate) original idea by [au.libhalt.net](https://au.libhalt.net)
-[Terrorist](#Terrorist)(Trickstar + Joker) : [Foolers Mod](https://github.com/MengTube/Foolers-Mod)
+More tips to modding and [BountyHunter](#bountyhunter),[Mafia](#mafia),[Vampire](#vampire),[Witch](#witch),[Bait](#bait),[Mayor](#mayor),[Sheriff](#sheriff),[Snitch](#snitch),[Lighter](#lighter),[Seer](#seer),[Jackal](#jackal) idea by [The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles)
+[Opportunist](#opportunist),[Watcher](#watcher),[Neko-Kabocha](#neko-kabocha),[PlagueDoctor](#plaguedoctor) original idea by [The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
+[SchrodingerCat](#schrodingercat),[EvilTracker](#eviltracker),[EvilHacker](#evilhacker) idea by [The Other Roles: GM Haoming Edition](https://github.com/haoming37/TheOtherRoles-GM-Haoming)
+[Doctor](#doctor) and [Sniper](#sniper) original idea by [Nebula on the Ship](https://github.com/Dolly1016/Nebula)
+[Jester](#jester) and [Madmate](#madmate-1) original idea by [au.libhalt.net](https://au.libhalt.net)
+[Terrorist](#terrorist)(Trickstar + Joker) : [Foolers Mod](https://github.com/MengTube/Foolers-Mod)
[Lovers](#lovers) : [Town-Of-Us-R](https://github.com/eDonnes124/Town-Of-Us-R)
+[EvilHacker](#evilhacker) idea by [tomarai/TheOtherRoles](https://github.com/tomarai/TheOtherRoles/tree/dev-v3.4.x) +[Penguin](#penguin) : original idea by [Super New Roles](https://github.com/ykundesu/SuperNewRoles)
Translate-Chinese : fivefirex, ZeMingOH233
OptionTab Icon Design by 花海.
Csv: Copyright (c) 2015 Steve Hansen [MIT License](https://raw.githubusercontent.com/stevehansen/csv/master/LICENSE)
diff --git a/README.md b/README.md index ebb199c94..9f6645b63 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## リリース -AmongUsバージョン : **2023.7.11** +AmongUsバージョン : **2023.7.12** **最新版は[こちら](https://github.com/tukasa0001/TownOfHost/releases/latest)** @@ -46,15 +46,16 @@ AmongUsバージョン : **2023.7.11** ### ホットキー #### ホストのみ -| キー | 機能 | 使えるシーン | -| ------------------- | ---------------------------- | ---------------- | -| `Shift`+`L`+`Enter` | 廃村 | ゲーム内 | -| `Shift`+`M`+`Enter` | ミーティングをスキップで終了 | ゲーム内 | -| `Ctrl`+`N` | 現在の設定を表示 | ロビー&ゲーム内 | -| `Ctrl`+`Shift`+`N` | 有効な設定の説明を表示 | ロビー&ゲーム内 | -| `C` | ゲーム開始を中断 | カウントダウン中 | -| `Shift` | ゲームを即開始 | カウントダウン中 | -| `Ctrl`+`右クリック` | クリックしたプレイヤーを処刑 | 会議画面 | +| キー | 機能 | 使えるシーン | +| ------------------- | ------------------------------- | ---------------------------- | +| `Shift`+`L`+`Enter` | 廃村 | ゲーム内 | +| `Shift`+`M`+`Enter` | ミーティングをスキップで終了 | ゲーム内 | +| `Ctrl`+`N` | 現在の設定を表示 | ロビー&ゲーム内 | +| `Ctrl`+`Shift`+`N` | 有効な設定の説明を表示 | ロビー&ゲーム内 | +| `C` | ゲーム開始を中断 | カウントダウン中 | +| `Shift` | ゲームを即開始 | カウントダウン中 | +| `Shift` | オプション値の変更幅を5倍にする | ロビー内でのオプション設定中 | +| `Ctrl`+`右クリック` | クリックしたプレイヤーを処刑 | 会議画面 | #### MODクライアントのみ | キー | 機能 | 使えるシーン | @@ -215,25 +216,83 @@ Modおよびバニラの全オプションを文字列形式に変換し、保 ## 役職 -| インポスター陣営 | クルー陣営 | ニュートラル | 属性 | その他 | | -| -------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------- | --------- | --- | -| [バウンティハンター](#BountyHunterバウンティハンター) | [ベイト](#Baitベイト) | [アーソニスト](#Arsonistアーソニスト) | [ラストインポスター](#LastImpostorラストインポスター) | [GM](#GM) | | -| [イビルトラッカー](#EvilTrackerイビルトラッカー) | [ディクテーター](#Dictatorディクテーター) | [エゴイスト](#Egoistエゴイスト) | [恋人](#Lovers恋人) | | | -| [花火職人](#FireWorks花火職人) | [ドクター](#Doctorドクター) | [エクスキューショナー](#Executionerエクスキューショナー) | [ウォッチャー](#Watcherウォッチャー) | | | -| [メアー](#Mareメアー) | [ライター](#Lighterライター) | [ジャッカル](#Jackalジャッカル) | [ワークホース](#Workhorseワークホース) | | | -| [パペッティア](#Puppeteerパペッティア) | [メイヤー](#Mayorメイヤー) | [ジェスター](#Jesterジェスター) | | | | -| [シリアルキラー](#SerialKillerシリアルキラー) | [サボタージュマスター](#SabotageMasterサボタージュマスター) | [オポチュニスト](#Opportunistオポチュニスト) | | | | -| [シェイプマスター](#ShapeMasterシェイプマスター) | [シーア](#Seerシーア) | [テロリスト](#Terroristテロリスト) | | | | -| [スナイパー](#Sniperスナイパー) | [シェリフ](#Sheriffシェリフ) | [シュレディンガーの猫](#SchrodingerCatシュレディンガーの猫) | | | | -| [タイムシーフ](#TimeThiefタイムシーフ) | [スニッチ](#Snitchスニッチ) | | | | | -| [ヴァンパイア](#Vampireヴァンパイア) | [スピードブースター](#SpeedBoosterスピードブースター) | | | | | -| [ウォーロック](#Warlockウォーロック) | [トラッパー](#Trapperトラッパー) | | | | | -| [魔女](#Witch魔女) | [タイムマネージャー](#TimeManagerタイムマネージャー) | | | | | -| [マフィア](#Mafiaマフィア) | | | | | | -| [マッドメイト](#Madmateマッドメイト) | | | | | | -| [マッドガーディアン](#MadGuardianマッドガーディアン) | | | | | | -| [マッドスニッチ](#MadSnitchマッドスニッチ) | | | | | | -| [サイドキックマッドメイト](#SidekickMadmateサイドキックマッドメイト) | | | | | | +
+インポスター陣営 + +- [バウンティハンター](#bountyhunterバウンティハンター) +- [イビルハッカー](#evilhackerイビルハッカー) +- [イビルトラッカー](#eviltrackerイビルトラッカー) +- [花火職人](#fireworks花火職人) +- [インサイダー](#insiderインサイダー) +- [メアー](#mareメアー) +- [ネコカボチャ](#neko-kabochaネコカボチャ) +- [ペンギン](#penguinペンギン) +- [パペッティア](#puppeteerパペッティア) +- [シリアルキラー](#serialkillerシリアルキラー) +- [シェイプマスター](#shapemasterシェイプマスター) +- [スナイパー](#sniperスナイパー) +- [ステルス](#stealthステルス) +- [タイムシーフ](#timethiefタイムシーフ) +- [ヴァンパイア](#vampireヴァンパイア) +- [ウォーロック](#warlockウォーロック) +- [魔女](#witch魔女) +- [マフィア](#mafiaマフィア) +- [マッドメイト](#madmateマッドメイト) +- [マッドガーディアン](#madguardianマッドガーディアン) +- [マッドスニッチ](#madsnitchマッドスニッチ) +- [サイドキックマッドメイト](#sidekickmadmateサイドキックマッドメイト) + +
+ +
+クルー陣営 + +- [ベイト](#baitベイト) +- [ディクテーター](#dictatorディクテーター) +- [ドクター](#doctorドクター) +- [ライター](#lighterライター) +- [メイヤー](#mayorメイヤー) +- [サボタージュマスター](#sabotagemasterサボタージュマスター) +- [シーア](#seerシーア) +- [シェリフ](#sheriffシェリフ) +- [スニッチ](#snitchスニッチ) +- [スピードブースター](#speedboosterスピードブースター) +- [トラッパー](#trapperトラッパー) +- [タイムマネージャー](#timemanagerタイムマネージャー) + +
+ +
+ニュートラル + +- [アーソニスト](#arsonistアーソニスト) +- [エゴイスト](#egoistエゴイスト) +- [エクスキューショナー](#executionerエクスキューショナー) +- [ジャッカル](#jackalジャッカル) +- [ジェスター](#jesterジェスター) +- [オポチュニスト](#opportunistオポチュニスト) +- [ペスト医師](#plaguedoctorペスト医師) +- [テロリスト](#terroristテロリスト) +- [シュレディンガーの猫](#schrodingercatシュレディンガーの猫) + +
+ +
+属性 + +- [ラストインポスター](#lastimpostorラストインポスター) +- [恋人](#lovers恋人) +- [ウォッチャー](#watcherウォッチャー) +- [ワークホース](#workhorseワークホース) + +
+ +
+その他 + +- [GM](#gm) + +
### GM @@ -260,6 +319,27 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー | ターゲット以外殺害時のキルクール(s) | | ターゲットへの矢印を表示する | +### EvilHacker/イビルハッカー + +制作者 : はいず + +陣営 : インポスター +判定 : インポスター + +毎会議の開始時に、チャットに最終のアドミン情報が送られます。 +インポスターのいる部屋には★印、死体のある部屋には死体の数が表記されます。 +インポスターがキルを行った際にはキルフラッシュを見ることができます。 +「キルの発生場所がわかる」オプションが有効な場合、キルが発生した部屋がイビルハッカーの名前の3行目に10秒間通知されます。 + +#### 設定 + +| 設定名 | +| -------------------------------------- | +| 死体位置がわかる | +| 他のインポスターの位置がわかる | +| インポスターキル時にフラッシュが見える | +| ┗ キルの発生場所がわかる | + ### EvilTracker/イビルトラッカー 制作者 : Masami
@@ -276,7 +356,7 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー - 変身先がインポスターの場合は能力は消費されません。 - 変身クールダウンはターゲット設定可能時は「1秒」、不可時は「255秒」で固定です。 - 変身持続時間は「1秒」で固定されているので、変身能力自体は殆ど使えません。 -- オプション次第で[サイドキックマッドメイト](#SidekickMadmateサイドキックマッドメイト)を指名できます。 +- オプション次第で[サイドキックマッドメイト](#sidekickmadmateサイドキックマッドメイト)を指名できます。 #### 設定 @@ -307,6 +387,27 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー | 花火の所持数 | | 花火の爆発半径 | +### Insider/インサイダー + +制作・考案者 : Masami
+ +陣営 : インポスター
+判定 : インポスター
+ +他プレイヤーの役職が分かるインポスターです。
+キルした相手の役職を知ることができます。
+また、味方インポスターの役職と能力表示が見えます。
+さらに、特定回数キルするとマッドメイトも特定できます。
+ +#### 設定 + +| 設定名 | +| ------------------------------ | +| 味方インポスターの能力が分かる | +| 幽霊全員の役職が分かる | +| マッドメイトが分かる | +| ┗ 必要なキル数 | + ### Mare/メアー 制作者 : Kihi,ゆりの,そうくん,しゅー @@ -325,6 +426,37 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー | 停電時のメアーの加速値 | | 停電時のメアーのキルクール | +### Neko-Kabocha/ネコカボチャ + +制作者 : はいず + +陣営 : インポスター +判定 : インポスター + +キルされた際、自分を殺してきたプレイヤーを殺し返して道連れにします。 +「追放された時に誰かを道連れにする」オプションが有効な場合、会議で追放された際に道連れを発生させます。 + +| 設定名 | +| -------------------------------- | +| インポスターを道連れにする | +| マッドメイトを道連れにする | +| 追放された時に誰かを道連れにする | + +### Penguin/ペンギン + +陣営 : インポスター
+判定 : シェイプシフター
+ +キルボタンで対象を引き摺りまわします。
+タイマーが0になるかもう一度キルボタンを押すことで対象をキル可能です。
+ +#### 設定 + +| 設定名 | +| ---------------------------------- | +| 引き摺れる時間 | +| 会議開始時に引き摺り中ならキルする | + ### Puppeteer/パペッティア 陣営 : インポスター
@@ -395,6 +527,23 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー | エイムアシスト | | 単発アシスト | +### Stealth/ステルス + +考案者 : Supeeee +制作者 : はいず + +陣営 : インポスター +判定 : インポスター + +キルを行うと、同じ部屋にいる自分以外のプレイヤーの視界が数秒間ゼロになります。 + +#### 設定 + +| 設定名 | +| ---------------------------------------- | +| 暗転効果の対象からインポスターを除外する | +| 暗転の持続時間 | + ### TimeThief/タイムシーフ 考案者 : みぃー
@@ -422,7 +571,7 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー キルボタンを押してから一定時間経って実際にキルが発生する役職です。
キルをしたときのテレポートは発生しません。
また、キルボタンを押してから設定された時間が経つまでに会議が始まるとその瞬間にキルが発生します。
-しかし、[ベイト](#Baitベイト)をキルした場合のみ通常のキルとなり、強制的に通報させられます。
+しかし、[ベイト](#baitベイト)をキルした場合のみ通常のキルとなり、強制的に通報させられます。
#### 設定 @@ -503,7 +652,7 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー インポスター陣営に属しますが、マッドスニッチからはインポスターが誰なのかはわかりません。
インポスターからもマッドスニッチが誰なのかはわかりません。
-タスクを全て完了させるとマッドスニッチからインポスターを認識できるようになります。
+タスクを一定数完了させるとマッドスニッチからインポスターを認識できるようになります。
#### 設定 @@ -511,6 +660,7 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー | ---------------------------- | | ベントを使える | | インポスターからも視認できる | +| 効果を発動するタスク数 | | マッドスニッチのタスク数 | ### SidekickMadmate/サイドキックマッドメイト @@ -522,7 +672,7 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー カウント : クルー
この役職はシェイプシフター系の一部役職が変身した際に最も近いプレイヤー(インポスター陣営を除く)が指名されます。
-指名できる役職はシェイプシフター、オプション有効時の[イビルトラッカー](#EvilTrackerイビルトラッカー)、[エゴイスト](#Egoistエゴイスト)です。
+指名できる役職はシェイプシフター、オプション有効時の[イビルトラッカー](#eviltrackerイビルトラッカー)、[エゴイスト](#egoistエゴイスト)です。
インポスター陣営に属しますが、サイドキックマッドメイトからはインポスターが誰なのかはわかりません。
インポスターからもサイドキックマッドメイトが誰なのかはわかりません。
@@ -575,14 +725,17 @@ GMはゲーム自体には何の影響も与えず、すべてのプレイヤー 陣営 : クルー
判定 :クルーメイト
-タスクを完了させると、自分の視界が広がり、停電の視界減少の影響を受けなくなります。
+タスクを一定数完了させるかタスクの進捗率によって、自分の視界が広がります。 +タスク完了していると停電の視界減少の影響を受けなくなります。
#### 設定 | 設定名 | | ------------------------------ | -| タスク完了時の視界 | +| 最大視界 | | タスク完了時に停電を無効にする | +| 能力発動条件 | +| ┗ 効果を発動するタスク数 | ### Mayor/メイヤー @@ -660,17 +813,17 @@ Polus や The Airship のドアを開けるとその部屋の全てのドアが | 誤爆時、ターゲットも死ぬ | | キル可能回数 | | 全員生存時にキルできる | -| [マッドメイト](#Madmateマッドメイト)をキルできる | +| [マッドメイト](#madmateマッドメイト)をキルできる | | ニュートラルをキルできる | -| ┣ [アーソニスト](#Arsonistアーソニスト)をキルできる | -| ┣ [エゴイスト](#Egoistエゴイスト)をキルできる | -| ┣ [シュレディンガーの猫](#SchrodingerCatシュレディンガーの猫)(エゴイスト陣営)をキルできる | -| ┣ [ジェスター](#Jesterジェスター)をキルできる | -| ┣ [オポチュニスト](#Opportunistオポチュニスト)をキルできる | -| ┣ [テロリスト](#Terroristテロリスト)をキルできる | -| ┣ [エクスキューショナー](#Executionerエクスキューショナー)をキルできる | -| ┣ [ジャッカル](#Jackalジャッカル)をキルできる | -| ┗ [シュレディンガーの猫](#SchrodingerCatシュレディンガーの猫)(ジャッカル陣営)をキルできる | +| ┣ [アーソニスト](#arsonistアーソニスト)をキルできる | +| ┣ [エゴイスト](#egoistエゴイスト)をキルできる | +| ┣ [シュレディンガーの猫](#schrodingercatシュレディンガーの猫)(エゴイスト陣営)をキルできる | +| ┣ [ジェスター](#jesterジェスター)をキルできる | +| ┣ [オポチュニスト](#opportunistオポチュニスト)をキルできる | +| ┣ [テロリスト](#terroristテロリスト)をキルできる | +| ┣ [エクスキューショナー](#executionerエクスキューショナー)をキルできる | +| ┣ [ジャッカル](#jackalジャッカル)をキルできる | +| ┗ [シュレディンガーの猫](#schrodingercatシュレディンガーの猫)(ジャッカル陣営)をキルできる | ### Snitch/スニッチ @@ -851,6 +1004,33 @@ Polus や The Airship のドアを開けるとその部屋の全てのドアが ゲーム終了時に生き残っていれば追加勝利となるその他陣営の役職です。
タスクはありません。
+### PlagueDoctor/ペスト医師 + +制作者 : こう。
+ +陣営 : ニュートラル(単独)
+判定 : インポスター
+カウント : クルー
+勝利条件 : すべての生存者が感染者になる
+ +キル動作で最初の感染者を作れます。
+感染者を作っていない場合、キルされた相手を感染者にします。
+感染者に近接したプレイヤーは時間累計で次の感染者になります。
+感染状況はリセットされません。
+すべての生存者が感染者になると勝利します。自身の生死は不問です。
+ +#### 設定 + +| 設定名 | +| -------------------------- | +| 初期感染者の作成回数 | +| キルされた時に感染させる | +| 感染に必要な累計時間 | +| 感染する距離 | +| 行動開始から感染しない時間 | +| 自身も感染する | +| ベント内外でも感染する | + ### SchrodingerCat/シュレディンガーの猫 陣営 : ニュートラル(その他)
@@ -1039,6 +1219,26 @@ Polus や The Airship のドアを開けるとその部屋の全てのドアが | ┣ ポーラスのリアクター制限時間 | | ┗ エアシップのリアクター制限時間 | +## サボタージュのクールダウン制御 + +サボタージュ間のクールダウン時間を変更することができます。 + +| 設定名 | +| ------------------------------ | +| サボタージュのクールダウン制御 | +| ┗ サボタージュのクールダウン | + +## 停電の特殊設定 + +停電に関する設定を行います。 + +| 設定名 | | +| ------------------------------------ | ---------------------------------------------- | +| 展望の配電盤を無効化(エアシップ) | | +| 昇降機の配電盤を無効化(エアシップ) | | +| 貨物室の配電盤を無効化(エアシップ) | | +| 配電盤妨害を無効化 | 配電盤のスイッチをオフにすることができなくなる | + ## マップ改造 ### AirShipVariableElectrical/電気室の構造変化(エアシップ) @@ -1057,6 +1257,15 @@ Polus や The Airship のドアを開けるとその部屋の全てのドアが | ---------------------------------- | | 昇降機のリフトを無効化(エアシップ) | +### 会議後にドア状況をリセットする(エアシップ・ポーラス) + +会議終了時に、サボタージュで閉めることができるドアの開閉が特定の状態にリセットされます。 + +| 設定名 | | +| ---------------------------------------------------- | --------------------------------------------- | +| 会議後にドア状況をリセットする(エアシップ・ポーラス) | | +| ┗ リセットモード | 全て開放/全て閉鎖/ドアごとにランダム から選択 | + ## モード ### DisableTasks/タスクを無効化する @@ -1255,13 +1464,15 @@ Polus や The Airship のドアを開けるとその部屋の全てのドアが ## クレジット -[バウンティーハンター](#BountyHunterバウンティハンター)や[マフィア](#Mafiaマフィア)、[ヴァンパイア](#Vampireヴァンパイア)、[魔女](#Witch魔女)、[ベイト](#Baitベイト)、[メイヤー](#Mayorメイヤー)、[シェリフ](#Sheriffシェリフ)、[スニッチ](#Snitchスニッチ)、[ライター](#Lighterライター)、[シーア](#Seerシーア)、[ジャッカル](#jackalジャッカル) のアイデア元であり、 Mod の作成方法の参考元 : [The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles)
-[オポチュニスト](#Opportunistオポチュニスト)、[ウォッチャー](#Watcherウォッチャー) のアイデア元 : [The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
-[シュレディンガーの猫](#SchrodingerCatシュレディンガーの猫)、[イビルトラッカー](#EvilTrackerイビルトラッカー) のアイデア元 : [The Other Roles: GM Haoming Edition](https://github.com/haoming37/TheOtherRoles-GM-Haoming)
-[ドクター](#Doctorドクター)、[スナイパー](#Sniperスナイパー)のアイデア元 : [Nebula on the Ship](https://github.com/Dolly1016/Nebula)
-[ジェスター](#Jesterジェスター)(てるてる)と[マッドメイト](#Madmateマッドメイト) のアイデア元 : [au.libhalt.net](https://au.libhalt.net)
-[テロリスト](#Terroristテロリスト)(Trickstar + Joker) : [Foolers Mod](https://github.com/MengTube/Foolers-Mod)
+[バウンティーハンター](#bountyhunterバウンティハンター)や[マフィア](#mafiaマフィア)、[ヴァンパイア](#vampireヴァンパイア)、[魔女](#witch魔女)、[ベイト](#baitベイト)、[メイヤー](#mayorメイヤー)、[シェリフ](#sheriffシェリフ)、[スニッチ](#snitchスニッチ)、[ライター](#lighterライター)、[シーア](#seerシーア)、[ジャッカル](#jackalジャッカル) のアイデア元であり、 Mod の作成方法の参考元 : [The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles)
+[オポチュニスト](#opportunistオポチュニスト)、[ウォッチャー](#watcherウォッチャー)、[ネコカボチャ](#neko-kabochaネコカボチャ)、[ペスト医師](#plaguedoctorペスト医師) のアイデア元 : [The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
+[シュレディンガーの猫](#schrodingercatシュレディンガーの猫)、[イビルトラッカー](#eviltrackerイビルトラッカー)、[イビルハッカー](#evilhackerイビルハッカー) のアイデア元 : [The Other Roles: GM Haoming Edition](https://github.com/haoming37/TheOtherRoles-GM-Haoming)
+[ドクター](#doctorドクター)、[スナイパー](#sniperスナイパー)のアイデア元 : [Nebula on the Ship](https://github.com/Dolly1016/Nebula)
+[ジェスター](#jesterジェスター)(てるてる)と[マッドメイト](#madmateマッドメイト) のアイデア元 : [au.libhalt.net](https://au.libhalt.net)
+[テロリスト](#terroristテロリスト)(Trickstar + Joker) : [Foolers Mod](https://github.com/MengTube/Foolers-Mod)
[恋人](#lovers恋人) : [Town-Of-Us-R](https://github.com/eDonnes124/Town-Of-Us-R)
+[イビルハッカー](#evilhackerイビルハッカー) のアイデア元 : [tomarai/TheOtherRoles](https://github.com/tomarai/TheOtherRoles/tree/dev-v3.4.x) +[ペンギン](#penguinペンギン)のアイデア元 : [Super New Roles](https://github.com/ykundesu/SuperNewRoles)
中国語翻訳 : fivefirex、ZeMingOH233
オプションタブのアイコン製作者 : 花海
Csv: Copyright (c) 2015 Steve Hansen [MIT License](https://raw.githubusercontent.com/stevehansen/csv/master/LICENSE)
diff --git a/README_SChinese.md b/README_SChinese.md deleted file mode 100644 index ff57aba5f..000000000 --- a/README_SChinese.md +++ /dev/null @@ -1,926 +0,0 @@ -# Town Of Host(房主小镇) - -[![TownOfHost-Title](./Images/TownOfHost-Title.png)](https://youtu.be/IGguGyq_F-c) - -

- -本自述文件由两个少年汉化组进行翻译,开发者们不对本自述文件进行支持。 -The developer group will not update this file and is not responsible for any delays in information. - - -## 关于这个模组 - -该模组不隶属于我们之中或 Innersloth LLC,其中包含的内容未经 Innersloth LLC 认可或以其他方式赞助。 此处包含的部分材料是 Innersloth LLC 的财产。 © Innersloth LLC。 - -[![Discord](./Images/TownOfHost-Discord.png)](https://discord.gg/W5ug6hXB9V) - -## 资源 - -AmongUs 版本: **2022.7.12** -**最新版本: [Here](https://github.com/tukasa0001/TownOfHost/releases/latest)** - -比较旧的版本: [Here](https://github.com/tukasa0001/TownOfHost/releases) - -## 特点 - -该模组只需要安装在宿主的客户端上即可工作,无论是否安装了其他客户端模组,无论设备类型如何,都能正常工作。
-与使用自定义服务器的模组不同,无需通过编辑 URL 或文件来添加服务器。
- -但是,请注意以下限制来使用。
- -- 如果由于房主在中途离开等因素导致房主发生变化,则与附加角色相关的处理可能无法正常工作。 - -请注意,如果房主以外的玩家安装了此模组进行游戏,则会进行以下更改。
- -- 显示特殊角色的开始画面。 -- 显示特殊角色的正常胜利画面。 -- 添加其他设置。 -- 等等.... - -## 特点 -### 热键 - -#### 仅限房主 -| 热键 | 功能 | 使用场景 | -| ------------------------ | ------------------------------ | -------------- | -| `Shift`+`L`+`Enter` | 强制结束游戏 | 游戏中 | -| `Shift`+`M`+`Enter` | 跳过会议 | 游戏中 | -| `Ctrl`+`N` | 显示职业描述 | 等候大厅&游戏中| -| `C` | 取消游戏开始 | 在倒计时的时候 | -| `Shift` | 立刻开始游戏 | 在倒计时的时候 | -| `Ctrl`+`Delete` | 设置所有选项为默认 | 在TOH的设置中 | -| `Ctrl`+`RMB` | 将被点击的玩家杀死 | 在会议中 | - -#### 仅限模组客户端 -| 热键 | 功能 | 使用场景 | -| ----------- | ------------------------------------------------------------- | ----------- | -| `Tab` | 查看配置列表页面 | 大厅 | -| `Ctrl`+`F1` | 将日志输出到桌面 | 任何地方 | -| `F11` | 更改分辨率
480x270 → 640x360 → 800x450 → 1280x720 → 1600x900 | 任何地方 | -| `Ctrl`+`C` | 复制文本 | 聊天栏 | -| `Ctrl`+`V` | 粘贴文本 | 聊天栏 | -| `Ctrl`+`X` | 剪切文本 | 聊天栏 | -| `↑` | 查看上一次的语言输入 | 聊天栏 | -| `↓` | 查看下一次的语言输入 | 聊天栏 | - -### 聊天命令 -可以在聊天栏中输入指令。 - -### 房主可以使用的命令 -| 指令 | 功能 | -| ----------------------------------------------------- | ------------------------------------------------- | -| /winner
/win | 显示获胜者 | -| /rename <名称>
/r <名称> | 更改自己的名字 | -| /dis | 以船员/内鬼的身份结束比赛 | -| /messagewait <秒>
/mw <秒c> | 设置消息发送间隔 | -| /help
/h | 显示帮助 | -| /help roles <职业>
/help r <职业> | 显示职业描述 | -| /help attributes <属性>
/help att <属性> | 显示属性描述 | -| /help modes <模式>
/help m <模式> | 显示模式说明 | -| /help now
/help n | 显示活动设置说明 | - -#### 仅限模组客户端 -| 指令 | 功能 | -| -------------- | --------------------------- | -| /dump | 储存日志 | -| /version
/v | 显示MOD客户端的版本 | - -#### 所有人 -| 指令 | 功能 | -| --------------------------- | --------------------------------------- | -| /lastresult
/l | 显示游戏结果 | -| /now
/n | 显示活动设置 | -| /now roles
/n r | 显示活动职业设置 | -| /template
/t | 显示标签对应的模板文本 | - -### 模版 -此功能允许您发送准备好的消息.
-通过键入`/template `或`/t `来执行.
-要设置文本,请在与AmongUs.exe 相同的文件夹中编辑“template.txt”。
-用冒号分隔每个条目,例如 `tag:content`。
-此外,您可以通过在句子中写入 `\n` 来换行,例如 `tag:line breaks 可以 be\nmade like this`。
- -#### 欢迎留言 -如果模板功能中标签设置为“welcome”,则在玩家加入时自动发送。
-例如: `welcome:这个房间使用TownOfHost模组.` - -## 职业 - -| 内鬼                                                 | 船员                                             | 中立                                                 | -| --------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------- | -| [BountyHunter/赏金猎人](#BountyHunter赏金猎人)       | [Bait/诱饵](#Bait诱饵)                            | [Arsonist/纵火犯](#Arsonist纵火犯)                    | -| [Evil Watcher/邪恶的窥视者](#Watcher窥视者)     | [Dictator/独裁者](#Dictator独裁者)                | [Egoist/自私者](#Egoist自私者)                        | -| [FireWorks/烟花商人人](#FireWorks烟花商人)                 | [Doctor/医生](#Doctor医生)                        | [Executioner/处刑人](#Executioner行刑者)              | -| [Mare/梦魇](#Mare梦魇)                               | [Lighter/执灯人](#Lighter执灯人)                  | [Jester/小丑](#Jester小丑)                            | -| [Puppeteer/傀儡师](#Puppeteer傀儡师)                 | [Mayor/市长](#Mayor市长)                          | [Lovers/恋人](#Lovers恋人)                            | -| [SerialKiller/嗜血杀手](#SerialKiller连环杀手)        | [Nice Watcher/正义的窥视者](#Watcher窥视者)  | [Opportunist/投机者](#Opportunist投机者)              | -| [Sniper/狙击手](#Sniper狙击手)              | [SabotageMaster/修理大师](#SabotageMaster/修理大师)                | [Terrorist/恐怖分子](#Terrorist恐怖分子)               | -| [TimeThief/蚀时者](#TimeThief蚀时者)                   | [Sheriff/警长](#Sheriff警长)                      | [SchrodingerCat/薛定谔的猫](#SchrodingerCat薛定谔的猫) | -| [Vampire/吸血鬼](#Vampire吸血鬼)            | [Snitch/告密者](#Snitch告密者)                     |                                                      | -| [Warlock/术士](#Warlock术士)                     | [SpeedBooster/增速者](#SpeedBooster增速者)        |                                                      | -| [Witch/女巫](#Witch女巫)                        | [Trapper/陷阱师](#Trapper陷阱师)                   |                                                      | -| [Mafia/黑手党](#Mafia黑手党)                         |                                                  |                                                      | -| [Madmate/叛徒](#Madmate叛徒)                     |                                                  |                                                      | -| [MadGuardian/背叛的守卫](#MadGuardian背叛的守卫)      |                                                  |                                                      | -| [MadSnitch/背叛的告密者](#MadSnitch背叛的告密者)      |                                                  |                                                      | -| [SidekickMadmate/叛徒小弟](#SidekickMadmate叛徒小弟)                 |                                                  |                                                      | - -## GM - -GM(游戏大师)是观察者角色
-他们的存在对游戏本身没有影响,所有玩家都知道GM是谁。
-始终分配给玩家,并且从一开始就是幽灵。
- - -## 内鬼 - -### BountyHunter/赏金猎人 - -阵营 : 内鬼阵营
-基础 : 内鬼
-如果赏金猎人击杀悬赏目标 ,那么他的冷却时间将会缩短(可设置)
-如果赏金猎人没击杀悬赏目标,那么他的冷却时间将会增加(可设置)
-目标在可设定的时间后变更。
- -#### 游戏选项 - -| 名称 | -| ---------------------------------------- | -| 变更赏金目标的时间(秒) | -| 击杀赏金目标后击杀冷却时间(秒) | -| 击杀其他人后击杀冷却时间(秒) | - -### EvilTracker/邪恶的追踪者 - -阵营 : 内鬼阵营
-基础 : 变形者
- -
-
-
- -#### 游戏选项 - -| 名称 | -| ------------------------------------- | -| 当内鬼进行击杀时邪恶的追踪者可以看到闪光 | -| 会议后重置邪恶的追踪者追踪目标 | - -### FireWorks/烟花商人人 - -是こう的主要并由他创建。
- -阵营 : 内鬼阵营
-基础 : 变形者
- -烟花商人人可以燃放烟花,一下子杀死所有人。
-他们可以通过变形放一些烟花。
-等他们放完所有的烟花,等其他内鬼阵营的人都走了之后,他们可以通过变形一次性点燃所有的烟花。
-他们可以在燃放烟花后进行击杀。
-即使他们意外地炸死了自己,杀死所有人也会导致内鬼获胜。
- -#### 游戏选项 - -| 名称 | -| ------------------- | -| 放置烟花最大数量 | -| 烟花爆炸半径 | - -### Mare/梦魇 - -由 Kihi、しゅー、そうくん、ゆりの创作
-Kihi的想法 - -阵营 : 内鬼阵营
-基础 : 内鬼
- -他们只能在关灯的情况下击杀,但下一次的击杀冷却时间将减半。
-关灯时他的移动速度变快,但他的名字在每个人眼里变成红色
- -#### 游戏选项 - -| 名称 | -| ------------------------------- | -| 关灯时梦魇的移动速度| - -### Puppeteer/傀儡师 - -阵营 : 内鬼阵营
-基础 : 内鬼
- -傀儡师可以操纵船员并强迫他们击杀他们靠近的下一个非冒名顶替者。
-被操控的队友也可以杀死一个叛徒职业。
-傀儡师不可能进行正常的击杀。
- -### SerialKiller/连环杀手 - -阵营 : 内鬼
-基础 : 变形者
- -连环杀手的击杀冷却时间更短.
-除非在自杀时间前击杀别人,否则会自杀.
- -#### 游戏选项 - -| 名称 | -| ----------------------------- | -| 连环杀手击杀冷却时间(秒) | -| 自杀限制时间(秒) | - -### ShapeMaster/千面鬼 - -由しゅー创作和构思
- -阵营: 内鬼阵营
-基础 : 变形者
- -千面鬼没有变形冷却时间.
-但是他的变形持续时间更短 (默认值: 10s).
- -#### 游戏选项 - -| 名称 | -| ---------------------- | -| 变形持续时间(秒) | - -### Sniper/狙击手 - -由こう创作和构思。
- -阵营 : 内鬼阵营
-基础 : 变形者
- -狙击手可以射击距离很远的玩家
-他们在从变形点到释放点的延长线上击杀了一名玩家。
-子弹线上的玩家都会听到枪声
-所有子弹射完后,可以使用正常击杀.
- -精确狙击:OFF
-![off](https://user-images.githubusercontent.com/96226646/167415213-b2291123-b2f8-4821-84a9-79d72dc62d22.png)
-精确狙击:ON
-![on](https://user-images.githubusercontent.com/96226646/167415233-97882c76-fcde-4bac-8fdd-1641e43e6efe.png)
- -#### 游戏选项 - -| 名称 | -| ----------------------- | -| 狙击手子弹数量 | -| 狙击精准射击 | - -### TimeThief/蚀时者 - -由integral, しゅー, そうくん, ゆりの创建
-みぃ的主意ー
- -阵营 : 内鬼阵营
-基础 : 内鬼
- -每一次击杀都会减少会议中的讨论和投票时间。
-根据选项,失去的时间会在他死后回来。
- -#### 游戏选项 - -| 名称 | -| --------------------------------- | -| 蚀时者减速的时间(秒) | -| 投票时间下限(秒) | -| 死后归还被盗的时间 | - -### Vampire/吸血鬼 - -阵营: 内鬼阵营
-阵营 : 内鬼阵营
- -当吸血鬼击杀时,击杀会延迟(被咬的玩家会在设定的时间或下次会议召开时死亡)。
-如果吸血鬼击败 [诱饵](#Bait),玩家将立即死亡并强制进行自我报告。
- -#### 游戏选项 - -| 名称 | -| --------------------- | -| 吸血鬼击杀延迟(s) | - -### Warlock/术士 - -阵营 : 内鬼阵营
-基础 : 变形者
- -术士按下击杀按钮会对附近玩家下咒
-下次术士转移时,被诅咒的玩家会杀死最近的人。
-如果您变身为术士,则可以进行常规击杀。
-请注意,如果您或其他冒名顶替者距离您转移时被诅咒的玩家最近,您将被杀死。
- - -### Witch/女巫 - -阵营 : 内鬼阵营
-基础 : 内鬼
- -女巫可以轮流执行杀戮或施法。
-会议前被女巫拼写的玩家在会议中被标记为“十字架”,除非放逐女巫,否则他们都会在会议结束后死亡。
- - - -### Mafia/黑手党 - -阵营 : 内鬼阵营
-基础 : 内鬼
- -黑手党最初可以使用通风口和破坏活动,但不能杀人(仍然有一个按钮)。
-内鬼阵营除了他们都消失后,他们将能够杀人。
- - -## 叛徒阵营 - -叛徒阵营有一些常见的选项 -#### 游戏选项 - -| 名称 | -| ----------------------------- | -| 可以修灯 | -| 可以修通讯 | -| 可以知道谁是内鬼 | -| 跳管冷却时间 | -| 在管道待的最长时间 | - -### Madmate/叛徒 - -阵营 : 内鬼阵营
-基础 : 工程师
- -叛徒属于内鬼阵营, 但他们不知道谁是内鬼.
-内鬼也不知道谁是叛徒
-叛徒不可以击杀或者破坏,但他们可以跳管道
- -### MadGuardian/背叛的守卫 - -由空き瓶/EmptyBottle 创作和构思
- -阵营 : 内鬼阵营
-基础 : 船员
- -叛徒守卫属于内鬼阵营, 属于叛徒的一种
-与叛徒相比,叛徒守卫不能使用通风口,但在完成所有任务后可以保护内鬼
- -#### 游戏选项 - -| 名称 | -| ------------------------------------- | -| 叛徒守卫能看见谁试图击杀| - -### MadSnitch/背叛的告密者 - -由そうくん创作和构思
- -阵营 : 内鬼阵营
-基础 : 船员 or 工程师
- -叛徒告密者属于内鬼阵营,是叛徒的一种
-完成所有任务后,他们可以看到谁是内鬼。
-根据选项,他们可以使用通风口。.
- -#### 游戏选项 - -| 名称 | -| ---------------------- | -| 叛徒告密者 可以使用管道 | -| 叛徒告密者的任务数 | - -### SidekickMadmate/叛徒小弟 - -由たんぽぽ创作和构思
-阵营 : 内鬼阵营
-基础 :未知
- -叛徒小弟是内鬼阵营收买的,属于叛徒的一种
-某种基于变形者的冒名顶替者可以通过在目标旁边变形来收买成叛徒小弟。
- -**笔记** -- **"最近"** 内鬼阵营 在船员附近变形无论是谁都会被收买成叛徒小弟
- - -## 内鬼/船员 - -### Watcher/窥视者 - -阵营 : 内鬼 or 船员阵营
-基础 : 内鬼 or 船员阵营
- -窥视者能看见所有人投给了谁
- -#### 游戏选项 - -| 名称 | -| ------------------ | -| 是邪恶窥视者的概率 | - - -## 船员 - -### Bait/诱饵 - -阵营 : 船员阵营
-基础 : 船员
- -当诱饵被杀死时,冒名顶替者会自动自我报告
-这也适用于延迟杀戮——一旦按下杀戮按钮,报告将立即生效。
- -### Dictator/独裁者 - -由そうくん创作和构思
- -阵营 : 船员阵营
-基础 : 船员
- -在会议中投票给某人时,独裁者会强行打破会议并流放他们投票的玩家。
-施展武力后,独裁者刚见面就死了
- -### Doctor/医生 - -阵营 : 船员阵营
-基础 : 科学家
- -医生可以使用地图上任何位置的生命体征来查看船员何时死亡。
-通过关闭聊天,医生可以在所有会议中看到死亡玩家的死因
- -#### 游戏选项 -| 名称 | -| ----------------------- | -| 博士电池续航时间 | - -### Lighter/执灯人 - -阵营 : 船员阵营
-基础 : 船员
- -完成所有任务后执灯人的视野会变大且无视关灯
- -#### 游戏选项 - -| 名称 | -| ----------------------------- | -| 小灯人扩大视野 | -| 小灯人获得内鬼的视野 | - -### 市长 - -阵营 : 船员阵营
-基础 : 船员 or 工程师
- -市长拥有更多的票数.
-根据选项,可以通过进入通风口召开会议
- -#### 游戏选项 - -| 名称 | -| --------------------------------- | -| 市长额外票数 | -| 市长可以随地召开会议 | -| 市长可以随地召开会议的次数 | - -### SabotageMaster/修理大师 - -由空き瓶/EmptyBottle 创作和构思
- -阵营 : 船员阵营
-基础 : 船员
- -修理大师修理破坏很快
-他可以通过修复一个门来修理大部分的门.
-他可以随地修灯
-在 波鲁斯地图或飞艇地图中打开一扇门将打开所有链接的门。
- -#### 游戏选项 - -| 名称 | -| ------------------------------------------------------ | -| 修理大师修复能力限制(除了开门)| -| 修理大师可以同时打开多个门 | -| 修理大师可以修理两个反应堆 | -| 修理大师可以同时修理氧气 | -| 修理大师可以同时修理通讯 | -| 修理大师可以修复灯光 | - -### Seer/灵媒 - -阵营 : 船员阵营
-基础 : 船员
- -
-
-
- -### Sheriff/警长 - -阵营 : 船员阵营
-基础 : 内鬼(只有阵营是船员)
- -警长能执法内鬼
-根据设置,警长也可以执法中立阵营
-警长没有任务
-执法船员阵营会因为走火而死
- -#### 游戏选项 - -| 名称 | -| ----------------------------------------------------------------- | -| 警长可以 执法 [Arsonist/纵火犯](#Arsonist纵火犯) | -| 警长可以 执法 [Jester/小丑](#Jester小丑) | -| 警长可以 执法 [Terrorist/恐怖分子](#Terrorist恐怖分子) | -| 警长可以 执法 [Opportunist/投机者](#Opportunist投机者) | -| 警长可以 执法 叛徒 | -| 警长可以 执法 [Egoist/自私者](#Egoist自私者) | -| 警长可以 执法 在其他阵营的 [SchrodingerCat/薛定谔的猫](#SchrodingerCat薛定谔的猫) | -| 警长走火会连带走火目标同归于尽 | -| 警长执法次数 | - -### Snitch/告密者 - -阵营 : 船员阵营
-基础 : 船员
- -告密者完成所有任务后将知道谁是内鬼
-根据设置,当他们的任务完成时,告密者也可能会看到指向其余冒名顶替者方向的箭头。
-当告密者还有 0 或 1 个任务剩余时,冒名顶替者将能够在告密者的名字旁边看到一个星号,并且有一个活着的告密者还剩下 0 或 1 个任务。
-当告密者剩下一个或更少的任务时,冒名顶替者还会看到指向告密者方向的箭头。
- -#### 游戏选项 - -| 名称 | -| ------------------------------ | -| 告密者可以看见指向目标的箭头 | -| 告密者可以看见对应不同阵营的彩色箭头 | -| 告密者可以看到中立阵营 | - -### SpeedBooster/增速者 - -由よっキング创作和构思
- -阵营 : 船员阵营
-基础 : 船员
- -完成所有任务可以提高所有活着的玩家的速度
- -#### 游戏选项 - -| 名称 | -| -------------------- | -| 提高玩家速度 | - -### Trapper/陷阱师 - -创建者: そうくん
- 想法提出者:ランニング
- -阵营 : 船员阵营
-基础 : 船员
- -被杀后,捕手会将凶手固定在原地。
-固定在身体上的时间由主机在设置中决定。
- -#### 游戏选项 - -| 名称 | -| --------------- | -| 冻结时间 | - - -## 中立阵营 - -#### 设置 - -| 设置名称 | -| --------------- | -| 冻结移动时间 | - -### Arsonist/纵火犯 - -阵营 : 中立阵营
-基础 : 内鬼
-胜利条件:给所有存活玩家涂油并点燃
- -点击击杀按钮会给附近船员涂油.
-要以纵火犯的身份胜利,您必须给所有玩家涂油并点燃才能获得胜利.
-在玩家旁边按击杀键进行涂油.
- -#### 游戏选项 - -| 名称 | -| ----------------------- | -| 涂油所需时间 | -| 涂油冷却时间 | - -### Egoist/自私者 - -创建者:そうくん
- 想法提出者:しゅー
- -阵营 : 自私者阵营
-基础 : 变形者
-胜利条件:在所有内鬼死亡后满足内鬼阵营胜利条件。
- -自私者被算作内鬼阵营.
-他具有与变形者相同的能力.
-内鬼阵营和自私者可以互相看见,但不能互相残杀.
-自私者必须先把所有内鬼票出去,然后才能赢得内鬼.
-自私者的胜利意味者 .
- -**笔记:** -- 自私者在以下情况下失败:
-1. 他死了.
-2. 内鬼胜利但还有幸存的内鬼.
-3. 船员或者其他中立阵营获胜.
- -### Executioner/行刑者 - -阵营 : 中立阵营
-基础 : 船员
-胜利条件:让行刑目标被票出去
- -行刑者可以看见目标名字右侧有菱形图标.
-如果行刑者的目标被票出,他们将独自获胜。
-如果目标是[Jester/小丑](#Jester小丑),他们会一起胜利。
- -#### 游戏选项 - -| 名称 | -| ------------------------------- | -| 处刑人目标可以是内鬼 | -| 目标死亡后变换的职业 | - -### Jester/小丑 - -阵营 : 中立阵营
-基础 : 船员
-胜利条件 : 被票出去
- -小丑没有任务,如果小丑在会议中被票出去则单独获胜
-一直存活到游戏或者被击杀,将会输.
- -### Opportunist/投机者 - -阵营 : 中立阵营
-基础 : 船员
-胜利条件: 活到最后
- -无论是哪方阵营获胜,只要投机者活到最后,他们就会跟着获胜阵营一起胜利.
- -### SchrodingerCat/薛定谔的猫 - -阵营 : 中立阵营
-基础 : 船员
-胜利条件 : 无
- -薛定谔猫没有任务,默认情况下没有胜利条件。只有满足以下条件才能获得胜利条件。
- -1. 如果被内鬼阵营击杀会加入内鬼阵营.
-2. 如果被警长击杀会加入船员阵营.
-3. 如果被中立阵营击杀会加入中立阵营.
-4. 如果被放逐会以放逐之前的获胜条件而死.
-5. 如果被内鬼的特殊能力击杀(除了吸血鬼),他们的胜利条件和以前一样。
- -#### 游戏选项 - -| 名称 | -| ------------------------------------------------ | -| 薛定谔的猫在没有阵营阵营的情况下可以船员阵营可以和船员阵营一起获胜 | -| 被放逐后更换阵营 | - -### Terrorist/恐怖分子 - -创建与想法提出者:空き瓶/EmptyBottle
- -阵营 : 中立阵营
-基础 : 工程师
-胜利条件 :完成所有任务然后死亡
- -恐怖分子属于中立阵营如果恐怖分子完成所有任务后死亡将独自获得胜利.
-任何方式的死亡都可以.
-如果恐怖分子在完成所有任务之前死亡,或者如果恐怖分子一直活到游戏结束,恐怖分子就会输.
- -## 属性 - -### LastImpostor/绝境者 - -创建与想法提出者 そうくん
- -赋予最后一个内鬼的附加属性.
-击杀冷却比一般的短.
-不分配给[赏金猎人](#bountyhunter), [连环杀手](#serialkiller), 或者 [吸血鬼](#vampire).
- -#### 游戏选项 - -| 名称 | -| -------------------------- | -| 绝境者击杀冷却时间| - -### Lovers/恋人 - -创建与想法提出者 ゆりの
- -阵营 : 中立阵营
-基础 : -
-胜利条件 : 存活到最后. (除了任务完成)
-随机分配给两名玩家(不分阵营).
-这是个独立阵营,双方玩家一起赢或者死.
-如果你的爱人赢了,你就赢了.
-如果你的爱人死了,你也会为爱而赴死.
-船员靠任务赢的时候,恋人阵营输.
-如果在游戏结束时两人都还活着并且船员没有通过任务获胜,那么恋人阵营也可以获胜。
-如果恋人阵营赢了,其他阵营都得输.
-船员恋人被分配任务,但不计入任务完成. 可以使用能力.
- -重叠职业示例:
-- [恐怖分子](#terrorist) 恋人: 你有任务,如果你在完成任务后死亡,你将作为恐怖分子获胜.
-- [背叛的告密者](#madsnitch) 恋人: 你有任务,做完任务后依旧可以知道谁是内鬼
-- [告密者](#snitch) 恋人: 有任务,做完任务后依旧可以知道谁是内鬼.
-- [警长](#sheriff) 恋人: 你可以执法内鬼阵营. 你能不能执法取决与职业和设置. (内鬼恋人可以被执法,船员恋人不能被执法)
-- [投机者](#opportunist) 链子: 存活到最后就胜利.
-- [小丑](#jester) 恋人:如果你被投票出局,你将作为小丑获胜。如果另一个恋人被投票出局,你就失败了。
-- [诱饵](#bait) 恋人: 当另一个恋人被杀后你死了,另一个情人会立即报告你的鸡腿
- -## 禁用设备 - -参考 : [SuperNewRoles](https://github.com/ykundesu/SuperNewRoles), [The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
- -可以禁用各种设备 (目前仅限管理员,不支持MiraHQ) - -| 设置名称 | -| --------------------- | -| 禁用管理 | -| ・ 这将禁用管理 | -## SabotageTimeControl/破坏时间控制 - -某些破坏的持续时间可以修改。 - -| 名称 | -| ------------------------- | -| 波鲁斯反应堆持续时间 | -| 飞船反应堆持续时间 | - -## 模式 - -### DisableTasks/禁用任务 - -可以不分配某些任务。
- -| 名称 | -| -------------------------- | -| 禁用启动反应堆任务 | -| 禁用提交扫描任务 | -| 禁用刷卡任务 | -| 禁用解锁歧管任务 | -| 禁用上载数据任务 | - -### LadderDeath/从梯子上掉下来 - -可以设置从梯子上掉下来摔死的概率
- -| 名称 | -| -------------------- | -| 梯子上掉下来摔死的概率 | - -### 躲猫猫 - -由空き瓶/EmptyBottle创作和构思
- -#### 船员阵营 阵营 (青色) 胜利条件 - -做完所有任务.
-※内鬼的任务不计算在内.
- -#### 内鬼阵营 (红色) 胜利条件 - -击杀所有船员.
-※即使船员和冒名顶替者的数量相等,除非所有船员都被杀死,否则比赛不会结束
- -#### 狐狸(紫色) 胜利条件 - - 当其他队伍(巨魔除外)获胜时都能活下来。
- -#### 巨魔(绿色) 胜利条件 - -被内鬼阵营击杀.
- -#### 禁止功能 - -- 可以破坏
-- 可以使用管理
-- 可以使用监控
-- 幽灵向幸存者传递位置信息
-- 任务胜利(这可能使船员无法在任务中获胜)
- -#### 禁止行为 - -- 报告尸体
-- 发起紧急会议
-- 破坏
- -#### 游戏选项 - -| 名称 | -| ------------------------- | -| 允许关门 | -| 内鬼等待时间(s) | -| 禁止装饰品 | -| 禁用管道 | - -###测试模式 - -#### 船员阵营胜利条件 - -无
- -#### 内鬼胜利条件 - -无
- -#### 禁止功能 - -无
- -#### 禁止行为 - -使用除了房主按下SHIFT+L+Enter的方式结束游戏.
- -不会判定任何人胜利.
- -### RandomMapsMode/随机地图模式 - -由 つがる创建
- -随机地图模式会随机选择地图.
- -#### 游戏选项 - -| 名称 | -| ------------------- | -| 包括骷髅舰 | -| 包括米拉总部 | -| 包括波鲁斯 | -| 包括飞船 | - -### 同步会议使用次数模式 - -这个模式设置了所有船员同步发起会议使用次数.
- -#### 游戏选项 - -| 名称 | -| ---------------- | -| 最大发起会议次数 | - -## 其他设置 - -| 名称 | -| -------------- | -| 何时跳过投票 | -| 不投票时 | - -#### 客户端设置 - -## Hide Game Codes/直播模式 - -使用,可以隐藏大厅代码 - -你可以配置文件 (BepInEx\config\com.emptybottle.townofhost.cfg) 设置隐藏代码后显示的字符. -你可以通过编辑隐藏游戏代码颜色文字来随意更改文本颜色 - -## Force Japanese/强制使用日语模式 - -无论语言设置如何,启用都会强制设置菜单语言为日语. - -## Japanese Role Name/强制日语职业名称 - -通过启用,职业名称可以用日语显示 -如果客户端语言是英语,除非使用“强制使用日语模式”,否则并无任何用处 - -## 贡献 - -[赏金猎人](#BountyHunter/赏金猎人), [黑手党](#Mafia/黑手党),[吸血鬼](#Vampire/吸血鬼),[女巫](#Witch/女巫) ,[诱饵](#Bait/诱饵) ,[市长](#Mayor/市长),[警长](#Sheriff/警长),[告密者](#Snitch/告密者),[执灯人](#Lighter/执灯人),[灵媒](#Seer/灵媒)想法提出者:[The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles)
-[投机者](#Opportunist/投机者),[术士](#Warlock/术士)原创:[The Other Roles: GM Edition](https://github.com/yukinogatari/TheOtherRoles-GM)
-[薛定谔的猫](#SchrodingerCat/薛定谔的猫),[邪恶的追踪者](#EvilTracker/邪恶的追踪者)想法提出者:[The Other Roles: GM Haoming Edition](https://github.com/haoming37/TheOtherRoles-GM-Haoming)
-[医生](#Doctor/医生)原创:[Nebula on the Ship](https://github.com/Dolly1016/Nebula)
-[小丑](#Jester/小丑)和[叛徒](#Madmate/叛徒)原创:[au.libhalt.net](https://au.libhalt.net)
-[陷阱师](#Trapper/陷阱师)(Trickstar + Joker) : [Foolers Mod](https://github.com/MengTube/Foolers-Mod)
-[恋人](#Lovers/恋人) : [Town-Of-Us-R](https://github.com/eDonnes124/Town-Of-Us-R)
-Translate-Chinese : 四个憨批汉化组:氢氧则名;两个少年汉化组:晖儿和小巾
-Mersenne Twister: Copyright (c) 2015 vpmedia [MIT License](https://raw.githubusercontent.com//vpmedia/template-unity/master/LICENSE)
- -翻译来自https://www.deepl.com
- -## 开发人员 -- [EmptyBottle](https://github.com/tukasa0001) ([Twitter](https://twitter.com/XenonBottle)) -- [Tanakarina](https://github.com/tanakanira0118) -- [Shu-](https://github.com/shu-TownofHost) ([Twitter](https://twitter.com/Shu_kundayo)) -- [kihi](https://github.com/Kihi1120) -- [TAKU_GG](https://github.com/TAKUGG) ([Twitter](https://twitter.com/TAKUGGYouTube1), [Youtube](https://www.youtube.com/c/TAKUGG)) -- [Soukun](https://github.com/soukunsandesu) ([Twitter](https://twitter.com/Soukun_Dev), [Youtube](https://www.youtube.com/channel/UCsCOqxmXBVT-BD_UKaXpUPw)) -- [Mii](https://github.com/mii-47) -- [Tampopo](https://github.com/tampopo-dandelion)([Twitter](https://twitter.com/2nomotokaicho), [Youtube](https://www.youtube.com/channel/UC8EwQ5gu-qyxVxek0jZw1Tg), [ニコニコ](https://www.nicovideo.jp/user/124305243)) -- [Kou](https://github.com/kou-hetare) -- [Ykundesu](https://github.com/ykundesu) -- [Yurino](https://github.com/yurinakira) -- [Masami](https://github.com/Masami4711) - -翻译来自https://www.deepl.com diff --git a/Resources/string.csv b/Resources/string.csv index 7937443c7..6de2f6d52 100644 --- a/Resources/string.csv +++ b/Resources/string.csv @@ -28,7 +28,12 @@ "Puppeteer","Puppeteer","パペッティア","傀儡师","傀儡師","Кукловод","Marionetista" "TimeThief","Time Thief","タイムシーフ","蚀时者","時間竊賊","Вор Времени","Ladrão de Tempo" "Sniper","Sniper","スナイパー","狙击手","狙擊手","Снайпер","Sniper" -"EvilTracker","EvilTracker","イビルトラッカー","邪恶的追踪者","邪惡的追蹤者","Злой Следопыт","Rastreador do Mal" +"EvilTracker","Evil Tracker","イビルトラッカー","邪恶的追踪者","邪惡的追蹤者","Злой Следопыт","Rastreador do Mal" +"Stealth","Stealth","ステルス","","","Хитрец","" +"NekoKabocha","Neko-Kabocha","ネコカボチャ","","","","" +"EvilHacker","Evil Hacker","イビルハッカー","","","Злой Хакер","" +"Penguin","Penguin","ペンギン","","","Пингвин","" +"Insider","Insider","インサイダー","","","Инсайдер","" "# マッドメイト系役職" "Madmate","Madmate","マッドメイト","叛徒","叛徒","Безумец","Tripulante Louco" @@ -40,7 +45,7 @@ "Bait","Bait","ベイト","诱饵","誘餌","Приманка","Isca" "Lighter","Lighter","ライター","执灯人","持燈人","Фонарь","Lanterneiro" "Mayor","Mayor","メイヤー","市长","市長","Мэр","Prefeito" -"SabotageMaster","Sabotage Master","サボタージュマスター","修理大师","修理工","Мастер Саботажа","Concertador" +"SabotageMaster","Sabotage Master","サボタージュマスター","修理大师","修理工","Мастер Саботажа","Faz-Tudo" "Sheriff","Sheriff","シェリフ","警长","警長","Шериф","Xerife" "Arsonist","Arsonist","アーソニスト","纵火犯","縱火犯","Поджигатель","Incendiário" "Snitch","Snitch","スニッチ","告密者","告密者","Стукач","Dedo-Duro" @@ -49,40 +54,37 @@ "Trapper","Beartrap","トラッパー","陷阱师","設陷者","Охотник","Armadilheiro" "Dictator","Dictator","ディクテーター","独裁者","獨裁主義者","Диктатор","Ditador" "Seer","Seer","シーア","灵媒","靈媒","Провидец","Vidente" -"TimeManager","TimeManager","タイムマネージャー","时间管理者","時間大師","Мастер Времени","" +"TimeManager","TimeManager","タイムマネージャー","时间管理者","時間大師","Мастер Времени","Dono do Tempo" "# ニュートラル役職" "Jester","Jester","ジェスター","小丑","小丑","Шут","Bobo" "Terrorist","Terrorist","テロリスト","恐怖分子","恐怖分子","Террорист","Terrorista" "Executioner","Executioner","エクスキューショナー","处刑人","劊子手","Палач","Executor" "SchrodingerCat","Schrödinger's Cat","シュレディンガーの猫","薛定谔的猫","薛丁格的貓","Пленник","Gato de Schrodinger" -"CSchrodingerCat","Schrödinger's Cat","シュレディンガーの猫","薛定谔的猫","薛丁格的貓","Пленник Экипажа","Gato de Schrodinger" -"MSchrodingerCat","Schrödinger's Cat","シュレディンガーの猫","薛定谔的猫","薛丁格的貓","Пленник Предателя","Gato de Schrodinger" -"EgoSchrodingerCat","Schrödinger's Cat","シュレディンガーの猫","薛定谔的猫","薛丁格的貓","Пленник Эгоиста","Gato de Schrodinger" -"JSchrodingerCat","Schrödinger's Cat","シュレディンガーの猫","薛定谔的猫","薛丁格的貓","Пленник Шакала","Gato de Schrodinger" "Opportunist","Opportunist","オポチュニスト","投机者","投機主義者","Выживший","Oportunista" "Egoist","Egoist","エゴイスト","野心家","利己主義者","Эгоист","Egoísta" "Lovers","Lovers","恋人","恋人","戀人","Любовники","Amantes" "Jackal","Jackal","ジャッカル","豺狼","豺狼","Шакал","Chacal" +"PlagueDoctor","Plague Doctor","ペスト医師","","","Чумной Доктор","" "# HideAndSeek" "HASFox","Fox","狐","狐狸","狐妖","Лис","Raposa" "HASTroll","Troll","トロール","猎人","誘捕者","Тролль","Troll" "# GM" -"GM","GM","GM","GM(管理员)","GM(遊戲大師)","GM(Призрак)","GM(Fantasma)" +"GM","GM","GM","GM(管理员)","GM(遊戲大師)","Мастер Игры","GM(Fantasma)" "# 属性" "LastImpostor","Last Impostor","ラストインポスター","绝境者","絕境者","Последний Предатель","Último Impostor" "Watcher","Watcher","ウォッチャー","窥视者","觀察者","Наблюдатель","Observador" -"Workhorse","Workhorse","ワークホース","实干家","加班狂","Работник","" +"Workhorse","Workhorse","ワークホース","实干家","加班狂","Работник","Burro de Carga" "# その他" -"CustomRoleTypes.Crewmate","Crewmate","クルー","船员","","Членов Экипажа","" -"CustomRoleTypes.Impostor","Impostor","インポスター","内鬼","","Предателей","" -"CustomRoleTypes.Neutral","Neutral","ニュートラル","独立","","Нейтралов","" -"CustomRoleTypes.Madmate","Mad","マッド","叛徒","","Безумцев","" -"TeamCrewmate","Team Crewmates","クルー陣営","船员阵营","","Команда Членов Экипажа","" +"CustomRoleTypes.Crewmate","Crewmate","クルー","船员","","Членов Экипажа","Tripulante" +"CustomRoleTypes.Impostor","Impostor","インポスター","内鬼","","Предателей","Impostor" +"CustomRoleTypes.Neutral","Neutral","ニュートラル","独立","","Нейтралов","Neutro" +"CustomRoleTypes.Madmate","Mad","マッド","叛徒","","Безумцев","Louco" +"TeamCrewmate","Team Crewmates","クルー陣営","船员阵营","","Команда Членов Экипажа","Time Tripulantes" "TeamImpostor","Team Impostors","インポスター陣営","内鬼阵营","幫助偽裝者","Команда Предателей","Time Impostor" "TeamEgoist","Team Egoist","エゴイスト陣営","野心家阵营","利己主義者陣營","Команда Эгоистов","Time Egoísta" "TeamJackal","Team Jackal","ジャッカル陣営","豺狼阵营","豺狼陣營","Команда Шакалов","Time Chacal" @@ -108,6 +110,11 @@ "SniperInfo","Snipe","狙い撃つぜ","瞄准敌人,射击!","讓你的敵人在你的狙擊下死亡","Стреляйте в Членов Экипажа на расстоянии","Hora de atirar" "LastImpostorInfo","You are the last of us","残ったのはあなただけ","你是狼村最后的希望…","你是狼村最後的希望...","Ты последний предатель","Você é o último Impostor" "EvilTrackerInfo","Track others","プレイヤーを追跡しろ","让我看看,我的小目标在哪","你不要過來啊啊阿","Отслеживайте Игроков","Localize os outros" +"StealthInfo","Act unseen in the dark world...","闇の世界で暗躍せよ","","","Действуйте незаметно в темноте","" +"NekoKabochaInfo","Take your killer to your grave","死なばもろとも","","","Отведи своего убийцу в могилу","" +"EvilHackerInfo","Hack systems","システムをハッキングせよ","","","Взломайте систему","" +"PenguinInfo","Let's drag and kill!","ぺちぺち!","","","Давайте тащить и убивать!","" +"InsiderInfo","Knowledge is power","知は力なり","","","Знание - сила!","" "# マッドメイト系役職" "MadmateInfo","Help the Impostors","インポスターの援助をしよう","帮助内鬼","幫助偽裝者","Помогите Предателям","Ajude os Impostores" @@ -128,7 +135,7 @@ "TrapperInfo","Trap your enemies","敵を罠にはめよう","引诱敌人落入你的陷阱当中","誘捕你的敵人","Ловите своих врагов","Prenda os seus inimigos" "DictatorInfo","Decide who to eject","独裁政治をしよう","让我统治世界,拥有无上的权利","讓所有人臣服於你","Повесьте своего врага на страх и риск","Escolha quem será exilado" "SeerInfo","You see the moment someone dies","他人の死んだ瞬間がわかる","你能感知死亡","你擁有陰陽眼","Вы видите когда умирают игроки","Veja o momento em que alguém morre" -"TimeManagerInfo","Do the tasks and extend meeting time","タスクをして会議時間を延ばそう","任务搞快点,不就有时间开会了嘛","完成你的任務來延長會議時間","Выполняйте задания чтобы увеличить время встречи","" +"TimeManagerInfo","Do the tasks and extend meeting time","タスクをして会議時間を延ばそう","任务搞快点,不就有时间开会了嘛","完成你的任務來延長會議時間","Выполняйте задания чтобы увеличить время встречи","Faça as tarefas e aumente o tempo de reunião" "# ニュートラル役職" "ArsonistInfo","Burn them to crisps","燃やせ","火焰给我燃烧起来吧!","燒吧,燒吧,燃燒吧","Облейте и подожгите всех игроков","Queime tudo em pedacinhos" @@ -144,6 +151,7 @@ "EgoistInfo","Take over the Impostors' victory","インポスター勝利を独占しよう","夺走内鬼的胜利","讓我們來奪取偽裝者的勝利","Не дай Предателям победить","Ganhe no lugar do Impostor" "LoversInfo","Live happily ever after, together","恋人と生きて幸せを掴もう","你们坠入了爱河,成为了一对恋人,一起活到最后吧!","你墜入了愛河","Выживите со своим Любовником","Vivam felizes para sempre, juntos" "JackalInfo","Kill Everyone","すべてを殺せ","快去杀光所有人,一只苍蝇都不要剩下!","殺光所有人不留活口","Убей всех игроков","Mate todos" +"PlagueDoctorInfo","Spread disease to wipe out the crew","ペストをばらまけ","","","Распространите чуму","" "# HideAndSeek" "HASFoxInfo","Just stay alive","とにかく生き残りましょう","活下去吧!活到最后你就成为了赢家!","盡你所能地活下去吧!","Останьтесь в живых","Sobreviva a qualquer custo" @@ -154,7 +162,7 @@ "# 属性" "WatcherInfo","Gaze upon all votes","みんなの投票に目を光らせよう","你可以看见所有人的投票,注意他们的选择","注意所有人的投票","Вы видите цвета голосов","Veja todos os votos" -"WorkhorseInfo","You have Extra tasks","タスクはまだ終わらない","这活还得继续肝","感覺不如做任務","Теперь у вас дополнительные задания","" +"WorkhorseInfo","You have Extra tasks","タスクはまだ終わらない","这活还得继续肝","感覺不如做任務","Теперь у вас дополнительные задания","Você tem tarefas adicionais" "## 役職説明ロング" @@ -172,6 +180,11 @@ "TimeThiefInfoLong","(Impostors):\nEvery kill reduces discussion and voting time in meeting.\nIf you are voted out or killed, the lost time returns to meetings.","(インポスター陣営):\nキルを行うと会議時間が減少する。\nタイムシーフが追放または殺害されると、失われた会議時間が返ってきます。","(内鬼阵营):\n蚀时者每击杀一个人,会议时间就将减少一定时间。\n如果蚀时者死亡,被偷取的时间的会议时间将返还。","(偽裝者陣營):\n時間竊賊每殺死一個人,那麼會議時間就會減少一定時間,如果時間竊賊死亡,那麼會議時間將會恢復。","(Предатель):\nКаждое убийство Вора Времени сокращает время обсуждения и голосования на собраниях. \nВ зависимости от настроек потерянное время возвращается после того, как он будет убит или изгнан.","(Impostores):\nCada morte reduz tempo de votação e discussão.\nSe você for exilado ou morto, o tempo perdido retorna para as reuniões." "SniperInfoLong","(Impostors):\nYou can shoot players from afar.\nYour line of fire continues as a straight line in the direction of the spot where you shapeshift to where you unshift.\nYou cannot perform normal kills until you use up all of your ammo.","(インポスター陣営):\n遠くの敵を狙撃できるインポスター。\nシェイプシフトした地点から解除した地点の延長線上の敵を一人撃ち抜く。\n撃ちきるまで通常キル出来ない。","(内鬼阵营):\n狙击手拥有远距离射杀的能力。\n方法为:变形的位置及解除变形位置的连线及其延长线上的一名玩家将被射杀。\n在狙击手的子弹耗尽前,狙击手无法进行常规击杀。","(偽裝者陣營):\n狙擊手擁有遠距離狙殺的技能,當狙擊手變形時會標記一個點(標記A),解除變形時也會標記一個點(標記B),標記A到標記B即為彈道,\n子彈將由標記A穿越彈道後再從標記B打出,並狙殺離這條彈道上最近的人(若有人處於彈道中則不會被狙殺),狙擊手在子彈用完之後可以正常殺人。","(Предатель):\nСнайпер может стрелять в игроков на расстоянии. \nОн убивает игрока который, находится с ним на одной линии, от точки Морфа до точки возвращения в свой облик. \nОн может совершать обычные убийства после того, как все его патроны закончатся.","(Impostor):\nVocê pode atirar em jogadores de longe.\nSua linha de fogo continua numa linha reta, do ponto que você se metamorfa ao ponto que você volta ao normal.\nVocê não pode fazer mortes normais até usar toda a sua munição." "EvilTrackerInfoLong","(Impostors):\nYou can see where others are.\nYou will see arrows pointing to the other impostors and anyone you shapeshift into.\nYou will see a ""kill flash"" when your fellow impostors kill.","(インポスター陣営):\n他人の位置が分かるインポスター。\n味方のインポスターとシェイプシフトで選んだ一人の位置が矢印で表示される。\n味方インポスターのキルによるキルフラッシュも見える。","(内鬼阵营):\n邪恶追踪者可以追踪其他内鬼以及其所变形的玩家。\n玩家名称下面的箭头代表着目标的方向。\n当内鬼队友杀人时,邪恶追踪者将会看到击杀闪烁提示。","(偽裝者陣營):\n邪惡追蹤者可以通過變形來指定一個追蹤目標,變形後將會立刻解除變形,並且玩家名稱會出現一個箭頭指向目標,當隊友殺人時,邪惡追蹤者將會看到螢幕閃爍為提示。","(Предатель):\nЗлой Следопыт может отслеживать других игроков. \nОн может видеть стрелки, которые указывают на Предателей или на того, в кого он превратился. \nОн может видеть когда было совершено убийство с помощью вспышки убийства.","(Impostores):\nVocê consegue ver a posição de outros.\nÉ possível ver setas apontando para outros Impostores e qualquer um em que você se transforme.\nVocê também verá uma piscada de tela quando um aliado Impostor matar alguém." +"StealthInfoLong","(Impostors):\nWhen the Stealth kills, players in the same room are blinded for a short while.","(インポスター陣営):\nキルを行うと、同じ部屋にいる他のプレイヤーの視界が少しの間暗転する。","","","(Предатель):\nКогда Хитрец убивает, игроки в одной локации будут ненадолго ослеплены","" +"NekoKabochaInfoLong","(Impostors):\nThe Neko-Kabocha kills back their killer.","(インポスター陣営):\nキルされた際、自分を殺してきたプレイヤーを殺し返す。","","","(Предатель):\nНеко-Кабоча убивает своего убийцу","" +"EvilHackerInfoLong","(Impostors):\nThe EvilHacker can get the last-minute admin information at the meeting beginning.\nUnoccupied rooms are not shown.\nA '★' marks rooms with impostors.\nRooms with dead-bodies are marked with the number of bodies.\ne.g.)★Cafeteria: 3(DEAD×1)","(インポスター陣営):\n会議直前のアドミン情報を見ることができる。\n誰もいない部屋は省略される。\nインポスターがいる部屋には★印が付く。\n死体がある部屋には死体数が表記される。\n例)★カフェテリア: 3(死体×1)","","","(Предатель):\nЗлой Хакер может получить последнюю информацию на админке в начале встречи.\nНезанятые локации не показаны.\nA '★' Помечает локации с Предателями.\nЛокации с трупами помечаются количеством трупом) \nНапример: \n★Кафетерия: 3 (Мертвы×1)""","" +"PenguinInfoLong","(Impostors):\nPenguins can restrain target by pressing the kill button, and drag around.\While dragging, the target dies by pressing the kill button again or after a certain period of time.\nPress the kill button twice for a direct kill.","(インポスター陣営):\nキルボタン一回で相手を拘束して連れまわすことが出来る。\nもう一度キルボタンを押すか一定時間経過でキルする。\nキルボタン2回で直接キル可能。","","","(Предатель):\nПингвины могут удерживать цель, нажимая кнопку убийства, и перетаскивать ее.\Во время перетаскивания цель умирает, нажав кнопку убийства еще раз или через определенный промежуток времени.\nНажмите кнопку убийства дважды, чтобы убить напрямую.","" +"InsiderInfoLong","(Impostors):\nYou can see roles whose you killed.\nYou can also see roles and abilities of all Impostors.\nKIlling specified times tells you Madmate as well.","(インポスター陣営):\nキルした相手の役職が分かる。\nまた、味方インポスターの役職と能力が見える。\nさらに、特定回数キルするとマッドメイトも分かる。","","","(Предатель):\nВы можете увидеть роли, которых Вы убили\nВы также можете увидеть роли и способности всех Предателей.\nУбийство в указанное время также сообщает вам о Безумце.","" "# マッドメイト系役職" @@ -182,7 +195,7 @@ "# 特殊クルー役職" "BaitInfoLong","(Crewmates):\nWhen you are killed, you force your killer to immediately self-report.","(クルー陣営):\nキルされた時に、自身をキルした人に\n強制的にセルフレポートさせる事ができる。","(船员阵营):\n诱饵被击杀时,击杀诱饵的凶手将被迫报告。","(船員):\n當被殺時,兇手將強制自行舉報","(Член Экипажа):\nКогда Приманку убивают, он заставляет убившего игрока \nмоментально зарепортить ваш труп.","(Tripulantes):\nQuando morrer, você força o seu assassino a reportar o seu corpo imediatamente." -"LighterInfoLong","(Crewmates):\nOnce you finish all your tasks, your vision will increase.","(クルー陣営):\nタスクを終わらせると、自分の視界を広げることができる。","(船员阵营):\n执灯人完成任务后,视野会扩大,且不受照明破坏影响。","(船員):\n當做完所有任務時可以增大視野且不受關燈影響","(Член Экипажа):\nПосле выполнения всех заданий Фонарик сможет увеличить свое поле зрения.","(Tripulantes):\nQuando você terminar todas as suas tarefas, sua visão irá aumentar." +"LighterInfoLong","(Crewmates):\nYour vision will increase.","(クルー陣営):\n条件を満たすと、自分の視界を広げることができる。","(船员阵营):\n执灯人完成任务后,视野会扩大,且不受照明破坏影响。","(船員):\n當做完所有任務時可以增大視野且不受關燈影響","(Член Экипажа):\nПосле выполнения всех заданий Фонарик сможет увеличить свое поле зрения.","(Tripulantes):\nQuando você terminar todas as suas tarefas, sua visão irá aumentar." "MayorInfoLong","(Crewmates):\nYou have multiple votes, which will all go towards the one person you vote or skip.","(クルー陣営):\n会議の票を複数持ち、まとめて一人または\nスキップに入れることができる。(設定有)","(船员阵营):\n市长在投票时可投下多票 (票数多寡根据设置),市长还拥有以跳通风管来召开紧急会议的能力。","(船員):\n可以一次投很多票並可以將它灌到一個人身上或跳過 (可以設定票數)","(Член Экипажа):\nМэр можете иметь несколько голосов на собрании, и проголосовать за любого игрока этими доп. голосами или \nпросто отдать все голоса в скип.","(Tripulantes):\nVocê tem votos adicionais, todo eles vão para a pessoa em que você votar ou pular." "SabotageMasterInfoLong","(Crewmates):\nYou can Fix Reactors, O2, Communications by yourself.\nLights can be fixed with the flick of one switch.\nOpening a door will open all doors to that room.","(クルー陣営):\nリアクター・O2・通信妨害を一人で修理可能。\n停電は一箇所のレバーに触れる事で全て直る。\nドアを開けるとその部屋の全てのドアが開く。","(船员阵营):\n氧气泄露、核反应堆熔毁以及米拉总部的通讯破坏维修大师只需要修复一边则另一边即可同时被修复。\n维修大师只需要按一个开关便可以修复照明破坏。\n维修大师打开波鲁斯与飞艇地图的门时维修大师所在房间的所有门同时打开。","(船員):\n反應堆, 氧氣, 通訊以及其他破壞可以修理工獨自修復。","(Член Экипажа):\nМастер Саботажа может в одиночку починить саботажи такие, как: Саботаж Реактора, Саботаж O2 и Саботаж Связи. \nТак же он может починить Саботаж Света коснувшись лишь одного рычага. \nОткрытие одной двери позволяет открыть все двери в этой комнате.","(Tripulantes):\nVocê pode consertar Reatores, O2, Comunicações sozinho.\nAs luzes podem ser reparadas apertando apenas um botão.\nAbrir uma porta, abre todas as outras daquela mesma sala." "SheriffInfoLong","(Crewmates):\nYou can kill anyone on Team Impostors.\nThe ability to kill Neutrals is a configurable setting.\nIf you try to kill a crew, you will kill yourself instead. You have no tasks.","(クルー陣営):\nインポスター陣営をキルすることができる。\n※ニュートラルをキル可能にする設定有。\nクルーメイトをキルすると自殺する。タスクはない。","(船员阵营):\n警长可以击杀内鬼(根据房间设置,警长也可以击杀独立阵营玩家)。\n警长若尝试击杀船员阵营的玩家,警长将会走火自杀。\n警长没有任务。","(船員):\n警長可以殺死狼人陣營的人,\n*可以設定中立陣營是否可以被殺\n警長若槍到好人陣營將會自殺,警長沒有任務。","(Член Экипажа):\nШериф может убивать Предателей. \nЕсть настройка, позволяющая убить даже Нейтралов. \nОднако если Шериф попытается убить Члена Экипажа, то это приведёт к его смерти. У него нет заданий.","(Tripulantes):\nVocê pode matar qualquer um, no Time Impostor.\nA habilidade de matar neutros é configurável.\nSe você tentar matar um Tripulante, irá morrer no lugar dele. Você não tem tarefas." @@ -193,7 +206,7 @@ "TrapperInfoLong","(Crewmates):\nWhen you are killed, you immobilize your killer for a configurable amount of time.","(クルー陣営):\nキルされると、キルした人を数秒間移動不可にすることができる。(設定有)","(船员阵营):\n陷阱师被击杀时,凶手一段时间内将不能移动。","(船員):\n如果設限者被殺,殺他的兇手將會被困在原地幾秒(可以設定)","(Член Экипажа):\nПосле того как Охотника убьют, то его убийца будет обездвижен на несколько секунд. (время зависит от настроек)","(Tripulantes): \nAo ser morto, imobilize o seu assassino por um certo período. (configurável)" "DictatorInfoLong","(Crewmates):\nWhen you vote someone, the meeting will end on the spot and the player you voted will be ejected.\nThe moment you vote someone out, you will also die.\nWhen you vote for someone in a meeting, they forcibly break that meeting and exile the player they vote for.\nAfter exercising the force, the Dictators die just after meeting.","(クルー陣営):\n会議中に誰かに投票をすると、会議を強制終了させて投票先を吊る事ができる。\n投票したタイミングでディクテーターは死ぬ。","(船员阵营):\n当独裁者在会议阶段投票给玩家后,会议会被强制结束并放逐其投票对象。\n该技能发动后独裁者将会死亡。","(船員):\n如果獨裁主義者在會議上投票給某位玩家,\n獨裁主義者將強制結束會議並無視場上票數將獨裁主義者投的那個人投出\n但是獨裁主義者將死於他投票的瞬間。","(Член Экипажа):\nЕсли Диктатор проголосует за любого игрока во время собрания, он сможет принудительно завершить собрание и кикнуть игрока за которого он отдал голос \nТак же Диктатор умрёт после собрания когда он отдаст голос.","(Tripulantes): \nQuando votar em alguém, durante uma reunião, a reunião será encerrada e a pessoa em que você votou, será exilada. \nAo exilar alguém, você morre-rá." "SeerInfoLong","(Crewmates):\nWhenever someone dies, you will see a ""kill flash"".","(クルー陣営):\nプレイヤーが死亡するごとに、シーアはキルフラッシュを見ることができる。","(船员阵营):\n每当玩家死亡时,灵媒将会看到击杀闪烁。","(船員):當有玩家死亡時,靈媒將看到閃光","(Член Экипажа):\nПровидец видит ''Вспышку Убийства'' каждый раз, когда игроки умирают.","(Tripulantes): \nQuando alguém morre, você vê um ""clarão de abate"" (a sua tela piscará brevemente)." -"TimeManagerInfoLong","(Crewmates):The more tasks you do, the longer your meeting will last. When you die, the meeting time will be restored.","(クルー陣営):\nタスクを終わらせるごとに会議時間が延びる。死亡すると会議時間は元に戻る。","(船员阵营):\n你做的任务越多,会议时间就会越长。在你死后,会议时间会复原。","(船員陣營):\n時間大師做越多任務,那麼會議時間就會延長越多(直到上限)。如果時間大師死亡,那麼會議時間就會恢復原狀。","(Член Экипажа):\nМастер Времени может увеличить время встречи, увеличивается время по мере выполнения заданий. Но когда он умрет, время встречи будет сброшено по умолчанию.","" +"TimeManagerInfoLong","(Crewmates):The more tasks you do, the longer your meeting will last. When you die, the meeting time will be restored.","(クルー陣営):\nタスクを終わらせるごとに会議時間が延びる。死亡すると会議時間は元に戻る。","(船员阵营):\n你做的任务越多,会议时间就会越长。在你死后,会议时间会复原。","(船員陣營):\n時間大師做越多任務,那麼會議時間就會延長越多(直到上限)。如果時間大師死亡,那麼會議時間就會恢復原狀。","(Член Экипажа):\nМастер Времени может увеличить время встречи, увеличивается время по мере выполнения заданий. Но когда он умрет, время встречи будет сброшено по умолчанию.","(Tripulantes):\nAo fazer tarefas você aumenta o tempo de reunião. Quando você morre, o tempo de reunião volta ao normal." "# ニュートラル役職" "ArsonistInfoLong","(Neutrals):\nWhen you use the kill button, you douse your target in oil.\nAfter dousing all living players, you set everything ablaze and win by venting.","(ニュートラル):\nキルボタンを使おうとすると、ターゲットに油を塗れる。\nすべてのクルーに油を塗ったら船を燃やして勝利する。","(独立阵营):\n纵火犯可以通过对玩家点击击杀按钮并在跟随其数秒来完成涂油行为。\n当所有存活玩家都被纵火犯涂油后,纵火犯可以通过跳通风管来点火,并单独获得胜利。","(中立):\n當縱火犯嘗試使用殺人鍵,它將對他身邊最近的一個人澆上油,\n當他對所有玩家澆上油之後,縱火縱火犯點火即勝利。","(Нейтрал):\nПоджигатель может обливать игроков, нажав на кнопку 'Убить' и находясь рядом с игроком в течение нескольких секунд. \nПосле того как он обольёт всех живых игроков Поджигатель сможет запрыгнуть в вентиляцию, чтобы поджечь всех игроков, что приводит к победе Поджигателя.","(Neutros):\nAo apertar o botão de matar, você encharca o seu inimigo em óleo.\nApós ter encharcado todos os jogadores vivos, você coloca tudo em chamas e ganha entrando na ventilação." @@ -204,6 +217,7 @@ "OpportunistInfoLong","(Neutrals):\nSo long as you are alive at the end of the game, you will win alongside whoever the victor is.","(ニュートラル):\n試合終了時に生存していれば追加勝利となる。","(独立阵营):\n若投机者在游戏结束时存活,则投机者跟随获胜玩家一同获得胜利。","(中立):\n如果投機主義者活到最後,他將跟著遊戲結束獲勝的陣營一起獲勝。","(Нейтрал):\nВыживший выигрывает игру с любыми другими ролями, но только если он выжил.","(Neutros): \nContanto que você esteja vivo no final do jogo, você dividirá a vitória ao lado do vencedor." "EgoistInfoLong","(Neutrals):\nWhen all impostors are dead, you solo victory by fulfilling impostor victory conditions.\nYou and the impostors know who each other are.","(ニュートラル):\n味方がすべて死んだ状態でインポスターが勝つと単独勝利する。\nインポスターは誰がエゴイストか分かる。エゴイストも、誰がインポスターか分かる。","(独立阵营):\n原则上野心家属于内鬼阵营。野心家与内鬼阵营玩家互认但不可以击杀对方。\n当其他内鬼阵营玩家全部死亡后,若野心家存活且内鬼阵营达成胜利条件,则野心家单独获得胜利。","(中立陣營):\n如果所有的偽裝者都死亡,且利己主義者存活,利己主義者將獨自獲勝。\n(偽裝者和利己主義者互相知道對方但是不可以互刀對方)","(Нейтрал):\nПосле того, как все Предатели умрут то Эгоист побеждает вместо Предателей. \nПредатели и Эгоист видят друг друга.","(Neutros): \nQuando todos os Impostores morrerem, você ganha sozinho assumindo o papel de Impostor. \nVocê e os Impostores sabem quem são uns aos outros." "JackalInfoLong","(Neutrals):\nJackal can kill all Crewmates, Impostors and Neutrals.\nTeam Jackal wins when living Jackal outnumbers living Crewmates and when there are no Impostors alive.","(ニュートラル):\nジャッカルはすべてのプレイヤーを殺すことができる。\nインポスターが残っておらず、生き残っているジャッカルの人数がクルーと同じかそれ以上でジャッカルが勝利する。","(独立阵营):\n豺狼需要击杀所有人。\n存活的玩家只剩豺狼和一名其他船员时,豺狼获得胜利。","(中立):\n豺狼需要殺死所有人來獲得勝利,\n如果場上存活的玩家只剩下豺狼和另一名不帶刀職業時,豺狼將獲勝。","(Нейтрал):\nШакал может убить всех Членов Экипажа, Нейтралов и Предателей тоже. \nШакал может победить, когда в живых остался только 1 Шакал и 1 Член Экипажа.","(Neutros): \nVocê pode matar todos os jogadores. \nGanhe quando não existirem mais Impostores, e o número de Chacais for maior que o de Tripulantes." +"PlagueDoctorInfoLong","(Neutrals):\nThe Plague Doctor's goal is to infect every living player.\nThey start by choosing one player to infect, after which anyone who spends a set\namount of time in range of the infected player becomes infected themselves.\nInfection progress is cumulative, and does not reset with distance or after meetings.","(第三陣営):\nペスト医師ははすべてのプレイヤーに感染を広げることが目的。\nキルで最初の感染者を選ぶ。接触感染していきすべてのプレイヤーに感染させたら勝利。会議では感染はリセットされない","","","(Нейтрал):\nЦель Чумного Доктора - заразить всех живых игроков.\nОн выбирает одного игрока для заражения с помощью кнопки 'Убить', после чего игрок который будет в радиусе зараженного игрока в течении X времени, станет зараженным.\nПрогресс заражения не сбрасывается после встреч и т.д.","" "# HideAndSeek" "HASFoxInfoLong","(HideAndSeek):\nThey win the game with other Roles (except Troll) only if they are alive at the game end.","(かくれんぼ):\nトロールを除くいずれかの陣営が勝利したときに生き残っていれば、勝利した陣営に追加で勝利することができる。","(躲猫猫):\n狐狸活到最后便与获胜阵营一同获胜。","(躲貓貓):\n除了其他的中立陣營以外,只要狐妖活下來即可獲勝\n他們將跟著遊戲結束的獲勝陣營一起獲勝。","(Прятки):\nЕсли какая-либо роль, кроме Тролля побеждает и выживает, то победившая роль может одержать дополнительную победу.","(Esconde-Esconde): \nEles ganham adicionalmente com qualquer classe (exceto Troll), se estiverem vivos no final." @@ -215,7 +229,7 @@ "# 属性" "LastImpostorInfoLong","(Add-ons):\nAn Add-on granted to the last Impostor remaining.\nKill cooldown is reduced according to this setting.\nNot granted to Bounty Hunters, Serial Killers, or Vampires.","(属性):\n最後のインポスターに付与される属性。\nキルクールが設定した時間まで短くなる。\nバウンティハンター、シリアルキラー、ヴァンパイアには付与されない。","(效果):\n这个效果在内鬼仅剩一人时赋予该内鬼。\n使其击杀冷却缩短。\n该效果对赏金猎人、嗜血杀手以及吸血鬼不适用。","(附加效果):\n該效果給予在場上的最後一位偽裝者,擁有該效果的偽裝者刀人冷卻時間會變短,該效果不會給予:\n賞金獵人, 連環殺手或吸血鬼 (可以設定冷卻)","(Атрибут):\nАтрибут, присваивается последнему Предателю. \nВремя отката убийства становится меньше, чем обычно. \nНе назначается Охотнику за головами, Серийному убийце или Вампиру.","(Atributos): \nUm Atributo dado ao último Impostor. \nO tempo de recarga (abate) é reduzido segundo a configuração. \nNão é dado para Caçadores de Recompensas, Serial Killers ou Vampiros." "WatcherInfoLong","(Add-ons):\nYou can see everyone's votes even if anonymous voting is on.","(属性):\n全員の投票先を見ることができる。","(效果):\n会议时窥视者可以看到所有人的投票。","(附加效果):\n會議時觀察者可以看到所有人的投票。","(Атрибут):\nНаблюдатель может видеть все цвета голосов несмотря на анонимное голосование.","(Atributos):\nVocê consegue ver os votos de todos, mesmo que os votos anônimos estejam ligados." -"WorkhorseInfoLong","(Add-ons):\nAn Add-on granted to the first living Crewmate finishing all the tasks.\nYou are assigned additional tasks necessary for the tasks win.\nNot granted to roles with no tasks or with abilities triggered by finishing tasks.","(属性):\n最初に生きてタスクを終えたクルーに付与される属性。\n追加タスクが割り当てられる。\nクルーメイト以外にも割り当てる設定でもタスクが無い、タスク完了で能力が発動する役職には付与されない。","(附加效果):\n该附加效果赋予给第一个完成所有任务并存活的船员。\n你需要完成额外的任务来达成任务胜利。\n此效果不会赋予那些没有任务或者通过完成任务才能使用能力的诸多职业。","(屬性):\n此效果會被賦予在第一個完成任務且存活的船員。\n實習生必須做完附加任務來獲勝,\n此效果不會賦予在那些需要通過完成任務來觸發技能的職業上。","(Атрибут):\nДополнительные задания присваиваются первому живому Члену Экипажа, который выполнит все задания. \nЕму назначаются дополнительные задания, необходимые для победы с помощью заданий. \nНе может присваиваться ролям которые не имеют заданий, или ролям со способностями которые активируются после выполнения заданий.","" +"WorkhorseInfoLong","(Add-ons):\nAn Add-on granted to the first living Crewmate finishing all the tasks.\nYou are assigned additional tasks necessary for the tasks win.\nNot granted to roles with no tasks or with abilities triggered by finishing tasks.","(属性):\n最初に生きてタスクを終えたクルーに付与される属性。\n追加タスクが割り当てられる。\nクルーメイト以外にも割り当てる設定でもタスクが無い、タスク完了で能力が発動する役職には付与されない。","(附加效果):\n该附加效果赋予给第一个完成所有任务并存活的船员。\n你需要完成额外的任务来达成任务胜利。\n此效果不会赋予那些没有任务或者通过完成任务才能使用能力的诸多职业。","(屬性):\n此效果會被賦予在第一個完成任務且存活的船員。\n實習生必須做完附加任務來獲勝,\n此效果不會賦予在那些需要通過完成任務來觸發技能的職業上。","(Атрибут):\nДополнительные задания присваиваются первому живому Члену Экипажа, который выполнит все задания. \nЕму назначаются дополнительные задания, необходимые для победы с помощью заданий. \nНе может присваиваться ролям которые не имеют заданий, или ролям со способностями которые активируются после выполнения заданий.","(Atributos):\nUm atributo dado ao primeiro tripulante que finalizar todas as tarefas.\nVocê recebe tarefas adicionais necessárias para vencer. \nNão é dado a classes quem não têm tarefas, ou com habilidades ativadas ao finalizar todas as tarefas." "#モードオプション" "HideAndSeek","Hide and Seek","かくれんぼ","躲猫猫","躲貓貓","Прятки","Esconde-Esconde" @@ -251,16 +265,16 @@ "ColorNameMode","Color Name Mode","色名前モード","显示颜色名称","名字將被顏色名稱替換","Режим: Никнейм соответствует цвету","Modo Nomes de Cores" "FixFirstKillCooldown","Normalize First Kill Cooldown","初期スポーン時のクールダウン修正","修正首刀冷却时间","修正開場時的殺人冷卻時間","Нормализовать откат убийства в начале Игры","Normalizar Tempo de Recarga do Primeiro Abate" "GhostCanSeeOtherRoles","Ghosts Can See Other Roles","幽霊が他人の役職を見ることができる","幽灵可见他人职业","幽靈可以看見所有玩家職業","Призраки могут видеть все Роли","Fantasmas Podem Ver Outros Papéis" -"GhostCanSeeOtherTasks","Ghosts Can See Other Tasks","幽霊が他人のタスク進捗を見ることができる","幽灵可见他人任务进度","","Призраки могут видеть прогресс заданий других игроков","" +"GhostCanSeeOtherTasks","Ghosts Can See Other Tasks","幽霊が他人のタスク進捗を見ることができる","幽灵可见他人任务进度","","Призраки могут видеть прогресс заданий других игроков","Fantasmas Podem Ver Tarefas de Outros" "GhostCanSeeOtherVotes","Ghosts Can See Other Votes","幽霊が他人の投票先を見ることができる","幽灵可见投票情况","幽靈可以看見所有玩家的投票","Призраки могут видеть цвета Голосов","Fantasmas Podem Ver Outros Votos" "GhostCanSeeDeathReason","Ghost Can See Cause Of Death","幽霊が死因を見ることができる","幽灵可以看见死因","幽靈可以看見死因","Призраки могут видеть Причины Смерти","Fantasmas Podem Ver Causa da Morte" "GhostIgnoreTasks","Ghosts Exempt From Tasks","死人のタスクを免除する","幽灵无视任务","幽靈可以免除任務","Призраки игнорируют Задания","Fantasmas Isentos de Tarefas" "DisableTaskWin","Disable Task Win","タスク勝利を無効化","禁用任务胜利","禁用任務勝利","Отключить победу по Заданиям","Desativar Vitória por Tarefas" "HideGameSettings","Hide Game Settings","ゲーム設定を隠す","隐藏游戏设置","隱藏遊戲設定","Скрыть настройки Игры","Esconder Configurações" -"RoleOptions","Role Options","役職設定","职业设置","職業設定","Настройка Ролей","Opções de Papéis" +"RoleOptions","Role Options","役職設定","职业设置","職業設定","Настройка Ролей","Opções de Classe" "ModeOptions","Mode Options","モード設定","模组设置","模式設定","Настройка Режима","Opções de Modos" "AutoDisplayLastResult","Auto Display Last Result","自動的に試合結果を表示","自动显示最终结果","自動顯示上一回合結果","Отображать результат последней игры в чате","Mostrar Ultimo Resultado" -"AutoDisplayKillLog","Auto Display KillLog","自動的にキルログを表示","自动显示击杀日志","自動顯示擊殺紀錄","Отображать историю убийств в чате","" +"AutoDisplayKillLog","Auto Display KillLog","自動的にキルログを表示","自动显示击杀日志","自動顯示擊殺紀錄","Отображать историю убийств в чате","Mostrar Log de Mortes Automaticamente" "VoteMode","Voting Mode","投票モード","投票相关设定","投票設定","Режим Голосования","Modo de Votação" "WhenSkipVote","When Skip Vote","スキップ時","跳过投票相当于投给自己","當跳過投票時","Когда пропускаете Голосование","Quando Pular Voto" "WhenSkipVoteIgnoreFirstMeeting","Ignore First Meeting","初回会議を除く","忽略首次会议","忽略首次會議","Игнорировать первое собрание","Ignorar Primeira Reunião" @@ -301,23 +315,29 @@ "DisableAirshipViewingDeckLightsPanel","Disable Viewing Deck Lights Panel(Airship)","展望の配電盤を無効化(エアシップ)","禁用瞭望台配电箱(飞艇地图)","關閉觀景台的配電箱 (Airship)","Отключить починку Света на Смотровой Палубе (Airship)","Desativar Painel de Luzes do Deck Panorâmico(Airship)" "DisableAirshipGapRoomLightsPanel","Disable Gap Room Lights Panel(Airship)","昇降機の配電盤を無効化(エアシップ)","禁用升降机配电箱(飞艇地图)","關閉間隙室右側的配電箱 (Airship)","Отключить починку Света в Комнате Пролета (Airship)","Desativar Painel de Luzes da Sala Suspensa(Airship)" "DisableAirshipCargoLightsPanel","Disable Cargo Lights Panel(Airship)","貨物室の配電盤を無効化(エアシップ)","禁用货舱配电箱(飞艇地图)","關閉貨艙的配電箱 (Airship)","Отключить починку Света в Грузовом Отсеке (Airship)","Desativar Painel de Luzes das Cargas(Airship)" -"MapModification","Map Modifications","マップ改造","地图修改","","Модификации карты","" -"DisableAirshipMovingPlatform","Disable Moving Platform(Airship)","昇降機のリフトを無効化(エアシップ)","禁用升降机(飞艇地图)","","Отключить движущуюся платформу (Airship)","" -"AirShipVariableElectrical","Variable Electrical(AirShip)","電気室の構造変化(エアシップ)","改变配电室构造(飞艇地图)","電力室構造變化 (Airship)","Двери в Электрощитовой меняются случайно (Airship)","" +"BlockDisturbancesToSwitches","Block Switches When They Are Up","配電盤妨害を無効化","","","Блокировать переключатели когда они подняты","" +"MapModification","Map Modifications","マップ改造","地图修改","","Модификации карты","Modificações do Mapa" +"DisableAirshipMovingPlatform","Disable Moving Platform(Airship)","昇降機のリフトを無効化(エアシップ)","禁用升降机(飞艇地图)","","Отключить движущуюся платформу (Airship)","Desativar Plataformas Voadoras(Airship)" +"AirShipVariableElectrical","Variable Electrical(AirShip)","電気室の構造変化(エアシップ)","改变配电室构造(飞艇地图)","電力室構造變化 (Airship)","Двери в Электрощитовой меняются случайно (Airship)","Elétrica Varia(AirShip)" +"ResetDoorsEveryTurns","Reset Doors After Meeting(Airship/Polus)","会議後にドア状況をリセットする(エアシップ・ポーラス)","","","Сбросить статус дверей после собраний","" +"DoorsResetMode","Reset Mode","リセットモード","","","Режим сброса дверей","" +"AllOpen","All Open","全て開放","","","Все Открыты","" +"AllClosed","All Closed","全て閉鎖","","","Все Закрыты","" +"RandomByDoor","Random By Door","ドアごとにランダム","","","Случайно для каждой двери","" "RandomSpawn","Random Spawn","ランダムスポーン","随机出生点","隨機出生點","Случайный спавн","Spawn Aleatório" "AirshipAdditionalSpawn","Additional Spawn(Airship)","追加スポーン位置(エアシップ)","额外出生点(飞艇地图)","額外出生點(The Airship地圖)","Дополнительный спавн(Airship)","Spawn Adicional(Airship)" "CommsCamouflage","Camouflage During Comms","コミュサボ時のカモフラージュ","通信破坏时伪装","通訊破壞時所有玩家變成小灰人","Камуфляж при Саботаже Связи","Camuflagem Durante Comunicação" "EnableDebugMode","Enable Debug Mode","デバッグモードを有効化する","开启调试模式","啟用偵錯模式","Включить режим отладки","Ativar Modo de Depuração" "ChangeNameToRoleInfo","Show Role Descriptions to Unmodded Client","役職説明を非modクライアントにも表示する","对未安装本mod的玩家显示职业说明","對未安裝本模組的玩家顯示職業說明","Показать описания Ролей игрокам играющие без Мода","Mostrar Descrição de Classe para Clientes Não Modificados" -"RoleAssigningAlgorithm","Role Assigning Algorithm","役職割り当てのアルゴリズム","职业分配算法","職業分配算法","Алгоритм назначения Ролей","" -"RoleAssigningAlgorithm.Default","Default","デフォルト","默认随机算法","預設算法","По умолчанию","" -"RoleAssigningAlgorithm.NetRandom",".NET System.Random",".NET System.Random",".NET 系统随机算法","NET系統隨機算法","Случайный","" -"RoleAssigningAlgorithm.HashRandom","HashRandom","HashRandom","哈希随机算法","Hash值隨機算法","HashRandom","" -"RoleAssigningAlgorithm.Xorshift","Xorshift","Xorshift","Xorshift随机算法","Xorshift隨機算法","Xorshift","" -"RoleAssigningAlgorithm.MersenneTwister","Mersenne Twister","Mersenne Twister","Mersenne Twister随机算法","Mersenne Twister隨機算法","MersenneTwister","" -"ApplyDenyNameList","Apply DenyName List","DenyNameリストを適用する","启用违禁昵称名单","自動禁止具有不良名字的人加入","Применить файл запрещённых имён (DenyName)","" -"KickPlayerFriendCodeNotExist","Kick Players Whose Friend Code Does Not Exist","フレンドコードが存在しないプレイヤーをキックする","踢出好友编号无效的玩家","將沒有好友代碼的玩家自動踢出","Кикнуть игроков у которых нет Кода Друга","" -"ApplyBanList","Apply BanList","BANリストを適用する","启用封禁名单","啟用封禁名單","Применить файл с забаненными игроками (BanList)","" +"RoleAssigningAlgorithm","Role Assigning Algorithm","役職割り当てのアルゴリズム","职业分配算法","職業分配算法","Алгоритм назначения Ролей","Algoritmo de Atribuição de Classes" +"RoleAssigningAlgorithm.Default","Default","デフォルト","默认随机算法","預設算法","По умолчанию","Padrão" +"RoleAssigningAlgorithm.NetRandom",".NET System.Random",".NET System.Random",".NET 系统随机算法","NET系統隨機算法","Случайный","Sistema Aleatório .NET" +"RoleAssigningAlgorithm.HashRandom","HashRandom","HashRandom","哈希随机算法","Hash值隨機算法","HashRandom","HashRandom" +"RoleAssigningAlgorithm.Xorshift","Xorshift","Xorshift","Xorshift随机算法","Xorshift隨機算法","Xorshift","Xorshift" +"RoleAssigningAlgorithm.MersenneTwister","Mersenne Twister","Mersenne Twister","Mersenne Twister随机算法","Mersenne Twister隨機算法","MersenneTwister","Mersenne Twister" +"ApplyDenyNameList","Apply DenyName List","DenyNameリストを適用する","启用违禁昵称名单","自動禁止具有不良名字的人加入","Применить файл запрещённых имён (DenyName)","Aplicar Lista de Nomes Proibidos (DenyName)" +"KickPlayerFriendCodeNotExist","Kick Players Whose Friend Code Does Not Exist","フレンドコードが存在しないプレイヤーをキックする","踢出好友编号无效的玩家","將沒有好友代碼的玩家自動踢出","Кикнуть игроков у которых нет Кода Друга","Expulsar Jogadores Sem Código de Amigo" +"ApplyBanList","Apply BanList","BANリストを適用する","启用封禁名单","啟用封禁名單","Применить файл с забаненными игроками (BanList)","Aplicar Lista de Banimentos (BanList)" "## モード説明" "HideAndSeekInfo","Hide and Seek:\nNo emergency meetings. Crewmates (Blue) can only win by finishing tasks\nand impostors (Red) can only win by killing all crewmates.","かくれんぼ:\n会議を開くことはできず、クルーはタスク完了、インポスターは全クルー殺害でのみ勝利することができる。\nインポスターは赤、クルーは青に体の色が変更される。","躲猫猫:\n不能开会;船员只能通过完成任务获得胜利;\n内鬼需要杀死所有船员来获得胜利;\n内鬼的皮肤颜色将变为红色,船员变为蓝色,以示区分。","躲貓貓:\n不能拍桌或舉報,船員只能做任務\n偽裝者的勝利條件為殺光所有船員\n狼人的顏色會轉變為紅色,船員則會變為藍色。","Прятки:\nНикто не может созвать срочное собрание, Члены Экипажа могут победить, только выполняя задания. \nПредатели меняют цвет тела на красный, а экипажи на синий.","Esconde-Esconde:\nNão há reuniões de emergência. Tripulantes (Azul) podem ganhar apenas ao completar todas as tarefas,\ne Impostores (Vermelho) ganham apenas quando matarem todos os tripulantes." @@ -334,21 +354,21 @@ "CanVent","Can Vent","ベントを使える","可以使用通风管道","可以使用通風口","Может использовать Вентиляцию","Pode Usar Dutos" "ImpostorVision","Impostor Vision","インポスター視界","拥有内鬼视野","擁有偽裝者的視野","Имеет дальность Обзора Предателя","Visão (Impostor)" "CanUseSabotage","Can Sabotage","サボタージュを使用できる","可以破坏","可以破壞","Может использовать Саботаж","Pode Sabotar" -"CanCreateMadmate","Can Make SideKick Madmate","マッドメイトを指名できる","可以指名叛徒","可以招募叛徒","Он может назначить Безумцев","" +"CanCreateMadmate","Can Make SideKick Madmate","マッドメイトを指名できる","可以指名叛徒","可以招募叛徒","Он может назначить Безумцев","Pode Criar Tripulante Louco Ajudante" -"AssignMode","Assign Algorithm Mode","アサインモード","分配算法","","Алгоритм назначения","" -"AssignAlgorithm.Fixed","Fixed","固定","固定","","Фиксированный","" -"AssignAlgorithm.Random","Random","ランダム","随机","","Случайный","" -"RoleTypeMin","Minimum %roleType% Roles","%roleType%役職の最小人数","%roleType%职业的最小人数","","Минимум ролей для %roleType% ","" -"RoleTypeMax","Maximum %roleType% Roles","%roleType%役職の最大人数","%roleType%职业的最大人数","","Максимум ролей для %roleType%","" -"%roleTypes%Maximum","Max Players In %roleTypes%","%roleTypes%の最大アサイン数","%roleTypes%最大玩家数","","Максимум %roleTypes%","" -"FixedRole","Fixed Role","役職を固定","固定职业","","Фиксированная Роль","" -"Role","Role","役職","职业","","Роль","" +"AssignMode","Assign Algorithm Mode","アサインモード","分配算法","","Алгоритм назначения","Atribuir Modo do Algorítimo" +"AssignAlgorithm.Fixed","Fixed","固定","固定","","Фиксированный","Fixo" +"AssignAlgorithm.Random","Random","ランダム","随机","","Случайный","Aleatório" +"RoleTypeMin","Minimum %roleType% Roles","%roleType%役職の最小人数","%roleType%职业的最小人数","","Минимум ролей для %roleType% ","Mínimo de Classes %roleType%" +"RoleTypeMax","Maximum %roleType% Roles","%roleType%役職の最大人数","%roleType%职业的最大人数","","Максимум ролей для %roleType%","Máximo de Classes %roleType%" +"%roleTypes%Maximum","Max Players In %roleTypes%","%roleTypes%の最大アサイン数","%roleTypes%最大玩家数","","Максимум %roleTypes%","Máximo de Jogadores em %roleTypes%" +"FixedRole","Fixed Role","役職を固定","固定职业","","Фиксированная Роль","Classe Fixa" +"Role","Role","役職","职业","","Роль","Classe" "BountyTargetChangeTime","Time Until Target Swaps","ターゲット変更時間","赏金目标切换时间","賞金目標切換時間","Время смены цели","Tempo Para Troca de Alvos" "BountySuccessKillCooldown","Kill Cooldown After Killing Bounty","ターゲット殺害時のキルクール","赏金猎人击杀赏金目标的奖励冷却时间","賞金獵人殺死賞金目標冷卻","Перезарядка после убийства цели","Tempo de Recarga ao Matar Alvo" "BountyFailureKillCooldown","Kill Cooldown After Killing Others","ターゲット以外殺害時のキルクール","赏金猎人击杀赏金目标以外玩家的惩罚冷却时间","賞金獵人殺死非賞金目標冷卻","Перезарядка после обычного Убийства","Tempo de Recarga ao Matar Outros" -"BountyShowTargetArrow","Bounty hunter show arrow pointing to target","ターゲットへの矢印を表示する","赏金猎人的目标以箭头显示位置","賞金獵人獲得指向目標的箭頭","Показывать стрелку указывающую на Цель","" +"BountyShowTargetArrow","Bounty hunter show arrow pointing to target","ターゲットへの矢印を表示する","赏金猎人的目标以箭头显示位置","賞金獵人獲得指向目標的箭頭","Показывать стрелку указывающую на Цель","Mostrar Seta Apontando ao Alvo" "DefaultShapeshiftCooldown","Default Shapeshift Cooldown","デフォルトの変身クールダウン","默认变形冷却时间","預設變身時間","Обычная перезарядка Оборотня","Tempo de Recarga Padrão (Mutar)" "VampireKillDelay","Kill Delay(S)","殺害までの時間(秒)","吸血目标延迟死亡时间","殺人延遲","Длительность укуса(Секунды)","Atraso do Abate (S)" "MareAddSpeedInLightsOut","Mare Add Player Add Speed In Lights Out","停電時のメアーの加速値","梦魇熄灯时的额外速度","關燈時黑暗博士的額外速度","Скорость Ночного при Саботаже Света","Velocidade Adicional do Mare em Apagão" @@ -357,6 +377,7 @@ "CanMakeMadmateCount","Sidekick Madmate Max Count","サイドキックマッドメイト(人)","变形者可以招募叛徒的数量","變形者招募叛徒最大人數","Максимум союзников Безумца","Máximo de Tripulantes Loucos Ajudantes" "MadSnitchTasks","Mad Snitch Tasks","マッドスニッチのタスク数","背叛的告密者任务数","背叛告密者任務數量","Задания Безумного Стукача","Tarefas do Dedo-Duro Louco" "MadSnitchCanAlsoBeExposedToImpostor","Known to Impostors","インポスターからも視認できる","对内鬼同样可见","對偽裝者同樣可見","Также не защищен от Предателей","Visível Para Impostor" +"MadSnitchTaskTrigger","Tasks Until Boost Activated","効果を発動するタスク数","","","","" "MadGuardianCanSeeWhoTriedToKill","Can See Attempted Murderer","自身の殺害未遂者を知ることができる","背叛的守卫可以得知尝试对其击杀的玩家","背叛天使可以看到是誰嘗試殺害自己","Может видеть кто пытался его убить","Pode Ver Quem Tentou Matar" "MadmateCanFixLightsOut","Mad Roles Can Fix Lights","マッドメイト系役職が停電を直せる","叛徒系职业可以修理照明破坏","叛徒職業的玩家可以修理電燈","Безумцы могут чинить Свет","Loucos Podem Consertar Luzes" "MadmateCanFixComms","Mad Roles Can Fix Comms","マッドメイト系役職が通信障害を直せる","叛徒系职业可以修理通讯","叛徒職業的玩家可以修理通訊","Безумцы могут чинить Связь","Loucos Podem Consertar Comunicações" @@ -364,11 +385,13 @@ "MadmateCanSeeKillFlash","Mad Roles Can See ""Kill Flash""","マッドメイト系役職にキルフラッシュが見える","叛徒系职业可以看到击杀闪光","叛徒職業的玩家可以看到殺人閃光","Безумцы могут видеть Вспышку Убийства","Loucos Podem Ver ""Clarão de Abate""" "MadmateCanSeeOtherVotes","Mad Roles Can See Votes","マッドメイト系役職に他人の投票先が分かる","叛徒系职业可以看到其他人所投的票","叛徒可以看到所有人的投票","Безумцы могут видеть цвета Голосов","Loucos Podem Ver Votos" "MadmateCanSeeDeathReason","Madmates Can See Cause Of Death","マッドメイト系役職に死因が分かる","叛徒系职业可以看见死因","叛徒職業的玩家可以看到死因","Безумцы могут видеть Причины Смерти","Loucos Podem Ver Causa da Morte" -"MadmateExileCrewmate","Madmates Revenge A Crewmate When Exiled","マッドメイト系役職が追放時クルーを道連れにする","叛徒系职业在被放逐时复仇, 带走一名船员垫背","叛徒被丟出時會隨機拖一個船員一起下水","Безумцы сохраняют команду при изгнании","" +"MadmateExileCrewmate","Madmates Revenge A Crewmate When Exiled","マッドメイト系役職が追放時クルーを道連れにする","叛徒系职业在被放逐时复仇, 带走一名船员垫背","叛徒被丟出時會隨機拖一個船員一起下水","Безумцы сохраняют команду при изгнании","Loucos Vingam um Tripulante Quando Exilados" "MadmateVentCooldown","Mad Roles Vent Cooldown","マッドメイト系役職のベントクールダウン","叛徒系职业跳管道冷却时间","叛徒職業的玩家跳管道冷卻","Откат вентиляции Безумцев","Tempo de Recarga de Dutos dos Loucos" "MadmateVentMaxTime","Mad Roles Max Vent Duration","マッドメイト系役職のベント内での最大時間","叛徒系职业在管道中停留的最大时间","叛徒職業的玩家在管道中可以停留的最大時間","Время использования вентиляции Безумцев","Tempo Máximo de Loucos nos Dutos" -"LighterTaskCompletedVision","Increased Vision","タスク完了時の視界","完成任务后的视野","做完任務的視野","Дальность обзора","Aumento de Visão" +"LighterMaxVision","Max Vision","最大視界","完成任务后的视野","做完任務的視野","Дальность обзора","Aumento de Visão" "LighterTaskCompletedDisableLightOut","Ignore Fix Lights Effect","タスク完了時に停電を無効にする","完成任务的执灯人不受熄灯影响","完成任務的小燈人視野不受關燈影響","Имеет дальность Обзора Предателя","Ignorar Efeitos do Apagão" +"LighterTriggerType","Ability Activation Condition","能力発動条件","","","Условие активации способности","" +"LighterTaskTrigger","Tasks Until Boost Activated","効果を発動するタスク数","","","Количество задач повышающие скорость","" "SabotageMasterFixesDoors","Can Open Multiple Doors","1度に複数のドアを開けられる","修理大师打开多扇关闭的门","修理工可以一次性修理多扇門","Может открыть все двери","Pode Abrir Múltiplas Portas" "SabotageMasterFixesReactors","Can Fix Both Reactors","リアクターに対して能力を使える","修理大师可以一人修理核反应堆","修理工可以獨自修理兩邊的反應堆","Может починить саботаж Реактора","Pode Consertar Reatores" "SabotageMasterFixesOxygens","Can Fix Both O2","酸素妨害に対して能力を使える","修理大师修理氧气破坏时另一边的氧气设备将会被同时修理","修理工可以獨自修理兩邊的氧氣","Может починить саботаж O2","Pode Consertar Ambos O2" @@ -376,7 +399,7 @@ "SabotageMasterFixesElectrical","Can Fix Lights With One Switch","停電に対して能力を使える","修理大师按一个按钮就可以修复照明破坏","修理工可以快速完成修理電燈","Может починить саботаж Света одним кликом","Pode Consertar Luzes Com um Botão" "SheriffCanKill%role%","Can Kill %role%","%role%をキルできる","警长可以执法%role%","警長可以槍死%role%","Может убить %role%","Pode Matar %role%" "SheriffCanKillNeutrals","Can Kill Neutrals","ニュートラルをキルできる","警长可以执法独立阵营","警長可以槍死中立","Может убить Нейтралов","Pode Matar Neutros" -"SheriffCanKillAll","All ON","全てオン","全开","全開","Все ВКЛ","Todos Ligados" +"SheriffCanKillAll","All ON","全てオン","全开","全開","Все ВКЛ","Todos Ligados" "SheriffCanKillSeparately","Individual Settings","個別に設定","个别设定","個別設定","Выбрать кого","Configuração Individual" "In%team%","(Team %team%)","(%team%陣営)","(%team%阵营)","(%team%陣營)","(Команда %team%)","(Time %team%)" "SheriffMisfireKillsTarget","Misfire Kills Target","誤爆時、ターゲットも死ぬ","警长误杀好人会同时击杀目标","警長誤殺好人會同時擊殺目標","Шериф убивает цель вместе с собой","Pode Matar Alvo Incorreto" @@ -386,7 +409,7 @@ "SnitchEnableTargetArrow","Can See Arrow To Target","ターゲットを示す矢印が見える","告密者完成任务后可以通过箭头确认所有被发现目标","告密者完成任務後可以看到指向目標的箭頭","Может видеть стрелку цели","Pode Ver Seta Para o Alvo" "SnitchCanGetArrowColor","Can See Target Team Colored Arrow","矢印の色で陣営がわかる","对不同阵营的目标以不同颜色的箭头表示","對不同陣營的目標使用不同顏色的箭頭標示","Может видеть цвета стрелок","Pode Ver Seta Colorida Para Time Alvo" "SnitchCanFindNeutralKiller","Can Find Neutral Killers","ニュートラルのキル可能役職を見つけることが出来る","告密者也可以和拥有击杀能力的独立阵营玩家互相发现","告密者也可以和中立陣營帶刀職業互認","Может видеть Нейтральных Убийц","Pode Achar Assassinos Neutros" -"SnitchRemainingTaskFound","Remaining tasks to be found","敵陣営に見つかるタスク残量","剩余任务数对敌对阵营可见","帶刀職業可見告密者剩餘任務數","Оставшиеся задания при которых он будет виден","" +"SnitchRemainingTaskFound","Remaining tasks to be found","敵陣営に見つかるタスク残量","剩余任务数对敌对阵营可见","帶刀職業可見告密者剩餘任務數","Оставшиеся задания при которых он будет виден","Tarefas restantes para encontrar" "SpeedBoosterUpSpeed","Random Player's Speed Boost","加速値","增速者加速时的移动速度","被加速器加速的玩家的移動速度","Повысить скорость игрока на","Impulso de Velocidade" "SpeedBoosterTaskTrigger","Tasks Until Boost Activated","効果を発動するタスク数","效果发动所需任务数","效果發動所需任務數","Задачи повышающие скорость","Tarefas Para Ativar Impulso" "MayorAdditionalVote","Additional Votes Count","追加投票の個数","附加票数","附加票數","Дополнительные голоса","Votos Adicionais" @@ -394,7 +417,7 @@ "MayorNumOfUseButton","Number Of Mobile Emergency Button","ポータブルボタンの使用可能回数","市长紧急会议最大次数","隨時拍桌最大次數","Количество портативных Кнопок","Número de Botões de Emergência" "CanBeforeSchrodingerCatWinTheCrewmate","Can Win With Crewmates If No Team","役職変化前であれば、クルー陣営と勝利できる","薛定谔的猫未加入其他阵营前可以跟随船员阵营获胜","薛定諤的貓的陣營轉變前可以隨船員一起獲勝","Без команды он может победить с Членами Экипажа","Pode Ganhar Com Tripulantes" "SchrodingerCatExiledTeamChanges","Team Changes When Ejected","吊られた際、陣営が変化する","薛定谔的猫被放逐时会加入其他阵营","薛定諤的貓被丟出時陣營會轉變","Команда меняется после его Изгнания","Time Muda Quando Exilado" -"SchrodingerCatCanSeeKillableTeammate","Can See Killable Teammate","変化した陣営のキル役職が分かる","变更阵营后可见带刀职业","陣營轉變後可以看見帶刀的隊友","Может видеть всю команду в которой он состоит","" +"SchrodingerCatCanSeeKillableTeammate","Can See Killable Teammate","変化した陣営のキル役職が分かる","变更阵营后可见带刀职业","陣營轉變後可以看見帶刀的隊友","Может видеть всю команду в которой он состоит","Vê Aliado que Pode Ser Morto" "ExecutionerCanTargetImpostor","Can Target Impostors","インポスターもターゲットにできる","内鬼阵营玩家可以成为处刑人的目标","劊子手的目標可以是偽裝者陣營的玩家","Может иметь цель изгнать Предателя","Alvo Pode Ser Impostor" "ExecutionerCanTargetNeutralKiller","Can Target Neutrals","キルできるニュートラルもターゲットにできる","独立阵营玩家可以成为处刑人的目标","劊子手的目標可以是中立陣營的玩家","Может иметь цель изгнать Нейтрального Убийцу","Pode Ter Alvo Neutro" "ExecutionerChangeRolesAfterTargetKilled","Role Changes When Target Dies","ターゲットがキルされた後に変化する役職","处刑人目标死亡后将变为的职业","劊子手的目標被殺後轉變的職業","Роль после смерти его цели","Mudar Classe Caso Alvo Morra" @@ -406,29 +429,55 @@ "FireWorksRadius","Firework Explosion Radius","花火の爆発半径","烟花商人烟花爆炸半径","煙火工匠的煙火爆炸半徑","Радиус фейерверка","Alcance da Explosão" "SniperBulletCount","Ammo","所持弾数","最大子弹数量","子彈最大數量","Количество пуль","Balas" "SniperPrecisionShooting","Precision Firing","精密射撃モード","精准射击模式","精準射擊模式","Точный выстрел","Tiro Com Precisão" -"SniperAimAssist","Aim Assist","エイムアシスト","瞄准辅助","瞄準輔助","Помощь в прицеливании","" -"SniperAimAssistOneshot","One shot Assist","単発アシスト","一枪爆头辅助","單發瞄準","Помощь только с одним выстрелом","" +"SniperAimAssist","Aim Assist","エイムアシスト","瞄准辅助","瞄準輔助","Помощь в прицеливании","Assistência de Mira" +"SniperAimAssistOneshot","One shot Assist","単発アシスト","一枪爆头辅助","單發瞄準","Помощь только с одним выстрелом","Assistência de One Shot" "NumberOfLovers","Number Of Lovers (Pairs)","ラバーズの組数(x2人数)","恋人对数","戀人最大數量(2位玩家)","Количество Любовников (x2участника)","Quantidade de Amantes (Pares)" "TimeThiefDecreaseMeetingTime","Time Thief Time Stolen","減少する会議時間","蚀时者每次击杀缩短的会议时间","時間小偷每次殺人減少的會議時間","Уменьшить длительность обсуждения на","Tempo Roubado de Reunião" "TimeThiefLowerLimitVotingTime","Minimum Voting Time","投票時間の下限","蚀时者存活时会议时间最低下限","時間小偷在場時會議時間最低下限","Уменьшить длительность голосования на","Tempo Mínimo de Votação" "TimeThiefReturnStolenTimeUponDeath","Return Stolen Time After Death","死亡後に盗んだ時間を返す","蚀时者死亡后会议时间重置","時間小偷死亡後將會議時間重設","Вернуть украденное время после его смерти","Devolver Tempo Roubado ao Morrer" "EvilTrackerCanSeeKillFlash","Can See ""Kill Flash"" for Impostor Kills","インポスターキル時にフラッシュが見える","内鬼进行击杀时邪恶的追踪者可见击杀闪光","當狼人隊友殺人時邪惡的追蹤者可以看到閃光","Может видеть ''Вспышку Убийства''","Pode Ver ""Clarão"" Quando Impostor Matar" -"EvilTrackerTargetMode","Can Set Target","ターゲットの設定タイミング","目标更换时点","可以更換目標","Может установить цель","" -"EvilTrackerTargetMode.Never","Never","なし","不更换","關閉","Никогда","" -"EvilTrackerTargetMode.OnceInGame","Once In Game","試合毎","每局游戏一次","每局遊戲一次","В каждой игре","" -"EvilTrackerTargetMode.EveryMeeting","Every Meeting","ターン毎","每次会议","每回合","На каждой встрече","" -"EvilTrackerTargetMode.Always","Always","常時","一直","隨時","Всегда","" -"EvilTrackerCanSeeLastRoomInMeeting","Can See Target's Last Room In Meeting","会議中、追跡対象の最終位置を表示する","可以在会议时知晓追踪目标的最后停留房间","可以在會議中看到目標最後位置","Может видеть местоположение Целей во время Собрания","" +"EvilTrackerTargetMode","Can Set Target","ターゲットの設定タイミング","目标更换时点","可以更換目標","Может установить цель","Pode Definir Alvos" +"EvilTrackerTargetMode.Never","Never","なし","不更换","關閉","Никогда","Nunca" +"EvilTrackerTargetMode.OnceInGame","Once In Game","試合毎","每局游戏一次","每局遊戲一次","В каждой игре","Uma Vez no Jogo" +"EvilTrackerTargetMode.EveryMeeting","Every Meeting","ターン毎","每次会议","每回合","На каждой встрече","Toda Reunião" +"EvilTrackerTargetMode.Always","Always","常時","一直","隨時","Всегда","Sempre" +"EvilTrackerCanSeeLastRoomInMeeting","Can See Target's Last Room In Meeting","会議中、追跡対象の最終位置を表示する","可以在会议时知晓追踪目标的最后停留房间","可以在會議中看到目標最後位置","Может видеть местоположение Целей во время Собрания","Pode Ver a Última Posição do Alvo na Reunião" "KillFlashDuration","""Kill Flash"" Duration","キルフラッシュの長さ","击杀闪烁持续时间","殺人閃光持續時間","Длительность ""Вспышки Убийства""","Duração do ""Clarão de Abate""" -"WitchModeSwitchAction","Mode Switch Action","モード変更アクション","切换击杀模式","切換模式操作","Действие для Смены Режима","" -"TriggerKill","Kill","キル","击杀","殺人鍵","Убийство","" -"TriggerVent","Vent","ベント","通风管","通風口","Вентиляция","" -"TimeManagerIncreaseMeetingTime","Increase voting time","伸びる会議時間","延长投票的时间","延長時間","Увеличить время встреч на","" -"TimeManagerLimitMeetingTime","Increase limit","会議時間の伸びる限界","增加限制","會議時間延長上限","Лимит увеличения времени встреч","" -"TriggerDouble","Double Click","ダブルクリック","双击","同時觸發兩個技能冷卻","Двойное нажатие","" -"AssignOnlyTo%role%","Assign Only To %role%","%role%のみに割り当てる","只赋予%role%","","Назначить только для %role%","" -"WorkhorseNumLongTasks","Additional Long Tasks","追加ロングタスクの個数","额外长任务数","增加的長任務數量","Дополнительные долгие задания","" -"WorkhorseNumShortTasks","Additional Short Tasks","追加ショートタスクの個数","额外短任务数","增加的短任務數量","Дополнительные короткие задания","" +"WitchModeSwitchAction","Mode Switch Action","モード変更アクション","切换击杀模式","切換模式操作","Действие для Смены Режима","Trocar de Modo" +"TriggerKill","Kill","キル","击杀","殺人鍵","Убийство","Matar" +"TriggerVent","Vent","ベント","通风管","通風口","Вентиляция","Duto" +"TimeManagerIncreaseMeetingTime","Increase voting time","伸びる会議時間","延长投票的时间","延長時間","Увеличить время встреч на","Aumentar Tempo de Votação" +"TimeManagerLimitMeetingTime","Increase limit","会議時間の伸びる限界","增加限制","會議時間延長上限","Лимит увеличения времени встреч","Aumentar Limite" +"TriggerDouble","Double Click","ダブルクリック","双击","同時觸發兩個技能冷卻","Двойное нажатие","Duplo Clique" +"AssignOnlyTo%role%","Assign Only To %role%","%role%のみに割り当てる","只赋予%role%","","Назначить только для %role%","Atribuir Apenas a %role%" +"WorkhorseNumLongTasks","Additional Long Tasks","追加ロングタスクの個数","额外长任务数","增加的長任務數量","Дополнительные долгие задания","Adicionar Tarefas Longas" +"WorkhorseNumShortTasks","Additional Short Tasks","追加ショートタスクの個数","额外短任务数","增加的短任務數量","Дополнительные короткие задания","Adicionar Tarefas Curtas" +"StealthExcludeImpostors","Exclude Impostors From Blindness","暗転効果の対象からインポスターを除外する","","","Исключить Предателей из слепоты","" +"StealthDarkenDuration","Blindness Duration","暗転の持続時間","","","Длительность слепоты","" +"NekoKabochaImpostorsGetRevenged","Impostors Get Revenged","インポスターを道連れにする","","","Предатели могут мстить","" +"NekoKabochaMadmatesGetRevenged","Madmates Get Revenged","マッドメイトを道連れにする","","","Безумцы могут мстить","" +"NekoKabochaRevengeOnExile","Revenge When Exiled","追放された時に誰かを道連れにする","","","Месть во время изгнания","" +"EvilHackerCanSeeDeadMark","Can See The Location of Dead-bodies","死体位置がわかる","","","Может видеть местоположение трупов","" +"EvilHackerCanSeeImpostorMark","Can See The Location of Other Impostors","他のインポスターの位置がわかる","","","Может видеть местоположение других Предателей","" +"EvilHackerCanSeeKillFlash","Can See The Kill-flash for Impostor Kills","インポスターキル時にフラッシュが見える","","","Может видеть ''Вспышку Убийства''","" +"EvilHackerCanSeeMurderRoom","Can See The Murder Location","キルの発生場所がわかる","","","Может увидеть место убийства","" +"PenguinAbductTimerLimit","Dragging Time","引き摺れる時間","","","Время перетаскивания","" +"PenguinMeetingKill","Kill if meeting starts during dragging.","会議開始時に引き摺り中ならキルする","","","Убить если встреча начнется во время перетаскивания","" +"InsiderCanSeeImpostorAbilities","Can See Impostor Abilities","味方インポスターの能力が分かる","","","Может видеть роли других Предателей","" +"InsiderCanSeeAllGhostsRoles","Can See All Ghost's Roles","幽霊全員の役職が分かる","","","Может видеть все роли Призраков","" +"InsiderCanSeeMadmates","Can See Madmates","マッドメイトが分かる","","","Может видеть Безумцев","" +"InsiderKillCountToSeeMadmates","Kill Count To See Madmates","必要なキル数","","","Количество убийств при котором будет виден Безумец","" +"PlagueDoctorInfectLimit","Infect Count","感染回数","","","Количество заражений","" +"PlagueDoctorInfectWhenKilled","Infect When Killed","キルされた時に感染させる","","","Заразить убийцу при смерти заражённого","" +"PlagueDoctorInfectTime","Infect Time","感染に必要な時間","","","Время заражения","" +"PlagueDoctorInfectDistance","Infect Distance","感染する距離","","","Радиус заражения","" +"PlagueDoctorInfectInactiveTime","Infect Invalid Time","行動開始から感染しない時間","","","Недействительное время заражения","" +"PlagueDoctorCanInfectSelf","Can Infect Self","自身も感染する","","","Может заразить себя","" +"PlagueDoctorCanInfectVent","Can Infect in Vent","ベント内外でも感染する","","","Может заразить в вентиляции","" + +"## 能力発動条件" +"TaskProgressRate","Task Progress","タスク進捗率","","","Прогресс заданий","" +"TaskCount","Task Count","タスク数","","","Количество заданий","" "## かくれんぼ設定" "HideAndSeekOptions","Hide and Seek Settings","かくれんぼの設定","躲猫猫设置","躲貓貓設定","Настройки Пряток","Configurações do Esconde-Esconde" @@ -452,28 +501,33 @@ "DeathReason.Execution","Execution","処刑","处决","處刑","Казнен","Executado" "DeathReason.Disconnected","Disconnected","回線切断","断连","斷線","Вышел","Desconectado" "DeathReason.Fall","Fall","転落","摔死","摔死","Упал","Queda" -"DeathReason.Revenge","Revenge","道連れ","复仇","復仇","Месть","" +"DeathReason.Revenge","Revenge","道連れ","复仇","復仇","Месть","Vingança" +"DeathReason.Infected","Infected","感染","","","Заражён","" "DeathReason.etc","Other","その他","其他","其他","Другое","Outros" "Alive","Alive","生存","存活","存活","Выжил","Vivo" "Win"," Wins","勝利","胜利","勝利"," Победили","Vitória" -"Last-","Last ","ラスト","仅存","獨活","Последний ","" +"Last-","Last ","ラスト","仅存","獨活","Последний ","Último" "## リアクターの時間制御" "SabotageTimeControl","Sabotage Duration Control","サボタージュの時間制御","更改修理时限","重新設定緊急任務時間","Изменить время Саботажа","Controle da Duração de Sabotagem" "PolusReactorTimeLimit","Polus Reactor Duration","ポーラスのリアクター制限時間","波鲁斯抗震稳定器修理时限","Polus地震抑制器破壞最大時間","Polus время саботажа Реактора","Duração do Reator em Polus" "AirshipReactorTimeLimit","Airship Reactor Duration","エアシップのリアクター制限時間","飞艇坠毁路线修理时限","Airship間隙室破壞最大時間","Airship время саботажа Реактора","Duração do Reator em Airship" +"## サボタージュのクールダウン変更" +"ModifySabotageCooldown","Sabotage Cooldown Control","サボタージュのクールダウン制御","","","Контролировать откат саботажа","" +"SabotageCooldown","Sabotage Cooldown","サボタージュのクールダウン","","","Откат саботажа","" + "## クライアント設定" -"Close","Close","閉じる","关闭","","Закрыть","" -"TOHOptions","TOH Options","TOHの設定","TOH 选项","","Настройки TOH","" +"Close","Close","閉じる","关闭","","Закрыть","Fechar" +"TOHOptions","TOH Options","TOHの設定","TOH 选项","","Настройки TOH","TOH Opções" "ForceJapanese","Force Japanese","日本語に強制","强制使用日语","強制使用日語","Принудительный Японский","Forçar Japonês" -"JapaneseRoleName","Japanese Role Name","日本語の役職名","用日语显示职业名","","Японские названия ролей","" -"UnloadMod","Disable The Mod","Modを無効化","切换为原版","","Отключить мод","" -"UnloadWarning","Warning\n\nTo re-enable the mod, you must restart the game. Would you like to continue anyway?","警告\n\nModを再び有効化するにはゲームの再起動が必要です。本当に無効化しますか?","警告:\n\n切换后需要重启游戏来恢复模组,确定要切换原版吗?","","Внимание!\n\nДля повторного включения мода необходимо перезапустить игру. Вы все равно хотите продолжить?","" -"CannotUnloadDuringGame","The mod cannot be disabled during a game.","試合中はModを無効化できません","游戏中不能切换原版","","Мод нельзя отключить во время игры.","" -"Cancel","Cancel","キャンセル","取消","","Отменить","" -"Unload","DISABLE","無効化","确认","","ОТКЛЮЧИТЬ","" -"DumpLog","Output Log","ログを出力","输出日志","","Вывести Журнал","" +"JapaneseRoleName","Japanese Role Name","日本語の役職名","用日语显示职业名","","Японские названия ролей","Nomes de Classes Japoneses" +"UnloadMod","Disable The Mod","Modを無効化","切换为原版","","Отключить мод","Desabilitar o Mod" +"UnloadWarning","Warning\n\nTo re-enable the mod, you must restart the game. Would you like to continue anyway?","警告\n\nModを再び有効化するにはゲームの再起動が必要です。本当に無効化しますか?","警告:\n\n切换后需要重启游戏来恢复模组,确定要切换原版吗?","","Внимание!\n\nДля повторного включения мода необходимо перезапустить игру. Вы все равно хотите продолжить?","Aviso\n\nPara reativar o mod, você precisará reiniciar o jogo. Deseja continuar assim mesmo?" +"CannotUnloadDuringGame","The mod cannot be disabled during a game.","試合中はModを無効化できません","游戏中不能切换原版","","Мод нельзя отключить во время игры.","O mod não pode ser desabilitado durante um jogo." +"Cancel","Cancel","キャンセル","取消","","Отменить","Cancelar" +"Unload","DISABLE","無効化","确认","","ОТКЛЮЧИТЬ","DESATIVAR" +"DumpLog","Output Log","ログを出力","输出日志","","Вывести Журнал","Log de Saída" "## ヘルプテキスト" "CommandList","Command List:","コマンド一覧:","指令列表:","指令列表:","Список команд:","Lista de Comandos:" @@ -500,31 +554,31 @@ "Message.Executed","{0} was executed","{0}を処刑しました","{0}被房主处决了","{0}被處刑了","{0} Был казнен","{0} foi executado" "Message.HideGameSettings","Game settings have been hidden by the host.","ゲーム設定はホストによって秘匿されています。","游戏设置被房主隐藏。","遊戲設定被房主隱藏。","Настройки игры скрыты хостом","As configurações do jogo foram escondidas pelo anfitrião." "Message.NoDescription","No description.","説明はありません。","无描述","無說明。","Описание отсутствует","Sem descrição." -"Message.KickedByDenyName","{0} was kicked because its name matched ""{1}"".","{0}は名前が「{1}」に一致したためキックされました。","{0} 被踢出,因其昵称违规 ""{1}"".","因為{0}的名字和不良名字清單中的「{1}」相符,所以踢出了他。","{0} был кикнут, так как его имя соответствует ''{1}''","" -"Message.BanedByBanList","{0} has been banned because it has been banned in the past.","{0}は過去にBAN済みのためBANされました。","{0} 已被封禁,因其之前就被封禁过","因為{0}在被記錄在黑名單中,所以禁止了{0}再次進入此房間。","{0} был заблокирован, потому что он был заблокирован в прошлый раз","" -"Message.KickedByNoFriendCode","{0} was kicked because the friend code does not exist.","{0}はフレンドコードが存在しないためキックされました。","{0}被踢出,因其好友编号无效。","{0}因為沒有好友代碼,所以踢出了他。","{0} был кикнут, так как у него нет кода друга","" -"Message.AddedPlayerToBanList","Added {0} to the ban list.","{0}をBANリストに追記しました。","{0}已被添加到封禁名单","已將 {0} 加入到黑名單中,此玩家將無法再進入你的房間。(需安裝TOH模組)","{0} был добавлен в список забаненых игроков","" -"Message.FailedToLoadOptions","Failed to load options","オプションの読み込みに失敗しました","","","","" -"Message.CopiedOptions","Copied options","オプションデータをコピーしました","","","","" -"Message.ExportedOptions","Exported options","オプションデータを出力しました","","","","" -"Message.LoadedOptions","Loaded options","オプションデータを読み込みました","","","","" -"Message.OnlyHostCanLoadOptions","Only the host can load options","ホストのみがオプションを読み込めます","","","","" +"Message.KickedByDenyName","{0} was kicked because its name matched ""{1}"".","{0}は名前が「{1}」に一致したためキックされました。","{0} 被踢出,因其昵称违规 ""{1}"".","因為{0}的名字和不良名字清單中的「{1}」相符,所以踢出了他。","{0} был кикнут, так как его имя соответствует ''{1}''","{0} foi expulso porque seu nome coincide com ""{1}""." +"Message.BanedByBanList","{0} has been banned because it has been banned in the past.","{0}は過去にBAN済みのためBANされました。","{0} 已被封禁,因其之前就被封禁过","因為{0}在被記錄在黑名單中,所以禁止了{0}再次進入此房間。","{0} был заблокирован, потому что он был заблокирован в прошлый раз","{0} foi banido por já ter sido banido antes." +"Message.KickedByNoFriendCode","{0} was kicked because the friend code does not exist.","{0}はフレンドコードが存在しないためキックされました。","{0}被踢出,因其好友编号无效。","{0}因為沒有好友代碼,所以踢出了他。","{0} был кикнут, так как у него нет кода друга","{0} foi expulso, pois o código de amigo não existe." +"Message.AddedPlayerToBanList","Added {0} to the ban list.","{0}をBANリストに追記しました。","{0}已被添加到封禁名单","已將 {0} 加入到黑名單中,此玩家將無法再進入你的房間。(需安裝TOH模組)","{0} был добавлен в список забаненых игроков","{0} foi adicionado a lista de banidos." +"Message.FailedToLoadOptions","Failed to load options","オプションの読み込みに失敗しました","","","Не удалось загрузить настройки","Erro ao carregar as opções" +"Message.CopiedOptions","Copied options","オプションデータをコピーしました","","","Скопированные настройки","Opções copiadas" +"Message.ExportedOptions","Exported options","オプションデータを出力しました","","","Экспортированные настройки","Opções exportadas" +"Message.LoadedOptions","Loaded options","オプションデータを読み込みました","","","Загруженные настройки","Opções carregadas" +"Message.OnlyHostCanLoadOptions","Only the host can load options","ホストのみがオプションを読み込めます","","","Только хост может загружать настройки","Apenas o anfitrião pode carregar opções" "## 警告" "Warning.EgoistCannotWin","Egoist cannot win","エゴイストが勝利できません","野心家无法获胜","利己主義者無法獲勝","Эгоист не может победить!","Egoísta não pode vencer" -"Warning.OverrideExiledPlayer","All ejects will be displayed as a tie since the total number of impostors is 1.","合計インポスター数が1のため、追放はすべて同数投票として表示されます。","由于场上仅剩一名内鬼,放逐投票将显示为平票","由於場上只剩一名偽裝者,所有逐出訊息都將顯示為平票","Все изгнания будут отображаться как ничья, потому что общее количество Предателей равно 1.","Todas as ejeções serão mostradas como empate, pois o total de Impostores é 1." +"Warning.OverrideExiledPlayer","All ejects will be displayed as a tie since the jackals are in effect.","ジャッカルがいるため、追放はすべて同数投票として表示されます。","","","Из-за того что в игре есть Шакал(ы) все изгнания будут видны как ничья (но только визуально, на результат голосования это не влияет)","" "Warning.InvalidRpc","Kicked {0} because an invalid RPC was received.\nPlease check that no mods other than TOH installed.","不正なRPCを受信したため{0}をキックしました。\nTOH以外のMODが入っていないか確認してください。","{0} 被踢出,因其 RPC 无效。 \n请确保没有 TOH 以外的模组。","{0}因為收到無效的RPC,所以踢出了他,\n請確保遊戲除了TOH之外沒有其他模組系統被載入","{0} Был кикнут так как получен недопустимый RPC. \nУбедитесь что в системе нет других модов кроме TOH.","{0} Foi expulso, porque um RPC inválido foi recebido. \nPor favor verifique se nenhum outro mod além de TOH está instalado." -"Warning.NoModHost","No mod installed on the host","ホストにMODが導入されていません","该房主并未安装mod","該房房主沒有安裝Town Of Host模組","У Хоста Лобби не установлен Town of Host","" -"Warning.MismatchedVersion","{0}\nhave a different version of {1}","{0} の\n{1} のバージョンがホストと合致しません。","{0} \n的游戏版本为{1},与主机版本不符。","","{0} Версия \nне соответствует версии Хоста лобби {1}","" -"Warning.AutoExitAtMismatchedVersion","The host has no or a different version of {0}\nYou will be kicked in {1}","{0} のバージョンがホストと合致しません。\nあと {1} 秒で切断されます。","{0} 的版本与主机不符。\n 将在 {1} 秒后被踢出。","","Версия {0} не соответствует Хосту лобби. \nВы будете исключены через {1} секунды","" -"Warning.NotMatchImpostorCount","Max/min number of Impostor roles is greater than the number of Impostors.","インポスター役職の最小/最大割り当て人数がインポスターより多すぎます。","内鬼数量过小或过大","","Макс./Мин. количество Предательских ролей больше, чем общее количество Предателей.","" -"Warning.NotMatchRoleCount","The role is not cast correctly because the minimum number of roles is greater than the number of players.","ロールの最少人数合計がプレイヤーより多いため正常に配役されません。","职业数量设置大于玩家数量","","Роль подобрана неправильно, так как минимальное количество ролей больше, чем количество игроков.","" +"Warning.NoModHost","No mod installed on the host","ホストにMODが導入されていません","该房主并未安装mod","該房房主沒有安裝Town Of Host模組","У Хоста Лобби не установлен Town of Host","O anfitrião não tem o mod instalado" +"Warning.MismatchedVersion","{0}\nhave a different version of {1}","{0} の\n{1} のバージョンがホストと合致しません。","{0} \n的游戏版本为{1},与主机版本不符。","","{0} Версия \nне соответствует версии Хоста лобби {1}","{0}\ntem uma versão diferente de {1}" +"Warning.AutoExitAtMismatchedVersion","The host has no or a different version of {0}\nYou will be kicked in {1}","{0} のバージョンがホストと合致しません。\nあと {1} 秒で切断されます。","{0} 的版本与主机不符。\n 将在 {1} 秒后被踢出。","","Версия {0} не соответствует Хосту лобби. \nВы будете исключены через {1} секунды","O anfitrião não tem ou tem uma versão diferente de {0}\nVocê será expulso em {1}" +"Warning.NotMatchImpostorCount","Max/min number of Impostor roles is greater than the number of Impostors.","インポスター役職の最小/最大割り当て人数がインポスターより多すぎます。","内鬼数量过小或过大","","Макс./Мин. количество Предательских ролей больше, чем общее количество Предателей.","O número max/min de classes de impostor é maior que o número de impostores." +"Warning.NotMatchRoleCount","The role is not cast correctly because the minimum number of roles is greater than the number of players.","ロールの最少人数合計がプレイヤーより多いため正常に配役されません。","职业数量设置大于玩家数量","","Роль подобрана неправильно, так как минимальное количество ролей больше, чем количество игроков.","A classe não foi lançada corretamente porque o número mínimo de funções é maior que o número de jogadores." "## エラー" "Error.MeetingException","Error: {0}\r\nPlease use SHIFT+M+ENTER to end the meeting","エラー: {0}\r\nSHIFT+M+ENTERで会議を強制終了してください","错误: {0}\r\n使用 SHIFT+M+ENTER 来中止会议","錯誤: {0}\r\n按下SHIFT+M+ENTER來結束會議","Ошибка: {0}\r\nПожалуйста используйте SHIFT+M+ENTER чтобы принудительно завершить собрание","Erro: {0}\r\nPor favor use SHIFT+M+ENTER para encerrar a reunião" "Error.InvalidRoleAssignment","Error: Invalid role found for a player during role assignment({0})","エラー: 役職設定中に無効な役職のプレイヤーを発見しました({0})","错误:在分配职业时发现职业无效的玩家 ({0})","錯誤:在設定職業時發現{0}持有無效職業","Ошибка: Во время назначения роли для игрока обнаружена недопустимая роль({0})","Erro: Classe inválida encontrada para um jogador no momento de dar as classes ({0})" "Error.SpeedBoosterNullException","Error: The speed boost destination is null.\nPlease save the log and create a bug report ticket.","エラー: スピードブースト先がnullです。\nログを保存し、バグ報告チケットを作成してください。","错误:增速者的增速目标为空。 \n请保存日志并创建错误报告票。","錯誤:加速器加速的對象無效,\n請將遊戲輸出記錄保存並在TOH官方DC群組開啟一張支援票","Ошибка: пункт назначения повышения скорости не указан. \nСохраните логи и создайте заявку на отчет об ошибке.","Erro: O destino do Impulso de Velocidade é nulo.\nPor favor, salve o log e crie um ticket de erro (bug report)." -"Error.InvalidColor","Error: Only default colors are available.","エラー:デフォルトカラー以外は使えません","错误: 仅默认颜色可用","錯誤: 無法使用遊戲自帶以外的顏色。","Ошибка: Нельзя использовать другие цвета, кроме цветов по умолчанию","" +"Error.InvalidColor","Error: Only default colors are available.","エラー:デフォルトカラー以外は使えません","错误: 仅默认颜色可用","錯誤: 無法使用遊戲自帶以外的顏色。","Ошибка: Нельзя использовать другие цвета, кроме цветов по умолчанию","Erro: Apenas as cores padrão estão disponíveis." "### ErrorText関連" "ErrorLevel1","Bugs may occur.","何らかのバグが発生する可能性があります。","可能同时产生多个bug","可能同時產生多個Bug","Могут возникнуть некоторые ошибки.","Bugs podem acontecer." @@ -537,15 +591,15 @@ "ERR-000-910-1","Test Error Lv.1","テストエラーLv1","测试错误Lv1","測試錯誤Lv1","Ошибка теста - Lv1","Erro de Teste Nvl.1" "ERR-000-920-2","Test Error Lv.2","テストエラーLv2","测试错误Lv2","測試錯誤Lv2","Ошибка теста - Lv2","Erro de Teste Nvl.2" "ERR-000-930-3","Test Error Lv.3","テストエラーLv3","测试错误Lv3","測試錯誤Lv3","Ошибка теста - Lv3","Erro de Teste Nvl.3" -"ERR-000-804-1","TownOfHost does not support the Vanilla HnS, so unloaded.","現在バニラのHide And Seekはサポートされていません。modを無効化しました。","TOH不支持原版躲猫猫, mod因此失效","Town Of Host模組尚未支援原版的Hide And Seek模式,所以模組將暫時卸載。","Мод не поддерживает обычные Прятки. Мод был отключён","" +"ERR-000-804-1","TownOfHost does not support the Vanilla HnS, so unloaded.","現在バニラのHide And Seekはサポートされていません。modを無効化しました。","TOH不支持原版躲猫猫, mod因此失效","Town Of Host模組尚未支援原版的Hide And Seek模式,所以模組將暫時卸載。","Мод не поддерживает обычные Прятки. Мод был отключён","TownOfHost não suporta o Esconde-Esconde padrão, então foi descarregado." "#### 001 Main" "ERR-001-000-3","Main dictionary has duplicated keys.","MainのDictonaryでKeyの重複が発生しています。","在主目录出现重复密钥","在主目錄出現重複密鑰","Дублирование ключей происходит в основном словаре.","Dicionário principal tem chaves duplicadas." "#### 002 Support" -"ERR-002-000-1","Unsupported AmongUs version. Please update.","サポートされていないAmongUsバージョンです。ゲームをアップデートしてください。","","","Неподдерживаемая версия AmongUs. Пожалуйста, обновите игру","" +"ERR-002-000-1","Unsupported AmongUs version. Please update.","サポートされていないAmongUsバージョンです。ゲームをアップデートしてください。","","","Неподдерживаемая версия AmongUs. Пожалуйста, обновите игру","Versão não suportada do Among Us. Por favor, atualize." "## その他" "DefaultSystemMessageTitle","【===== System Message =====】","【===== システムメッセージ ======】","【===== 系统信息 ======】","【===== 系統訊息 ======】","【=== Системное сообщение ===】","【===== Mensagem do Sistema ======】" -"MessageFromTheHost","【Message From The Host】","【ホストからの伝言】","【房主消息】","【房主訊息】","【Сообщение от Хоста】","" +"MessageFromTheHost","【Message From The Host】","【ホストからの伝言】","【房主消息】","【房主訊息】","【Сообщение от Хоста】","【Mensagem do Anfitrião】" "TabGroup.MainSettings","Main Settings","メイン設定","主要设置","主要設定","Основная настройка","Configurações Principais" "TabGroup.CrewmateRoles","Crewmate Roles","クルー役職","船员阵营职业","船員職業","Роли Членов Экипажа","Classes de Tripulante" "TabGroup.NeutralRoles","Neutral Roles","ニュートラル役職","独立阵营职业","中立職業設定","Нейтральные Роли","Classes Neutras" @@ -568,7 +622,7 @@ "onSetPublicNoLatest","Public rooms are only available in the latest version.\nPlease update.","最新版以外で公開ルームにはできません。\nアップデートをしてください。","不可以在未安装最新模组的前提下创建公开房间。\n请更新本模组。","無法建立一個公共房間,因為使用的不是最新版本的模組\n請更新本模組。","Публичные лобби запрещены за исключением последней версии.\nПожалуйста обновите.","Salas públicas estão disponíveis apenas na última versão.\nPor favor atualize." "CanNotJoinPublicRoomNoLatest","You can't join public rooms without the latest version.\nPlease update.","最新版以外で公開ルームには参加できません。\nアップデートをしてください。","不可以在未安装最新模组的前提下进入公开房间。\n请更新本模组。","無法在未安裝最新版本的前提下進入公開房間\n請更新本模組","Вы не можете присоединяться к публичным лобби за исключением последней версии.\nПожалуйста обновите.","Você não pode entrar em salas públicas sem a última versão.\nPor favor atualize." "ModBrokenMessage","The MOD file is damaged.\nPlease reinstall.","MODを構成するファイルが破損しています。\nもう一度導入しなおしてください。","模组文件损坏。\n请重新安装本模组。","模組檔案損壞\n請重新安裝模組","Мод был поврежден.\nПожалуйста переустановите его снова.","Os arquivos do MOD estão danificados.\nPor favor reinstale." -"UnsupportedVersion","Unsupported AmongUs version.\nPlease update.","サポートされていないAmongUsバージョンです。\nゲームをアップデートしてください。","","","Неподдерживаемая версия AmongUs.\nПожалуйста, обновите игру","" +"UnsupportedVersion","Unsupported AmongUs version.\nPlease update.","サポートされていないAmongUsバージョンです。\nゲームをアップデートしてください。","","","Неподдерживаемая версия AmongUs.\nПожалуйста, обновите игру","Versão não suportada do Among Us. \nPor favor, atualize." "DisabledByProgram","Public rooms have been disabled by the program","公開ルームはプログラムによって無効化されています","公开房间的操作已被程序禁用","公開房間的操作已經被程式禁用","Создание публичного лобби отключена программой","Salas públicas foram desativadas pelo programa" "EnterVentToWin","Enter Vent to Win!!","ベントに入って勝利しろ!!","跳管道来获得胜利!","跳管道來獲得勝利!!","Запрыгните в вентиляцию чтобы победить!!","Entre no Duto Para Ganhar!!!" "FireworksPutPhase","{0} Fireworks Left","あと{0}個置け","还要安放{0}枚烟花","還需要安裝{0}枚煙火","Осталось {0} Фейерверков","Restam {0} Fogos de Artifício" @@ -579,19 +633,20 @@ "InvalidArgs","Invalid Args","無効な引数","无效参数","無效的參數","Недопустимые Аргументы","Argumento Inválido" "On","ON","オン","开启","開啟","ВКЛ","Ativado" "Off","OFF","オフ","关闭","關閉","ВЫКЛ","Desativado" -"ColoredOn","ON","オン","开启","開啟","ВКЛ","Ativado" +"ColoredOn","ON","オン","开启","開啟","ВКЛ","Ativado" "ColoredOff","OFF","オフ","关闭","關閉","ВЫКЛ","Desativado" "CurrentActiveSettingsHelp","Current Active Settings Help","現在有効な設定の説明","当前启用设置及帮助","現在設定的模式和職業說明","Справка по текущим активным настройкам","Ajuda com Configurações Ativas" "WitchCurrentMode","Current Mode:","現在のモード:","当前模式:","現在模式:","Текущий Режим: ","Modo Atual:" "WitchModeKill","Kill","キル","击杀","殺人","Убить","Matar" "WitchModeSpell","Spell","スペル","诅咒","下咒","Заклясть","Feitiço" -"WitchModeDouble","Double Enable","ダブル有効","双重有效","兩個模式同時啟用","Двойной","" +"WitchModeDouble","Double Enable","ダブル有効","双重有效","兩個模式同時啟用","Двойной","Duplo Ativado" "BountyCurrentTarget","Current Target","現在のターゲット","当前目标","目標","Текущая цель","Alvo Atual" +"StealthDarkened","Darkened: {0}","暗転中: {0}","","","Затемнено: {0}","" "Roles","Roles","役職","职业","職業","Роли","Classes" "Settings","Settings","設定","设定","設定","Настройки","Configurações" "Addons","Add-Ons","属性","附加效果","屬性","Атрибут","Atributos" "LastResult","Match Results","試合結果","游戏结果","上一局的遊戲結果","Результат матча","Resultados da Partida" -"KillLog","Kill Log","キル履歴","击杀日志","擊殺紀錄","История убийств","" +"KillLog","Kill Log","キル履歴","击杀日志","擊殺紀錄","История убийств","Log de Mortes" "Maximum","Max","最大数","最多人数","最大數量","Максимум","Máximo" "Rate0","0%","0%","0%","0%","0%","0%" "Rate5","5%","5%","5%","5%","5%","5%" @@ -637,8 +692,14 @@ "ArsonistDouseButtonText","Douse","塗る","涂油","澆油","Облить","Encharcar" "PuppeteerOperateButtonText","Manipulate","操る","操控","操控","Управлять","Manipular" "BountyHunterChangeButtonText","Swap","変更","变更","變更","Смена цели","Trocar" -"EvilTrackerChangeButtonText","Track","追跡","追踪","追蹤","Отслеживать","" +"EvilTrackerChangeButtonText","Track","追跡","追踪","追蹤","Отслеживать","Rastrear" "DefaultShapeshiftText","Shift","変身","变形","變身","Превращение","Mutar" "DisabledBySettings","Disabled by Settings","設定で無効化されています","已在设置被中禁用","在設定中被禁用","Отключено настройкой","Desativado Pelas Configurações" "Disabled","Disabled","無効","禁用","已被禁用","Отключено","Desativado" -"FailToTrack","Failed To Track","追跡失敗","追踪失败","無法追蹤","Не удалось отследить","" +"FailToTrack","Failed To Track","追跡失敗","追踪失败","無法追蹤","Не удалось отследить","Falha ao Rastrear" +"LastAdminInfo","Last-minute admin information","直前のアドミン情報","","","Актуальная информация администратора","" +"MurderNotify","Murder","キル発生","","","Убийство","" +"Deadbody","DEAD","死体","","","Труп","" +"PenguinKillButtonText","Drag","拉致","","","ПЕРЕТАСКИВАТЬ","" +"PenguinTimerText","Drag Timer","残り時間","","","Время перетаскивания","" +"Infected","Infected","感染","","","Заражён","" diff --git a/Roles/AddOns/Common/AddOnsAssignData.cs b/Roles/AddOns/Common/AddOnsAssignData.cs index bb7833109..1ea75b13d 100644 --- a/Roles/AddOns/Common/AddOnsAssignData.cs +++ b/Roles/AddOns/Common/AddOnsAssignData.cs @@ -24,11 +24,7 @@ public class AddOnsAssignData static readonly CustomRoles[] InvalidRoles = { CustomRoles.GuardianAngel, - CustomRoles.CSchrodingerCat, CustomRoles.SKMadmate, - CustomRoles.MSchrodingerCat, - CustomRoles.EgoSchrodingerCat, - CustomRoles.JSchrodingerCat, CustomRoles.HASFox, CustomRoles.HASTroll, CustomRoles.GM, diff --git a/Roles/AddOns/Impostor/LastImpostor.cs b/Roles/AddOns/Impostor/LastImpostor.cs index 8b2de9801..5d24e1072 100644 --- a/Roles/AddOns/Impostor/LastImpostor.cs +++ b/Roles/AddOns/Impostor/LastImpostor.cs @@ -12,7 +12,7 @@ public static class LastImpostor public static OptionItem KillCooldown; public static void SetupCustomOption() { - SetupSingleRoleOptions(Id, TabGroup.Addons, CustomRoles.LastImpostor, 1); + SetupRoleOptions(Id, TabGroup.Addons, CustomRoles.LastImpostor, new(1, 1, 1)); KillCooldown = FloatOptionItem.Create(Id + 10, "KillCooldown", new(0f, 180f, 1f), 15f, TabGroup.Addons, false).SetParent(CustomRoleSpawnChances[CustomRoles.LastImpostor]) .SetValueFormat(OptionFormat.Seconds); } diff --git a/Roles/Core/CustomRoleManager.cs b/Roles/Core/CustomRoleManager.cs index 92bdf2c07..02baaa528 100644 --- a/Roles/Core/CustomRoleManager.cs +++ b/Roles/Core/CustomRoleManager.cs @@ -373,15 +373,19 @@ public enum CustomRoles Witch, Warlock, Mare, + Penguin, Puppeteer, TimeThief, EvilTracker, + Stealth, + NekoKabocha, + EvilHacker, + Insider, //Madmate MadGuardian, Madmate, MadSnitch, SKMadmate, - MSchrodingerCat,//インポスター陣営のシュレディンガーの猫 //Crewmate(Vanilla) Engineer, GuardianAngel, @@ -399,18 +403,16 @@ public enum CustomRoles Doctor, Seer, TimeManager, - CSchrodingerCat,//クルー陣営のシュレディンガーの猫 //Neutral Arsonist, Egoist, - EgoSchrodingerCat,//エゴイスト陣営のシュレディンガーの猫 Jester, Opportunist, - SchrodingerCat,//無所属のシュレディンガーの猫 + PlagueDoctor, + SchrodingerCat, Terrorist, Executioner, Jackal, - JSchrodingerCat,//ジャッカル陣営のシュレディンガーの猫 //HideAndSeek HASFox, HASTroll, diff --git a/Roles/Core/Interfaces/IImpostor.cs b/Roles/Core/Interfaces/IImpostor.cs index c3b94e758..221738281 100644 --- a/Roles/Core/Interfaces/IImpostor.cs +++ b/Roles/Core/Interfaces/IImpostor.cs @@ -1,13 +1,30 @@ +using AmongUs.GameOptions; +using TownOfHost.Roles.Neutral; + namespace TownOfHost.Roles.Core.Interfaces; /// /// インポスターのインタフェイス
/// を継承 ///
-public interface IImpostor : IKiller +public interface IImpostor : IKiller, ISchrodingerCatOwner { /// /// ラストインポスターになれるかどうか デフォルトtrue /// public bool CanBeLastImpostor => true; + /// + /// シュレディンガーの猫を切った際の変化先役職
+ /// デフォルト + ///
+ SchrodingerCat.TeamType ISchrodingerCatOwner.SchrodingerCatChangeTo => SchrodingerCat.TeamType.Mad; + + /// + /// この役職に切られたシュレディンガーの猫へのオプション変更
+ /// デフォルト + ///
+ void ISchrodingerCatOwner.ApplySchrodingerCatOptions(IGameOptions option) + { + SchrodingerCat.ApplyMadCatOptions(option); + } } diff --git a/Roles/Core/Interfaces/INekomata.cs b/Roles/Core/Interfaces/INekomata.cs new file mode 100644 index 000000000..e6549add8 --- /dev/null +++ b/Roles/Core/Interfaces/INekomata.cs @@ -0,0 +1,20 @@ +namespace TownOfHost.Roles.Core.Interfaces; + +/// +/// 追放されたときに誰かを道連れにする役職 +/// +public interface INekomata +{ + /// + /// 道連れが発動するかのチェック + /// + /// 猫又の死因 + /// 道連れを発生させるならtrue + public bool DoRevenge(CustomDeathReason deathReason); + /// + /// プレイヤーが道連れ対象の候補に含まれるかどうかのチェック + /// + /// 判定するプレイヤー + /// playerを道連れ対象候補に含ませるならtrue + public bool IsCandidate(PlayerControl player); +} diff --git a/Roles/Core/Interfaces/ISchrodingerCatOwner.cs b/Roles/Core/Interfaces/ISchrodingerCatOwner.cs new file mode 100644 index 000000000..ce152f022 --- /dev/null +++ b/Roles/Core/Interfaces/ISchrodingerCatOwner.cs @@ -0,0 +1,25 @@ +using AmongUs.GameOptions; +using TownOfHost.Roles.Neutral; + +namespace TownOfHost.Roles.Core.Interfaces; + +/// +/// シュレディンガーの猫をキルして仲間に引き入れる事ができる役職のインタフェイス +/// +public interface ISchrodingerCatOwner +{ + /// + /// シュレディンガーの猫を切った際の変化先役職 + /// + public SchrodingerCat.TeamType SchrodingerCatChangeTo { get; } + /// + /// この役職に切られたシュレディンガーの猫へのオプション変更
+ /// デフォルトではなにもしない + ///
+ public void ApplySchrodingerCatOptions(IGameOptions option) { } + + /// + /// シュレディンガーの猫をキルした際に追加で実行するアクション + /// + public void OnSchrodingerCatKill(SchrodingerCat schrodingerCat) { } +} diff --git a/Roles/Core/RoleBase.cs b/Roles/Core/RoleBase.cs index 5b263ce92..438723748 100644 --- a/Roles/Core/RoleBase.cs +++ b/Roles/Core/RoleBase.cs @@ -187,12 +187,22 @@ public virtual void OnStartMeeting() { } /// - /// 誰かが投票したときに発火する + /// 自分が投票した瞬間,票がカウントされる前に呼ばれる
+ /// falseを返すと投票行動自体をなかったことにし,再度投票できるようになる
+ /// 投票行動自体は取り消さず,票だけカウントさせない場合はを使用し,doVoteをfalseにする + ///
+ /// 投票先 + /// falseを返すと投票自体がなかったことになり,投票者自身以外には投票したことがバレません + public virtual bool CheckVoteAsVoter(PlayerControl votedFor) => true; + + /// + /// 誰かが投票した瞬間に呼ばれ,票を書き換えることができる
+ /// 投票行動自体をなかったことにしたい場合はを使用する ///
/// 投票した人のID /// 投票された人のID /// (変更後の投票先(変更しないならnull), 変更後の票数(変更しないならnull), 投票をカウントするか) - public virtual (byte? votedForId, int? numVotes, bool doVote) OnVote(byte voterId, byte sourceVotedForId) => (null, null, true); + public virtual (byte? votedForId, int? numVotes, bool doVote) ModifyVote(byte voterId, byte sourceVotedForId, bool isIntentional) => (null, null, true); /// /// 追放後に行われる処理 @@ -245,22 +255,29 @@ public virtual void AfterMeetingTasks() // Suffix:ターゲット矢印などの追加情報。 /// - /// seenによるRoleNameの書き換え + /// seenによる表示上のRoleNameの書き換え /// /// 見る側 /// RoleNameを表示するかどうか /// RoleNameの色 /// RoleNameのテキスト - public virtual void OverrideRoleNameAsSeen(PlayerControl seer, ref bool enabled, ref Color roleColor, ref string roleText) + public virtual void OverrideDisplayRoleNameAsSeen(PlayerControl seer, ref bool enabled, ref Color roleColor, ref string roleText) { } /// - /// seerによるRoleNameの書き換え + /// seerによる表示上のRoleNameの書き換え /// /// 見られる側 /// RoleNameを表示するかどうか /// RoleNameの色 /// RoleNameのテキスト - public virtual void OverrideRoleNameAsSeer(PlayerControl seen, ref bool enabled, ref Color roleColor, ref string roleText) + public virtual void OverrideDisplayRoleNameAsSeer(PlayerControl seen, ref bool enabled, ref Color roleColor, ref string roleText) + { } + /// + /// 本来の役職名の書き換え + /// + /// RoleNameの色 + /// RoleNameのテキスト + public virtual void OverrideTrueRoleName(ref Color roleColor, ref string roleText) { } /// /// seerによるProgressTextの書き換え diff --git a/Roles/Core/SimpleRoleInfo.cs b/Roles/Core/SimpleRoleInfo.cs index 518b14ab0..7ede88a98 100644 --- a/Roles/Core/SimpleRoleInfo.cs +++ b/Roles/Core/SimpleRoleInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using UnityEngine; using AmongUs.GameOptions; @@ -27,6 +28,19 @@ public class SimpleRoleInfo public AudioClip IntroSound => introSound?.Invoke(); private Func canMakeMadmate; public bool CanMakeMadmate => canMakeMadmate?.Invoke() == true; + /// + /// 人数設定の最小人数, 最大人数, 一単位数 + /// + public IntegerValueRule AssignCountRule; + /// + /// 人数設定に対し何人単位でアサインするか + /// 役職の抽選回数 = 設定人数 / AssignUnitCount + /// + public int AssignUnitCount => AssignCountRule?.Step ?? 1; + /// + /// 実際にアサインされる役職の内訳 + /// + public CustomRoles[] AssignUnitRoles; private SimpleRoleInfo( Type classType, @@ -42,7 +56,9 @@ public class SimpleRoleInfo bool requireResetCam, TabGroup tab, Func introSound, - Func canMakeMadmate + Func canMakeMadmate, + IntegerValueRule assignCountRule, + CustomRoles[] assignUnitRoles ) { ClassType = classType; @@ -57,11 +73,14 @@ Func canMakeMadmate this.introSound = introSound; this.canMakeMadmate = canMakeMadmate; ChatCommand = chatCommand; + AssignCountRule = assignCountRule; + AssignUnitRoles = assignUnitRoles; if (colorCode == "") colorCode = customRoleType switch { CustomRoleTypes.Impostor or CustomRoleTypes.Madmate => "#ff1919", + CustomRoleTypes.Crewmate => "#8cffff", _ => "#ffffff" }; RoleColorCode = colorCode; @@ -95,12 +114,18 @@ Func canMakeMadmate TabGroup tab = TabGroup.MainSettings, Func introSound = null, Func canMakeMadmate = null, - CountTypes? countType = null + CountTypes? countType = null, + IntegerValueRule assignCountRule = null, + CustomRoles[] assignUnitRoles = null ) { countType ??= customRoleType == CustomRoleTypes.Impostor ? CountTypes.Impostor : CountTypes.Crew; + assignCountRule ??= customRoleType == CustomRoleTypes.Impostor ? + new(1, 3, 1) : + new(1, 15, 1); + assignUnitRoles ??= Enumerable.Repeat(roleName, assignCountRule.Step).ToArray(); return new( @@ -117,7 +142,9 @@ Func canMakeMadmate requireResetCam, tab, introSound, - canMakeMadmate + canMakeMadmate, + assignCountRule, + assignUnitRoles ); } public static SimpleRoleInfo CreateForVanilla( @@ -176,7 +203,9 @@ Func canMakeMadmate false, TabGroup.MainSettings, null, - () => canMakeMadmate + () => canMakeMadmate, + new(1, 15, 1), + new CustomRoles[1] { roleName } ); } public delegate void OptionCreatorDelegate(); diff --git a/Roles/Crewmate/Dictator.cs b/Roles/Crewmate/Dictator.cs index 3144678b8..93dd83c99 100644 --- a/Roles/Crewmate/Dictator.cs +++ b/Roles/Crewmate/Dictator.cs @@ -24,11 +24,11 @@ public Dictator(PlayerControl player) player ) { } - public override (byte? votedForId, int? numVotes, bool doVote) OnVote(byte voterId, byte sourceVotedForId) + public override (byte? votedForId, int? numVotes, bool doVote) ModifyVote(byte voterId, byte sourceVotedForId, bool isIntentional) { - var (votedForId, numVotes, doVote) = base.OnVote(voterId, sourceVotedForId); + var (votedForId, numVotes, doVote) = base.ModifyVote(voterId, sourceVotedForId, isIntentional); var baseVote = (votedForId, numVotes, doVote); - if (voterId != Player.PlayerId || sourceVotedForId == Player.PlayerId || sourceVotedForId >= 253 || !Player.IsAlive()) + if (!isIntentional || voterId != Player.PlayerId || sourceVotedForId == Player.PlayerId || sourceVotedForId >= 253 || !Player.IsAlive()) { return baseVote; } diff --git a/Roles/Crewmate/Lighter.cs b/Roles/Crewmate/Lighter.cs index 7bf2f8d5b..d2b5d91d5 100644 --- a/Roles/Crewmate/Lighter.cs +++ b/Roles/Crewmate/Lighter.cs @@ -22,47 +22,91 @@ public Lighter(PlayerControl player) player ) { - TaskCompletedVision = OptionTaskCompletedVision.GetFloat(); + MaxVision = OptionMaxVision.GetFloat(); TaskCompletedDisableLightOut = OptionTaskCompletedDisableLightOut.GetBool(); + TaskTrigger = OptionLighterTaskTrigger.GetInt(); + CurrentVision = Main.DefaultCrewmateVision; + LighterTriggerType = (TriggerType)OptionLighterTriggerType.GetValue(); } - - private static OptionItem OptionTaskCompletedVision; + /// 最大視野 + private static OptionItem OptionMaxVision; + /// タスク完了時に停電の影響を受けなくする private static OptionItem OptionTaskCompletedDisableLightOut; + /// 効果発揮のタイプを変更する [タスク進捗率,一定数のタスク達成] + private static OptionItem OptionLighterTriggerType; + /// 能力発動タスク数 TriggerType[一定数のタスク達成]選択時のみ有効 + private static OptionItem OptionLighterTaskTrigger; enum OptionName { - LighterTaskCompletedVision, - LighterTaskCompletedDisableLightOut + LighterMaxVision, + LighterTaskCompletedDisableLightOut, + LighterTriggerType, + LighterTaskTrigger + } + /// 効果を発揮するタイプ + public enum TriggerType + { + TaskProgressRate,//タスク進捗率 + TaskCount//一定数のタスク達成 } - private static float TaskCompletedVision; + public static TriggerType LighterTriggerType; + + private static float MaxVision; private static bool TaskCompletedDisableLightOut; + private static int TaskTrigger; + private float CurrentVision; private static void SetupOptionItem() { - OptionTaskCompletedVision = FloatOptionItem.Create(RoleInfo, 10, OptionName.LighterTaskCompletedVision, new(0f, 5f, 0.25f), 2f, false) + OptionMaxVision = FloatOptionItem.Create(RoleInfo, 10, OptionName.LighterMaxVision, new(0.0f, 3.0f, 0.1f), 1.0f, false) .SetValueFormat(OptionFormat.Multiplier); OptionTaskCompletedDisableLightOut = BooleanOptionItem.Create(RoleInfo, 11, OptionName.LighterTaskCompletedDisableLightOut, true, false); + OptionLighterTriggerType = StringOptionItem.Create(RoleInfo, 12, OptionName.LighterTriggerType, EnumHelper.GetAllNames(), 0, false); + OptionLighterTaskTrigger = IntegerOptionItem.Create(RoleInfo, 13, OptionName.LighterTaskTrigger, new(1, 99, 1), 5, false) + .SetParent(OptionLighterTriggerType); } public override void ApplyGameOptions(IGameOptions opt) { - if (!IsTaskFinished) return; - + if (!Player.IsAlive() || MyTaskState.CompletedTasksCount == 0) return;//死んでる or タスク数0 + //タスクトリガーの場合 トリガータスク数を下回っている or タスク完了していない + if (LighterTriggerType == TriggerType.TaskCount && !MyTaskState.HasCompletedEnoughCountOfTasks(TaskTrigger)) return; + Logger.Info("ApplyGameOptions Trigger", "Lighter"); var crewLightMod = FloatOptionNames.CrewLightMod; - - opt.SetFloat(crewLightMod, TaskCompletedVision); - if (TaskCompletedDisableLightOut && Utils.IsActive(SystemTypes.Electrical)) + opt.SetFloat(crewLightMod, CurrentVision); + if (TaskCompletedDisableLightOut && Utils.IsActive(SystemTypes.Electrical) && MyTaskState.IsTaskFinished) { - opt.SetFloat(crewLightMod, TaskCompletedVision * 5); + opt.SetFloat(crewLightMod, CurrentVision * 5); } } public override bool OnCompleteTask() { - if (IsTaskFinished) + if (!Player.IsAlive() || MyTaskState.CompletedTasksCount == 0) return true;//死んでる or タスク数0 + if (LighterTriggerType == TriggerType.TaskCount && MyTaskState.CompletedTasksCount != TaskTrigger) return true; + Logger.Info("Ability activation condition", "Lighter"); + if (LighterTriggerType == TriggerType.TaskCount && MyTaskState.CompletedTasksCount == TaskTrigger) { - Player.MarkDirtySettings(); + CurrentVision = MaxVision; } - + if (LighterTriggerType == TriggerType.TaskProgressRate) + { + //進捗率(%) = 完了タスク数 / 全タスク数 例:1/4 = 0.25=> 0.25*100 =>25% + int progressRate = MyTaskState.CompletedTasksCount * 100 / MyTaskState.AllTasksCount; + //視野差 = 最大視野 - デフォルト視野 例:(1.25 - 0.25)/100=> 1.00 + float viewBetween = (MaxVision * 100 - Main.DefaultCrewmateVision * 100) / 100; + //例:1.00 * 25 / 100 => 0.25(上昇値) + CurrentVision += viewBetween * progressRate / 100; + Logger.Info("viewBetween :" + viewBetween.ToString() + "*" + " progressRate:" + progressRate.ToString() + "%", "Lighter"); + Logger.Info("タスク進捗率で視野変更 タスク:" + MyTaskState.CompletedTasksCount + "/" + MyTaskState.AllTasksCount + " セットする視野:" + CurrentVision.ToString(), "Lighter"); + } + Player.MarkDirtySettings(); return true; } + + ///Lighter以外から視野を変更する場合は以下メソッドを使用すること + public void AddCurrentVision(float addVision) + { + CurrentVision += addVision; + } } \ No newline at end of file diff --git a/Roles/Crewmate/Mayor.cs b/Roles/Crewmate/Mayor.cs index 5c12e0a11..f4cfdf299 100644 --- a/Roles/Crewmate/Mayor.cs +++ b/Roles/Crewmate/Mayor.cs @@ -55,7 +55,6 @@ private static void SetupOptionItem() } public override void ApplyGameOptions(IGameOptions opt) { - Logger.Warn($"{LeftButtonCount} <= 0", "Mayor.ApplyGameOptions"); AURoleOptions.EngineerCooldown = LeftButtonCount <= 0 ? 255f @@ -78,10 +77,10 @@ public override bool OnEnterVent(PlayerPhysics physics, int ventId) return false; } - public override (byte? votedForId, int? numVotes, bool doVote) OnVote(byte voterId, byte sourceVotedForId) + public override (byte? votedForId, int? numVotes, bool doVote) ModifyVote(byte voterId, byte sourceVotedForId, bool isIntentional) { // 既定値 - var (votedForId, numVotes, doVote) = base.OnVote(voterId, sourceVotedForId); + var (votedForId, numVotes, doVote) = base.ModifyVote(voterId, sourceVotedForId, isIntentional); if (voterId == Player.PlayerId) { numVotes = AdditionalVote + 1; diff --git a/Roles/Crewmate/Sheriff.cs b/Roles/Crewmate/Sheriff.cs index 8d6308bfd..f4919e7b2 100644 --- a/Roles/Crewmate/Sheriff.cs +++ b/Roles/Crewmate/Sheriff.cs @@ -6,10 +6,11 @@ using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; +using TownOfHost.Roles.Neutral; using static TownOfHost.Translator; namespace TownOfHost.Roles.Crewmate; -public sealed class Sheriff : RoleBase, IKiller +public sealed class Sheriff : RoleBase, IKiller, ISchrodingerCatOwner { public static readonly SimpleRoleInfo RoleInfo = SimpleRoleInfo.Create( @@ -50,12 +51,16 @@ enum OptionName SheriffCanKill, } public static Dictionary KillTargetOptions = new(); + public static Dictionary SchrodingerCatKillTargetOptions = new(); public int ShotLimit = 0; public float CurrentKillCooldown = 30; public static readonly string[] KillOption = { - "SheriffCanKillAll", "SheriffCanKillSeparately" - }; + "SheriffCanKillAll", "SheriffCanKillSeparately" + }; + + public SchrodingerCat.TeamType SchrodingerCatChangeTo => SchrodingerCat.TeamType.Crew; + private static void SetupOptionItem() { KillCooldown = FloatOptionItem.Create(RoleInfo, 10, GeneralOption.KillCooldown, new(0f, 990f, 1f), 30f, false) @@ -78,21 +83,37 @@ or CustomRoles.HASFox SetUpKillTargetOption(neutral, idOffset, true, CanKillNeutrals); idOffset++; } + foreach (var catType in EnumHelper.GetAllValues()) + { + if ((byte)catType < 50) + { + continue; + } + SetUpSchrodingerCatKillTargetOption(catType, idOffset, true, CanKillNeutrals); + idOffset++; + } } public static void SetUpKillTargetOption(CustomRoles role, int idOffset, bool defaultValue = true, OptionItem parent = null) { var id = RoleInfo.ConfigId + idOffset; if (parent == null) parent = RoleInfo.RoleOption; - var roleName = Utils.GetRoleName(role) + role switch - { - CustomRoles.EgoSchrodingerCat => $" {GetString("In%team%", new Dictionary() { { "%team%", Utils.GetRoleName(CustomRoles.Egoist) } })}", - CustomRoles.JSchrodingerCat => $" {GetString("In%team%", new Dictionary() { { "%team%", Utils.GetRoleName(CustomRoles.Jackal) } })}", - _ => "", - }; + var roleName = Utils.GetRoleName(role); Dictionary replacementDic = new() { { "%role%", Utils.ColorString(Utils.GetRoleColor(role), roleName) } }; KillTargetOptions[role] = BooleanOptionItem.Create(id, OptionName.SheriffCanKill + "%role%", defaultValue, RoleInfo.Tab, false).SetParent(parent); KillTargetOptions[role].ReplacementDictionary = replacementDic; } + public static void SetUpSchrodingerCatKillTargetOption(SchrodingerCat.TeamType catType, int idOffset, bool defaultValue = true, OptionItem parent = null) + { + var id = RoleInfo.ConfigId + idOffset; + parent ??= RoleInfo.RoleOption; + // (%team%陣営) + var inTeam = GetString("In%team%", new Dictionary() { ["%team%"] = GetRoleString(catType.ToString()) }); + // シュレディンガーの猫(%team%陣営) + var catInTeam = Utils.ColorString(SchrodingerCat.GetCatColor(catType), Utils.GetRoleName(CustomRoles.SchrodingerCat) + inTeam); + Dictionary replacementDic = new() { ["%role%"] = catInTeam }; + SchrodingerCatKillTargetOptions[catType] = BooleanOptionItem.Create(id, OptionName.SheriffCanKill + "%role%", defaultValue, RoleInfo.Tab, false).SetParent(parent); + SchrodingerCatKillTargetOptions[catType].ReplacementDictionary = replacementDic; + } public override void Add() { var playerId = Player.PlayerId; @@ -157,6 +178,22 @@ public void OnCheckMurderAsKiller(MurderInfo info) public static bool CanBeKilledBy(PlayerControl player) { var cRole = player.GetCustomRole(); + + if (player.GetRoleClass() is SchrodingerCat schrodingerCat) + { + if (schrodingerCat.Team == SchrodingerCat.TeamType.None) + { + Logger.Warn($"シェリフ({player.GetRealName()})にキルされたシュレディンガーの猫のロールが変化していません", nameof(Sheriff)); + return false; + } + return schrodingerCat.Team switch + { + SchrodingerCat.TeamType.Mad => KillTargetOptions.TryGetValue(CustomRoles.Madmate, out var option) && option.GetBool(), + SchrodingerCat.TeamType.Crew => false, + _ => CanKillNeutrals.GetValue() == 0 || (SchrodingerCatKillTargetOptions.TryGetValue(schrodingerCat.Team, out var option) && option.GetBool()), + }; + } + return cRole.GetCustomRoleTypes() switch { CustomRoleTypes.Impostor => true, diff --git a/Roles/Crewmate/SpeedBooster.cs b/Roles/Crewmate/SpeedBooster.cs index f233a993e..4a25de3d9 100644 --- a/Roles/Crewmate/SpeedBooster.cs +++ b/Roles/Crewmate/SpeedBooster.cs @@ -56,7 +56,7 @@ public override bool OnCompleteTask() var playerId = Player.PlayerId; if (Player.IsAlive() && BoostTarget == byte.MaxValue - && (IsTaskFinished || MyTaskState.CompletedTasksCount >= TaskTrigger)) + && MyTaskState.HasCompletedEnoughCountOfTasks(TaskTrigger)) { //スピブが生きていて、SpeedBoostTargetに登録済みでなく、全タスク完了orトリガー数までタスクを完了している場合 var rand = IRandom.Instance; List targetPlayers = new(); diff --git a/Roles/Impostor/BountyHunter.cs b/Roles/Impostor/BountyHunter.cs index b63b0816b..246d88123 100644 --- a/Roles/Impostor/BountyHunter.cs +++ b/Roles/Impostor/BountyHunter.cs @@ -6,6 +6,7 @@ using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; +using TownOfHost.Roles.Neutral; using static TownOfHost.Translator; namespace TownOfHost.Roles.Impostor; @@ -212,4 +213,11 @@ public override string GetSuffix(PlayerControl seer, PlayerControl seen = null, //矢印オプションがありミーティング以外で矢印表示 return TargetArrow.GetArrows(Player, target.PlayerId); } + public void OnSchrodingerCatKill(SchrodingerCat schrodingerCat) + { + if (GetTarget() == schrodingerCat.Player) + { + ResetTarget(); // ターゲットの選びなおし + } + } } \ No newline at end of file diff --git a/Roles/Impostor/EvilHacker.cs b/Roles/Impostor/EvilHacker.cs new file mode 100644 index 000000000..70835a033 --- /dev/null +++ b/Roles/Impostor/EvilHacker.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AmongUs.GameOptions; +using Hazel; +using TownOfHost.Modules; +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; +using UnityEngine; + +namespace TownOfHost.Roles.Impostor; + +public sealed class EvilHacker : RoleBase, IImpostor, IKillFlashSeeable +{ + public static readonly SimpleRoleInfo RoleInfo = + SimpleRoleInfo.Create( + typeof(EvilHacker), + player => new EvilHacker(player), + CustomRoles.EvilHacker, + () => RoleTypes.Impostor, + CustomRoleTypes.Impostor, + 3100, + SetupOptionItems, + "eh" + ); + public EvilHacker(PlayerControl player) + : base( + RoleInfo, + player + ) + { + canSeeDeadMark = OptionCanSeeDeadMark.GetBool(); + canSeeImpostorMark = OptionCanSeeImpostorMark.GetBool(); + canSeeKillFlash = OptionCanSeeKillFlash.GetBool(); + canSeeMurderRoom = OptionCanSeeMurderRoom.GetBool(); + + CustomRoleManager.OnMurderPlayerOthers.Add(HandleMurderRoomNotify); + instances.Add(this); + } + public override void OnDestroy() + { + instances.Remove(this); + } + + private static OptionItem OptionCanSeeDeadMark; + private static OptionItem OptionCanSeeImpostorMark; + private static OptionItem OptionCanSeeKillFlash; + private static OptionItem OptionCanSeeMurderRoom; + private enum OptionName + { + EvilHackerCanSeeDeadMark, + EvilHackerCanSeeImpostorMark, + EvilHackerCanSeeKillFlash, + EvilHackerCanSeeMurderRoom, + } + private static bool canSeeDeadMark; + private static bool canSeeImpostorMark; + private static bool canSeeKillFlash; + private static bool canSeeMurderRoom; + + private static HashSet instances = new(1); + + private HashSet activeNotifies = new(2); + + private static void SetupOptionItems() + { + OptionCanSeeDeadMark = BooleanOptionItem.Create(RoleInfo, 10, OptionName.EvilHackerCanSeeDeadMark, true, false); + OptionCanSeeImpostorMark = BooleanOptionItem.Create(RoleInfo, 11, OptionName.EvilHackerCanSeeImpostorMark, true, false); + OptionCanSeeKillFlash = BooleanOptionItem.Create(RoleInfo, 12, OptionName.EvilHackerCanSeeKillFlash, true, false); + OptionCanSeeMurderRoom = BooleanOptionItem.Create(RoleInfo, 13, OptionName.EvilHackerCanSeeMurderRoom, true, false, OptionCanSeeKillFlash); + } + /// 相方がキルした部屋を通知する設定がオンなら各プレイヤーに通知を行う + private static void HandleMurderRoomNotify(MurderInfo info) + { + if (canSeeMurderRoom) + { + foreach (var evilHacker in instances) + { + evilHacker.OnMurderPlayer(info); + } + } + } + + public override void OnReportDeadBody(PlayerControl reporter, GameData.PlayerInfo target) + { + if (!Player.IsAlive()) + { + return; + } + var admins = AdminProvider.CalculateAdmin(); + var builder = new StringBuilder(512); + + // 送信するメッセージを生成 + foreach (var admin in admins) + { + var entry = admin.Value; + if (entry.TotalPlayers <= 0) + { + continue; + } + // インポスターがいるなら星マークを付ける + if (canSeeImpostorMark && entry.NumImpostors > 0) + { + builder.Append(ImpostorMark); + } + // 部屋名と合計プレイヤー数を表記 + builder.Append(DestroyableSingleton.Instance.GetString(entry.Room)); + builder.Append(": "); + builder.Append(entry.TotalPlayers); + // 死体があったら死体の数を書く + if (canSeeDeadMark && entry.NumDeadBodies > 0) + { + builder.Append('(').Append(Translator.GetString("Deadbody")); + builder.Append('×').Append(entry.NumDeadBodies).Append(')'); + } + builder.Append('\n'); + } + + // 送信 + var message = builder.ToString(); + var title = Utils.ColorString(Color.green, Translator.GetString("LastAdminInfo")); + + _ = new LateTask(() => + { + if (GameStates.IsInGame) + { + Utils.SendMessage(message, Player.PlayerId, title, false); + } + }, 4f, "EvilHacker Admin Message"); + return; + } + private void OnMurderPlayer(MurderInfo info) + { + // 生きてる間に相方のキルでキルフラが鳴った場合に通知を出す + if (!Player.IsAlive() || !CheckKillFlash(info) || info.AttemptKiller == Player) + { + return; + } + RpcCreateMurderNotify(info.AttemptTarget.GetPlainShipRoom()?.RoomId ?? SystemTypes.Hallway); + } + private void RpcCreateMurderNotify(SystemTypes room) + { + CreateMurderNotify(room); + if (AmongUsClient.Instance.AmHost) + { + using var sender = CreateSender(CustomRPC.EvilHackerCreateMurderNotify); + sender.Writer.Write((byte)room); + } + } + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType == CustomRPC.EvilHackerCreateMurderNotify) + { + CreateMurderNotify((SystemTypes)reader.ReadByte()); + } + } + /// + /// 名前の下にキル発生通知を出す + /// + /// キルが起きた部屋 + private void CreateMurderNotify(SystemTypes room) + { + activeNotifies.Add(new() + { + CreatedAt = DateTime.Now, + Room = room, + }); + if (AmongUsClient.Instance.AmHost) + { + Utils.NotifyRoles(SpecifySeer: Player); + } + } + public override void OnFixedUpdate(PlayerControl player) + { + // 古い通知の削除処理 Mod入りは自分でやる + if (!AmongUsClient.Instance.AmHost && Player != PlayerControl.LocalPlayer) + { + return; + } + if (activeNotifies.Count <= 0) + { + return; + } + // NotifyRolesを実行するかどうかのフラグ + var doNotifyRoles = false; + // 古い通知があれば削除 + foreach (var notify in activeNotifies) + { + if (DateTime.Now - notify.CreatedAt > NotifyDuration) + { + activeNotifies.Remove(notify); + doNotifyRoles = true; + } + } + if (doNotifyRoles && AmongUsClient.Instance.AmHost) + { + Utils.NotifyRoles(SpecifySeer: Player); + } + } + public override string GetSuffix(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false) + { + seen ??= seer; + if (isForMeeting || !canSeeMurderRoom || seer != Player || seen != Player || activeNotifies.Count <= 0) + { + return base.GetSuffix(seer, seen, isForMeeting); + } + var roomNames = activeNotifies.Select(notify => DestroyableSingleton.Instance.GetString(notify.Room)); + return Utils.ColorString(Color.green, $"{Translator.GetString("MurderNotify")}: {string.Join(", ", roomNames)}"); + } + public bool CheckKillFlash(MurderInfo info) => + canSeeKillFlash && !info.IsSuicide && !info.IsAccident && info.AttemptKiller.Is(CustomRoleTypes.Impostor); + + private static readonly string ImpostorMark = "★".Color(Palette.ImpostorRed); + /// 相方がキルしたときに名前の下に通知を表示する長さ + private static readonly TimeSpan NotifyDuration = TimeSpan.FromSeconds(10); + + private readonly struct MurderNotify + { + /// 通知が作成された時間 + public DateTime CreatedAt { get; init; } + /// キルが起きた部屋 + public SystemTypes Room { get; init; } + } +} diff --git a/Roles/Impostor/Insider.cs b/Roles/Impostor/Insider.cs new file mode 100644 index 000000000..d5835875c --- /dev/null +++ b/Roles/Impostor/Insider.cs @@ -0,0 +1,133 @@ +using System.Text; +using UnityEngine; +using AmongUs.GameOptions; + +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; + +namespace TownOfHost.Roles.Impostor +{ + public sealed class Insider : RoleBase, IImpostor + { + public static readonly SimpleRoleInfo RoleInfo = + SimpleRoleInfo.Create( + typeof(Insider), + player => new Insider(player), + CustomRoles.Insider, + () => RoleTypes.Impostor, + CustomRoleTypes.Impostor, + 2800, + SetupOptionItem, + "ins" + ); + public Insider(PlayerControl player) + : base( + RoleInfo, + player + ) + { + canSeeImpostorAbilities = optionCanSeeImpostorAbilities.GetBool(); + canSeeAllGhostsRoles = optionCanSeeAllGhostsRoles.GetBool(); + canSeeMadmates = optionCanSeeMadmates.GetBool(); + killCountToSeeMadmates = optionKillCountToSeeMadmates.GetInt(); + } + private static OptionItem optionCanSeeAllGhostsRoles; + private static OptionItem optionCanSeeImpostorAbilities; + private static OptionItem optionCanSeeMadmates; + private static OptionItem optionKillCountToSeeMadmates; + private enum OptionName + { + InsiderCanSeeAllGhostsRoles, + InsiderCanSeeImpostorAbilities, + InsiderCanSeeMadmates, + InsiderKillCountToSeeMadmates, + } + private static bool canSeeAllGhostsRoles; + private static bool canSeeImpostorAbilities; + private static bool canSeeMadmates; + private static int killCountToSeeMadmates; + + private static void SetupOptionItem() + { + optionCanSeeAllGhostsRoles = BooleanOptionItem.Create(RoleInfo, 10, OptionName.InsiderCanSeeAllGhostsRoles, false, false); + optionCanSeeImpostorAbilities = BooleanOptionItem.Create(RoleInfo, 11, OptionName.InsiderCanSeeImpostorAbilities, true, false); + optionCanSeeMadmates = BooleanOptionItem.Create(RoleInfo, 12, OptionName.InsiderCanSeeMadmates, false, false); + optionKillCountToSeeMadmates = IntegerOptionItem.Create(RoleInfo, 13, OptionName.InsiderKillCountToSeeMadmates, new(0, 15, 1), 2, false) + .SetParent(optionCanSeeMadmates) + .SetValueFormat(OptionFormat.Times); + } + + /// + ///役職を見る前提条件 + /// + private bool IsAbilityAvailable(PlayerControl target) + { + if (Player == null || target == null) return false; + if (Player == target) return false; + if (target.Is(CustomRoles.GM)) return false; + if (!Player.IsAlive() && Options.GhostCanSeeOtherRoles.GetBool()) return false; + return true; + } + /// + ///Impostor, Madmateの内通能力 + /// + private bool KnowAllyRole(PlayerControl target) + => IsAbilityAvailable(target) + && target.GetCustomRole().GetCustomRoleTypes() switch + { + CustomRoleTypes.Impostor => canSeeImpostorAbilities, + CustomRoleTypes.Madmate => canSeeMadmates && MyState.GetKillCount(true) >= killCountToSeeMadmates, + _ => false, + }; + /// + ///幽霊の役職が見えるケース + /// + private bool KnowDeadRole(PlayerControl target) + => IsAbilityAvailable(target) + && !target.IsAlive() + && (canSeeAllGhostsRoles //全員見える + || target.GetRealKiller() == Player); //自分でキルした相手 + /// + ///内通or幽霊 + /// + private bool KnowTargetRole(PlayerControl target) + => KnowDeadRole(target) || KnowAllyRole(target); + + public override void OverrideDisplayRoleNameAsSeer(PlayerControl seen, ref bool enabled, ref Color roleColor, ref string roleText) + { + enabled |= KnowTargetRole(seen); + } + public override void OverrideProgressTextAsSeer(PlayerControl seen, ref bool enabled, ref string text) + { + enabled |= KnowAllyRole(seen); + } + public override string GetProgressText(bool isComms = false) + { + if (!canSeeMadmates) return ""; + + int killCount = MyState.GetKillCount(true); + string mark = killCount >= killCountToSeeMadmates ? "★" : $"({killCount}/{killCountToSeeMadmates})"; + return Utils.ColorString(Palette.ImpostorRed.ShadeColor(0.5f), mark); + } + public override string GetMark(PlayerControl seer, PlayerControl seen, bool isForMeeting = false) + { + //seenが省略の場合seer + seen ??= seer; + var mark = new StringBuilder(50); + + // 死亡したLoversのマーク追加 + if (seen.Is(CustomRoles.Lovers) && !seer.Is(CustomRoles.Lovers) && KnowDeadRole(seen)) + mark.Append(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Lovers), "♡")); + + if (canSeeImpostorAbilities) + { + foreach (var impostor in Main.AllPlayerControls) + { + if (seer == impostor || impostor.Is(CustomRoles.Insider) || !impostor.Is(CustomRoleTypes.Impostor)) continue; + mark.Append(impostor.GetRoleClass()?.GetMark(impostor, seen, isForMeeting)); + } + } + return mark.ToString(); + } + } +} \ No newline at end of file diff --git a/Roles/Impostor/Mare.cs b/Roles/Impostor/Mare.cs index f90448946..eb1d576d5 100644 --- a/Roles/Impostor/Mare.cs +++ b/Roles/Impostor/Mare.cs @@ -1,4 +1,5 @@ using AmongUs.GameOptions; +using Hazel; using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; @@ -66,6 +67,28 @@ public override void ApplyGameOptions(IGameOptions opt) Main.AllPlayerSpeed[Player.PlayerId] -= SpeedInLightsOut;//Mareの速度を減算 } } + private void ActivateKill(bool activate) + { + IsActivateKill = activate; + if (AmongUsClient.Instance.AmHost) + { + SendRPC(); + Player.MarkDirtySettings(); + Utils.NotifyRoles(); + } + } + public void SendRPC() + { + using var sender = CreateSender(CustomRPC.MareSync); + sender.Writer.Write(IsActivateKill); + } + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType != CustomRPC.MareSync) return; + + IsActivateKill = reader.ReadBoolean(); + } + public override void OnFixedUpdate(PlayerControl player) { if (GameStates.IsInTask && IsActivateKill) @@ -73,9 +96,7 @@ public override void OnFixedUpdate(PlayerControl player) if (!Utils.IsActive(SystemTypes.Electrical)) { //停電解除されたらキルモード解除 - IsActivateKill = false; - Player.MarkDirtySettings(); - Utils.NotifyRoles(); + ActivateKill(false); } } } @@ -90,9 +111,7 @@ public override bool OnSabotage(PlayerControl player, SystemTypes systemType, by //まだ停電が直っていなければキル可能モードに if (Utils.IsActive(SystemTypes.Electrical)) { - IsActivateKill = true; - Player.MarkDirtySettings(); - Utils.NotifyRoles(); + ActivateKill(true); } }, 4.0f, "Mare Activate Kill"); } diff --git a/Roles/Impostor/NekoKabocha.cs b/Roles/Impostor/NekoKabocha.cs new file mode 100644 index 000000000..a461b56e7 --- /dev/null +++ b/Roles/Impostor/NekoKabocha.cs @@ -0,0 +1,82 @@ +using AmongUs.GameOptions; +using TownOfHost.Modules; +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; + +namespace TownOfHost.Roles.Impostor; + +public sealed class NekoKabocha : RoleBase, IImpostor, INekomata +{ + public static readonly SimpleRoleInfo RoleInfo = + SimpleRoleInfo.Create( + typeof(NekoKabocha), + player => new NekoKabocha(player), + CustomRoles.NekoKabocha, + () => RoleTypes.Impostor, + CustomRoleTypes.Impostor, + 3300, + SetupOptionItems, + "nk", + introSound: () => PlayerControl.LocalPlayer.KillSfx + ); + public NekoKabocha(PlayerControl player) + : base( + RoleInfo, + player + ) + { + impostorsGetRevenged = optionImpostorsGetRevenged.GetBool(); + madmatesGetRevenged = optionMadmatesGetRevenged.GetBool(); + revengeOnExile = optionRevengeOnExile.GetBool(); + } + + #region カスタムオプション + /// インポスターに仕返し/道連れするかどうか + private static BooleanOptionItem optionImpostorsGetRevenged; + /// マッドに仕返し/道連れするかどうか + private static BooleanOptionItem optionMadmatesGetRevenged; + private static BooleanOptionItem optionRevengeOnExile; + private static void SetupOptionItems() + { + optionImpostorsGetRevenged = BooleanOptionItem.Create(RoleInfo, 10, OptionName.NekoKabochaImpostorsGetRevenged, false, false); + optionMadmatesGetRevenged = BooleanOptionItem.Create(RoleInfo, 20, OptionName.NekoKabochaMadmatesGetRevenged, false, false); + optionRevengeOnExile = BooleanOptionItem.Create(RoleInfo, 30, OptionName.NekoKabochaRevengeOnExile, false, false); + } + private enum OptionName { NekoKabochaImpostorsGetRevenged, NekoKabochaMadmatesGetRevenged, NekoKabochaRevengeOnExile, } + #endregion + + private static bool impostorsGetRevenged; + private static bool madmatesGetRevenged; + private static bool revengeOnExile; + private static readonly LogHandler logger = Logger.Handler(nameof(NekoKabocha)); + + public override void OnMurderPlayerAsTarget(MurderInfo info) + { + // 普通のキルじゃない.もしくはキルを行わない時はreturn + if (info.IsAccident || info.IsSuicide || !info.CanKill || !info.DoKill) + { + return; + } + // 殺してきた人を殺し返す + logger.Info("ネコカボチャの仕返し"); + var killer = info.AttemptKiller; + if (!IsCandidate(killer)) + { + logger.Info("キラーは仕返し対象ではないので仕返しされません"); + return; + } + killer.SetRealKiller(Player); + PlayerState.GetByPlayerId(killer.PlayerId).DeathReason = CustomDeathReason.Revenge; + Player.RpcMurderPlayer(killer); + } + public bool DoRevenge(CustomDeathReason deathReason) => revengeOnExile && deathReason == CustomDeathReason.Vote; + public bool IsCandidate(PlayerControl player) + { + return player.GetCustomRole().GetCustomRoleTypes() switch + { + CustomRoleTypes.Impostor => impostorsGetRevenged, + CustomRoleTypes.Madmate => madmatesGetRevenged, + _ => true, + }; + } +} diff --git a/Roles/Impostor/Penguin.cs b/Roles/Impostor/Penguin.cs new file mode 100644 index 000000000..3c0c8fe3e --- /dev/null +++ b/Roles/Impostor/Penguin.cs @@ -0,0 +1,259 @@ +using UnityEngine; +using AmongUs.GameOptions; +using Hazel; + +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; +using static TownOfHost.Translator; + +namespace TownOfHost.Roles.Impostor; + +class Penguin : RoleBase, IImpostor +{ + public static readonly SimpleRoleInfo RoleInfo = + SimpleRoleInfo.Create( + typeof(Penguin), + player => new Penguin(player), + CustomRoles.Penguin, + () => RoleTypes.Shapeshifter, + CustomRoleTypes.Impostor, + 3400, + SetupOptionItem, + "pe" + ); + public Penguin(PlayerControl player) + : base(RoleInfo, player) + { + AbductTimerLimit = OptionAbductTimerLimit.GetFloat(); + MeetingKill = OptionMeetingKill.GetBool(); + } + public override void OnDestroy() + { + AbductVictim = null; + } + + static OptionItem OptionAbductTimerLimit; + static OptionItem OptionMeetingKill; + + enum OptionName + { + PenguinAbductTimerLimit, + PenguinMeetingKill, + } + + private PlayerControl AbductVictim; + private float AbductTimer; + private float AbductTimerLimit; + private bool stopCount; + private bool MeetingKill; + + //拉致中にキルしそうになった相手の能力を使わせないための処置 + public bool IsKiller => AbductVictim == null; + public static void SetupOptionItem() + { + OptionAbductTimerLimit = FloatOptionItem.Create(RoleInfo, 11, OptionName.PenguinAbductTimerLimit, new(5f, 20f, 1f), 10f, false) + .SetValueFormat(OptionFormat.Seconds); + OptionMeetingKill = BooleanOptionItem.Create(RoleInfo, 12, OptionName.PenguinMeetingKill, false, false); + } + public override void Add() + { + AbductTimer = 255f; + stopCount = false; + } + public override void ApplyGameOptions(IGameOptions opt) => AURoleOptions.ShapeshifterCooldown = AbductVictim != null ? AbductTimer : 255f; + private void SendRPC() + { + using var sender = CreateSender(CustomRPC.PenguinSync); + + sender.Writer.Write(AbductVictim?.PlayerId ?? 255); + } + + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType != CustomRPC.PenguinSync) return; + + var victim = reader.ReadByte(); + if (victim == 255) + { + AbductVictim = null; + AbductTimer = 255f; + } + else + { + AbductVictim = Utils.GetPlayerById(victim); + AbductTimer = AbductTimerLimit; + } + } + void AddVictim(PlayerControl target) + { + PlayerState.GetByPlayerId(target.PlayerId).CanUseMovingPlatform = MyState.CanUseMovingPlatform = false; + AbductVictim = target; + AbductTimer = AbductTimerLimit; + Player.SyncSettings(); + Player.RpcResetAbilityCooldown(); + SendRPC(); + } + void RemoveVictim() + { + if (AbductVictim != null) + { + PlayerState.GetByPlayerId(AbductVictim.PlayerId).CanUseMovingPlatform = true; + AbductVictim = null; + } + MyState.CanUseMovingPlatform = true; + AbductTimer = 255f; + Player.SyncSettings(); + Player.RpcResetAbilityCooldown(); + SendRPC(); + } + public void OnCheckMurderAsKiller(MurderInfo info) + { + var target = info.AttemptTarget; + if (AbductVictim != null) + { + if (target != AbductVictim) + { + //拉致中は拉致相手しか切れない + Player.RpcMurderPlayer(AbductVictim); + Player.ResetKillCooldown(); + info.DoKill = false; + } + RemoveVictim(); + } + else + { + info.DoKill = false; + AddVictim(target); + } + } + public bool OverrideKillButtonText(out string text) + { + if (AbductVictim != null) + { + text = GetString("KillButtonText"); + } + else + { + text = GetString("PenguinKillButtonText"); + } + return true; + } + public override string GetAbilityButtonText() + { + return GetString("PenguinTimerText"); + } + public override bool CanUseAbilityButton() + { + return AbductVictim != null; + } + public override void OnReportDeadBody(PlayerControl reporter, GameData.PlayerInfo target) + { + stopCount = true; + // 時間切れ状態で会議を迎えたらはしご中でも構わずキルする + if (AbductVictim != null && AbductTimer <= 0f) + { + Player.RpcMurderPlayer(AbductVictim); + } + if (MeetingKill) + { + if (!AmongUsClient.Instance.AmHost) return; + if (AbductVictim == null) return; + Player.RpcMurderPlayer(AbductVictim); + RemoveVictim(); + } + } + public override void AfterMeetingTasks() + { + if (Main.NormalOptions.MapId == 4) return; + + //マップがエアシップ以外 + RestartAbduct(); + } + public void OnSpawnAirship() + { + RestartAbduct(); + } + public void RestartAbduct() + { + if (AbductVictim != null) + { + Player.SyncSettings(); + Player.RpcResetAbilityCooldown(); + stopCount = false; + } + } + public override void OnFixedUpdate(PlayerControl player) + { + if (!AmongUsClient.Instance.AmHost) return; + if (!GameStates.IsInTask) return; + + if (!stopCount) + AbductTimer -= Time.fixedDeltaTime; + + if (AbductVictim != null) + { + if (!Player.IsAlive() || !AbductVictim.IsAlive()) + { + RemoveVictim(); + return; + } + if (AbductTimer <= 0f && !Player.MyPhysics.Animations.IsPlayingAnyLadderAnimation()) + { + // 先にIsDeadをtrueにする(はしごチェイス封じ) + AbductVictim.Data.IsDead = true; + GameData.Instance.SetDirty(); + // ペンギン自身がはしご上にいる場合,はしごを降りてからキルする + if (!AbductVictim.MyPhysics.Animations.IsPlayingAnyLadderAnimation()) + { + var abductVictim = AbductVictim; + _ = new LateTask(() => + { + var sId = abductVictim.NetTransform.lastSequenceId + 5; + abductVictim.NetTransform.SnapTo(Player.transform.position, (ushort)sId); + Player.MurderPlayer(abductVictim); + + var sender = CustomRpcSender.Create("PenguinMurder"); + { + sender.AutoStartRpc(abductVictim.NetTransform.NetId, (byte)RpcCalls.SnapTo); + { + NetHelpers.WriteVector2(Player.transform.position, sender.stream); + sender.Write(abductVictim.NetTransform.lastSequenceId); + } + sender.EndRpc(); + sender.AutoStartRpc(Player.NetId, (byte)RpcCalls.MurderPlayer); + { + sender.WriteNetObject(abductVictim); + } + sender.EndRpc(); + } + sender.SendMessage(); + }, 0.3f, "PenguinMurder"); + RemoveVictim(); + } + } + // はしごの上にいるプレイヤーにはSnapToRPCが効かずホストだけ挙動が変わるため,一律でテレポートを行わない + else if (!AbductVictim.MyPhysics.Animations.IsPlayingAnyLadderAnimation()) + { + var position = Player.transform.position; + if (Player.PlayerId != 0) + { + RandomSpawn.TP(AbductVictim.NetTransform, position); + } + else + { + _ = new LateTask(() => + { + if (AbductVictim != null) + RandomSpawn.TP(AbductVictim.NetTransform, position); + } + , 0.25f, ""); + } + } + } + else if (AbductTimer <= 100f) + { + AbductTimer = 255f; + Player.RpcResetAbilityCooldown(); + } + } +} diff --git a/Roles/Impostor/SerialKiller.cs b/Roles/Impostor/SerialKiller.cs index 00ae40899..21aa56667 100644 --- a/Roles/Impostor/SerialKiller.cs +++ b/Roles/Impostor/SerialKiller.cs @@ -3,6 +3,7 @@ using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; +using TownOfHost.Roles.Neutral; using static TownOfHost.Translator; namespace TownOfHost.Roles.Impostor @@ -73,7 +74,7 @@ public override void OnReportDeadBody(PlayerControl reporter, GameData.PlayerInf } public override void OnFixedUpdate(PlayerControl player) { - if (AmongUsClient.Instance.AmHost) + if (AmongUsClient.Instance.AmHost && !ExileController.Instance) { if (!HasKilled()) { @@ -107,5 +108,9 @@ public override void AfterMeetingTasks() SuicideTimer = 0f; } } + public void OnSchrodingerCatKill(SchrodingerCat schrodingerCat) + { + SuicideTimer = null; + } } } \ No newline at end of file diff --git a/Roles/Impostor/Stealth.cs b/Roles/Impostor/Stealth.cs new file mode 100644 index 000000000..f6385207f --- /dev/null +++ b/Roles/Impostor/Stealth.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; +using Hazel; +using TownOfHost.Modules; +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; +using UnityEngine; + +namespace TownOfHost.Roles.Impostor; + +public sealed class Stealth : RoleBase, IImpostor +{ + public Stealth(PlayerControl player) : base(RoleInfo, player) + { + excludeImpostors = optionExcludeImpostors.GetBool(); + darkenDuration = darkenTimer = optionDarkenDuration.GetFloat(); + darkenedPlayers = null; + } + public static readonly SimpleRoleInfo RoleInfo = SimpleRoleInfo.Create( + typeof(Stealth), + player => new Stealth(player), + CustomRoles.Stealth, + () => RoleTypes.Impostor, + CustomRoleTypes.Impostor, + 3200, + SetupOptionItems, + "st", + introSound: () => GetIntroSound(RoleTypes.Shapeshifter)); + private static LogHandler logger = Logger.Handler(nameof(Stealth)); + + #region カスタムオプション + private static BooleanOptionItem optionExcludeImpostors; + private static FloatOptionItem optionDarkenDuration; + private enum OptionName { StealthExcludeImpostors, StealthDarkenDuration, } + private static void SetupOptionItems() + { + optionExcludeImpostors = BooleanOptionItem.Create(RoleInfo, 10, OptionName.StealthExcludeImpostors, true, false); + optionDarkenDuration = FloatOptionItem.Create(RoleInfo, 20, OptionName.StealthDarkenDuration, new(0.5f, 5f, 0.5f), 1f, false); + optionDarkenDuration.SetValueFormat(OptionFormat.Seconds); + } + #endregion + + private bool excludeImpostors; + private float darkenDuration; + /// 暗転解除までのタイマー + private float darkenTimer; + /// 今暗転させているプレイヤー 暗転効果が発生してないときはnull + private PlayerControl[] darkenedPlayers; + /// 暗くしている部屋 + private SystemTypes? darkenedRoom = null; + + public void OnCheckMurderAsKiller(MurderInfo info) + { + // キルできない,もしくは普通のキルじゃないならreturn + if (!info.CanKill || !info.DoKill || info.IsSuicide || info.IsAccident || info.IsFakeSuicide) + { + return; + } + var playersToDarken = FindPlayersInSameRoom(info.AttemptTarget); + if (playersToDarken == null) + { + logger.Info("部屋の当たり判定を取得できないため暗転を行いません"); + return; + } + if (excludeImpostors) + { + playersToDarken = playersToDarken.Where(player => !player.Is(CustomRoles.Impostor)); + } + DarkenPlayers(playersToDarken); + } + /// 自分と同じ部屋にいるプレイヤー全員を取得する + private IEnumerable FindPlayersInSameRoom(PlayerControl killedPlayer) + { + var room = killedPlayer.GetPlainShipRoom(); + if (room == null) + { + return null; + } + var roomArea = room.roomArea; + var roomName = room.RoomId; + RpcDarken(roomName); + return Main.AllAlivePlayerControls.Where(player => player != Player && player.Collider.IsTouching(roomArea)); + } + /// 渡されたプレイヤーを秒分視界ゼロにする + private void DarkenPlayers(IEnumerable playersToDarken) + { + darkenedPlayers = playersToDarken.ToArray(); + foreach (var player in playersToDarken) + { + PlayerState.GetByPlayerId(player.PlayerId).IsBlackOut = true; + player.MarkDirtySettings(); + } + } + public override void OnFixedUpdate(PlayerControl player) + { + if (!AmongUsClient.Instance.AmHost) + { + return; + } + // 誰かを暗転させているとき + if (darkenedPlayers != null) + { + // タイマーを減らす + darkenTimer -= Time.fixedDeltaTime; + // タイマーが0になったらみんなの視界を戻してタイマーと暗転プレイヤーをリセットする + if (darkenTimer <= 0) + { + ResetDarkenState(); + } + } + } + public override void OnStartMeeting() + { + ResetDarkenState(); + } + private void RpcDarken(SystemTypes? roomType) + { + logger.Info($"暗転させている部屋を{roomType?.ToString() ?? "null"}に設定"); + darkenedRoom = roomType; + using var sender = CreateSender(CustomRPC.StealthDarken); + sender.Writer.Write((byte?)roomType ?? byte.MaxValue); + } + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType == CustomRPC.StealthDarken) + { + var roomId = reader.ReadByte(); + darkenedRoom = roomId == byte.MaxValue ? null : (SystemTypes)roomId; + } + } + /// 発生している暗転効果を解除 + private void ResetDarkenState() + { + if (darkenedPlayers != null) + { + foreach (var player in darkenedPlayers) + { + PlayerState.GetByPlayerId(player.PlayerId).IsBlackOut = false; + player.MarkDirtySettings(); + } + darkenedPlayers = null; + } + darkenTimer = darkenDuration; + RpcDarken(null); + Utils.NotifyRoles(SpecifySeer: Player); + } + + public override string GetSuffix(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false) + { + seen ??= seer; + // 会議中,自分のSuffixじゃない,どこも暗転させてなければ何も出さない + if (isForMeeting || seer != Player || seen != Player || !darkenedRoom.HasValue) + { + return base.GetSuffix(seer, seen, isForMeeting); + } + return string.Format(Translator.GetString("StealthDarkened"), DestroyableSingleton.Instance.GetString(darkenedRoom.Value)); + } +} diff --git a/Roles/Madmate/MadSnitch.cs b/Roles/Madmate/MadSnitch.cs index a672fc1cb..facb85313 100644 --- a/Roles/Madmate/MadSnitch.cs +++ b/Roles/Madmate/MadSnitch.cs @@ -32,43 +32,58 @@ public MadSnitch(PlayerControl player) canVent = OptionCanVent.GetBool(); canAlsoBeExposedToImpostor = OptionCanAlsoBeExposedToImpostor.GetBool(); + TaskTrigger = OptionTaskTrigger.GetInt(); CustomRoleManager.MarkOthers.Add(GetMarkOthers); } private static OptionItem OptionCanVent; private static OptionItem OptionCanAlsoBeExposedToImpostor; + /// 能力発動タスク数 + private static OptionItem OptionTaskTrigger; private static Options.OverrideTasksData Tasks; enum OptionName { CanVent, MadSnitchCanAlsoBeExposedToImpostor, + MadSnitchTaskTrigger, } private static bool canSeeKillFlash; private static bool canSeeDeathReason; private static bool canVent; private static bool canAlsoBeExposedToImpostor; + private static int TaskTrigger; public static void SetupOptionItem() { OptionCanVent = BooleanOptionItem.Create(RoleInfo, 10, OptionName.CanVent, false, false); OptionCanAlsoBeExposedToImpostor = BooleanOptionItem.Create(RoleInfo, 11, OptionName.MadSnitchCanAlsoBeExposedToImpostor, false, false); + OptionTaskTrigger = IntegerOptionItem.Create(RoleInfo, 12, OptionName.MadSnitchTaskTrigger, new(0, 99, 1), 1, false).SetValueFormat(OptionFormat.Pieces); Tasks = Options.OverrideTasksData.Create(RoleInfo, 20); } - public bool KnowsImpostor() => IsTaskFinished; - - public override bool OnCompleteTask() + private bool KnowsImpostor() { - if (KnowsImpostor()) + return MyTaskState.HasCompletedEnoughCountOfTasks(TaskTrigger); + } + private void CheckAndAddNameColorToImpostors() + { + if (!KnowsImpostor()) return; + + foreach (var impostor in Main.AllPlayerControls.Where(player => player.Is(CustomRoleTypes.Impostor))) { - foreach (var impostor in Main.AllPlayerControls.Where(player => player.Is(CustomRoleTypes.Impostor)).ToArray()) - { - NameColorManager.Add(Player.PlayerId, impostor.PlayerId, impostor.GetRoleColorCode()); - } + NameColorManager.Add(Player.PlayerId, impostor.PlayerId, impostor.GetRoleColorCode()); } + } + public override void Add() + { + CheckAndAddNameColorToImpostors(); + } + public override bool OnCompleteTask() + { + CheckAndAddNameColorToImpostors(); return true; } public static string GetMarkOthers(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false) diff --git a/Roles/Neutral/Arsonist.cs b/Roles/Neutral/Arsonist.cs index 4ee8d12d1..6b4038fc3 100644 --- a/Roles/Neutral/Arsonist.cs +++ b/Roles/Neutral/Arsonist.cs @@ -21,6 +21,7 @@ public sealed class Arsonist : RoleBase, IKiller SetupOptionItem, "ar", "#ff6633", + true, introSound: () => GetIntroSound(RoleTypes.Crewmate) ); public Arsonist(PlayerControl player) @@ -215,6 +216,16 @@ public override string GetMark(PlayerControl seer, PlayerControl seen, bool isFo return ""; } + public override string GetLowerText(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false, bool isForHud = false) + { + if (isForMeeting) return ""; + //seenが省略の場合seer + seen ??= seer; + //seeおよびseenが自分である場合以外は関係なし + if (!Is(seer) || !Is(seen)) return ""; + + return IsDouseDone(Player) ? Utils.ColorString(RoleInfo.RoleColor, GetString("EnterVentToWin")) : ""; + } public bool IsDousedPlayer(byte targetId) => IsDoused.TryGetValue(targetId, out bool isDoused) && isDoused; public static bool IsDouseDone(PlayerControl player) { diff --git a/Roles/Neutral/Egoist.cs b/Roles/Neutral/Egoist.cs index 74dacddc0..57708620b 100644 --- a/Roles/Neutral/Egoist.cs +++ b/Roles/Neutral/Egoist.cs @@ -6,7 +6,7 @@ using TownOfHost.Roles.Core.Interfaces; namespace TownOfHost.Roles.Neutral; -public sealed class Egoist : RoleBase, ISidekickable, IKiller +public sealed class Egoist : RoleBase, ISidekickable, IKiller, ISchrodingerCatOwner { public static readonly SimpleRoleInfo RoleInfo = SimpleRoleInfo.Create( @@ -39,6 +39,9 @@ public Egoist(PlayerControl player) public static bool CanCreateMadmate; public static List Egoists = new(3); + + public SchrodingerCat.TeamType SchrodingerCatChangeTo => SchrodingerCat.TeamType.Egoist; + private static void SetupOptionItem() { OptionKillCooldown = FloatOptionItem.Create(RoleInfo, 10, GeneralOption.KillCooldown, new(2.5f, 180f, 2.5f), 20f, false) @@ -76,7 +79,10 @@ private static void Win() { CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Egoist); CustomWinnerHolder.WinnerRoles.Add(CustomRoles.Egoist); - CustomWinnerHolder.WinnerRoles.Add(CustomRoles.EgoSchrodingerCat); } public bool CanMakeSidekick() => CanCreateMadmate; + public void ApplySchrodingerCatOptions(IGameOptions option) + { + option.SetVision(true); + } } \ No newline at end of file diff --git a/Roles/Neutral/Jackal.cs b/Roles/Neutral/Jackal.cs index 8689097f4..f91a24238 100644 --- a/Roles/Neutral/Jackal.cs +++ b/Roles/Neutral/Jackal.cs @@ -5,7 +5,7 @@ namespace TownOfHost.Roles.Neutral { - public sealed class Jackal : RoleBase, IKiller + public sealed class Jackal : RoleBase, IKiller, ISchrodingerCatOwner { public static readonly SimpleRoleInfo RoleInfo = SimpleRoleInfo.Create( @@ -18,7 +18,9 @@ public sealed class Jackal : RoleBase, IKiller SetupOptionItem, "jac", "#00b4eb", - countType: CountTypes.Jackal + true, + countType: CountTypes.Jackal, + assignCountRule: new(1, 1, 1) ); public Jackal(PlayerControl player) : base( @@ -41,6 +43,9 @@ public Jackal(PlayerControl player) public static bool CanVent; public static bool CanUseSabotage; private static bool HasImpostorVision; + + public SchrodingerCat.TeamType SchrodingerCatChangeTo => SchrodingerCat.TeamType.Jackal; + private static void SetupOptionItem() { OptionKillCooldown = FloatOptionItem.Create(RoleInfo, 10, GeneralOption.KillCooldown, new(2.5f, 180f, 2.5f), 30f, false) @@ -56,5 +61,6 @@ public static void SetHudActive(HudManager __instance, bool isActive) __instance.SabotageButton.ToggleVisible(isActive && CanUseSabotage); } public override bool OnInvokeSabotage(SystemTypes systemType) => CanUseSabotage; + public void ApplySchrodingerCatOptions(IGameOptions option) => ApplyGameOptions(option); } } \ No newline at end of file diff --git a/Roles/Neutral/PlagueDoctor.cs b/Roles/Neutral/PlagueDoctor.cs new file mode 100644 index 000000000..6ae6f2bb3 --- /dev/null +++ b/Roles/Neutral/PlagueDoctor.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using AmongUs.GameOptions; + +using TownOfHost.Roles.Core; +using TownOfHost.Roles.Core.Interfaces; +using static TownOfHost.Translator; +using Hazel; + +namespace TownOfHost.Roles.Neutral; + +public sealed class PlagueDoctor : RoleBase, IKiller +{ + public static readonly SimpleRoleInfo RoleInfo = + SimpleRoleInfo.Create( + typeof(PlagueDoctor), + player => new PlagueDoctor(player), + CustomRoles.PlagueDoctor, + () => RoleTypes.Impostor, + CustomRoleTypes.Neutral, + 51100, + SetupOptionItem, + "pd", + "#ff6633", + true, + introSound: () => GetIntroSound(RoleTypes.Crewmate) + ); + public PlagueDoctor(PlayerControl player) + : base( + RoleInfo, + player, + () => HasTask.False + ) + { + PlagueDoctors.Add(this); + if (PlagueDoctors.Count == 1) + { + InfectLimit = OptionInfectLimit.GetInt(); + InfectWhenKilled = OptionInfectWhenKilled.GetBool(); + InfectTime = OptionInfectTime.GetFloat(); + InfectDistance = OptionInfectDistance.GetFloat(); + InfectInactiveTime = OptionInfectInactiveTime.GetFloat(); + CanInfectSelf = OptionInfectCanInfectSelf.GetBool(); + CanInfectVent = OptionInfectCanInfectVent.GetBool(); + + InfectInfos = new(GameData.Instance.PlayerCount); + //他視点用のMarkメソッド登録 + CustomRoleManager.MarkOthers.Add(GetMarkOthers); + CustomRoleManager.LowerOthers.Add(GetLowerTextOthers); + CustomRoleManager.OnFixedUpdateOthers.Add(OnFixedUpdateOthers); + CustomRoleManager.OnMurderPlayerOthers.Add(OnMurderPlayerOthers); + } + } + public override void OnDestroy() + { + PlagueDoctors.Clear(); + } + public bool CanKill { get; private set; } = false; + + private static OptionItem OptionInfectLimit; + private static OptionItem OptionInfectWhenKilled; + private static OptionItem OptionInfectTime; + private static OptionItem OptionInfectDistance; + private static OptionItem OptionInfectInactiveTime; + private static OptionItem OptionInfectCanInfectSelf; + private static OptionItem OptionInfectCanInfectVent; + + private static int InfectLimit; + private static bool InfectWhenKilled; + private static float InfectTime; + private static float InfectDistance; + private static float InfectInactiveTime; + private static bool CanInfectSelf; + private static bool CanInfectVent; + enum OptionName + { + PlagueDoctorInfectLimit, + PlagueDoctorInfectWhenKilled, + PlagueDoctorInfectTime, + PlagueDoctorInfectDistance, + PlagueDoctorInfectInactiveTime, + PlagueDoctorCanInfectSelf, + PlagueDoctorCanInfectVent, + } + private static void SetupOptionItem() + { + OptionInfectLimit = IntegerOptionItem.Create(RoleInfo, 10, OptionName.PlagueDoctorInfectLimit, new(1, 3, 1), 1, false) + .SetValueFormat(OptionFormat.Times); + OptionInfectWhenKilled = BooleanOptionItem.Create(RoleInfo, 11, OptionName.PlagueDoctorInfectWhenKilled, false, true); + OptionInfectTime = FloatOptionItem.Create(RoleInfo, 12, OptionName.PlagueDoctorInfectTime, new(3f, 20f, 1f), 8f, false) + .SetValueFormat(OptionFormat.Seconds); + OptionInfectDistance = FloatOptionItem.Create(RoleInfo, 13, OptionName.PlagueDoctorInfectDistance, new(0.5f, 2f, 0.25f), 1.5f, false); + OptionInfectInactiveTime = FloatOptionItem.Create(RoleInfo, 14, OptionName.PlagueDoctorInfectInactiveTime, new(0.5f, 10f, 0.5f), 5f, false) + .SetValueFormat(OptionFormat.Seconds); + OptionInfectCanInfectSelf = BooleanOptionItem.Create(RoleInfo, 15, OptionName.PlagueDoctorCanInfectSelf, false, true); + OptionInfectCanInfectVent = BooleanOptionItem.Create(RoleInfo, 16, OptionName.PlagueDoctorCanInfectVent, false, true); + } + + private int InfectCount; + private static Dictionary InfectInfos; + private static bool InfectActive; + private static bool LateCheckWin; + private static List PlagueDoctors = new(); + + public override void Add() + { + InfectCount = InfectLimit; + + InfectActive = true; + if (Main.NormalOptions.MapId == 4) + //エアシップのリスポーン選択分固定で遅延させる + InfectInactiveTime += 5f; + } + public bool CanUseKillButton() => InfectCount != 0; + public bool OverrideKillButtonText(out string text) + { + text = GetString("Infected"); + return true; + } + public override bool OnInvokeSabotage(SystemTypes systemType) => false; + public override string GetProgressText(bool comms = false) + { + return Utils.ColorString(RoleInfo.RoleColor.ShadeColor(0.25f), $"({InfectCount})"); + } + public override void ApplyGameOptions(IGameOptions opt) + { + opt.SetVision(false); + } + public static bool CanInfect(PlayerControl player) + { + var pd = PlagueDoctors.FirstOrDefault(x => x.Player == player); + //ペスト医師でないか、自己感染可能かつ感染者作成済み + return pd == null || (CanInfectSelf && pd.InfectCount == 0); + } + public void SendRPC(byte targetId, float rate) + { + using var sender = CreateSender(CustomRPC.SyncPlagueDoctor); + sender.Writer.Write(targetId); + sender.Writer.Write(rate); + } + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType != CustomRPC.SyncPlagueDoctor) return; + + var targetId = reader.ReadByte(); + var rate = reader.ReadSingle(); + InfectInfos[targetId] = rate; + } + public void OnCheckMurderAsKiller(MurderInfo info) + { + var (killer, target) = info.AttemptTuple; + if (InfectCount > 0) + { + InfectCount--; + killer.RpcGuardAndKill(target); + DirectInfect(target); + } + info.DoKill = false; + } + public override void OnMurderPlayerAsTarget(MurderInfo info) + { + var (killer, target) = info.AttemptTuple; + if (InfectWhenKilled && InfectCount > 0) + { + InfectCount = 0; + DirectInfect(killer); + } + } + public static void OnMurderPlayerOthers(MurderInfo info) + { + //非感染者が死んだ場合勝利するかもしれない + LateCheckWin = true; + } + public override void OnReportDeadBody(PlayerControl reporter, GameData.PlayerInfo target) + { + InfectActive = false; + } + public static void OnFixedUpdateOthers(PlayerControl player) + { + if (!AmongUsClient.Instance.AmHost) return; + + if (!GameStates.IsInTask) return; + if (LateCheckWin) + { + //吊り/キルの後、念のため勝利条件チェック + LateCheckWin = false; + CheckWin(); + } + if (!player.IsAlive() || !InfectActive) return; + + if (InfectInfos.TryGetValue(player.PlayerId, out var rate) && rate >= 100) + { + //感染者の場合 + var changed = false; + var inVent = player.inVent; + foreach (var target in Main.AllAlivePlayerControls) + { + //ペスト医師は自身が感染できない場合は除外 + if (!CanInfect(target)) continue; + //ベント内外であれば除外 + if (!CanInfectVent && target.inVent != inVent) continue; + + InfectInfos.TryGetValue(target.PlayerId, out var oldRate); + //感染者は除外 + if (oldRate >= 100) continue; + + //範囲外は除外 + var distance = UnityEngine.Vector3.Distance(player.transform.position, target.transform.position); + if (distance > InfectDistance) continue; + + var newRate = oldRate + Time.fixedDeltaTime / InfectTime * 100; + newRate = Math.Clamp(newRate, 0, 100); + InfectInfos[target.PlayerId] = newRate; + if ((oldRate < 50 && newRate >= 50) || newRate >= 100) + { + changed = true; + Logger.Info($"InfectRate[{target.GetNameWithRole()}]:{newRate}%", "OnCheckMurderAsKiller"); + PlagueDoctors[0].SendRPC(target.PlayerId, newRate); + } + } + if (changed) + { + //誰かの感染が進行していたら + CheckWin(); + Utils.NotifyRoles(); + } + } + } + public override void AfterMeetingTasks() + { + if (PlagueDoctors[0] == this) + { + //非感染者が吊られた場合勝利するかもしれない + LateCheckWin = true; + + _ = new LateTask(() => + { + Logger.Info("InfectActive", "PlagueDoctor"); + InfectActive = true; + }, + InfectInactiveTime, "ResetInfectInactiveTime"); + } + } + + public static string GetMarkOthers(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false) + { + seen ??= seer; + if (!CanInfect(seen)) return ""; + if (!seer.Is(CustomRoles.PlagueDoctor) && seer.IsAlive()) return ""; + var str = new StringBuilder(40); + str.Append($""); + str.Append(GetInfectRateCharactor(seen)); + str.Append(""); + return str.ToString(); + } + public static string GetLowerTextOthers(PlayerControl seer, PlayerControl seen = null, bool isForMeeting = false, bool isForHud = false) + { + seen ??= seer; + if (!seen.Is(CustomRoles.PlagueDoctor)) return ""; + if (!seer.Is(CustomRoles.PlagueDoctor) && seer.IsAlive()) return ""; + var str = new StringBuilder(40); + str.Append($""); + foreach (var player in Main.AllAlivePlayerControls) + { + if (!player.Is(CustomRoles.PlagueDoctor)) + str.Append(GetInfectRateCharactor(player)); + } + str.Append(""); + return str.ToString(); + } + public static bool IsInfected(byte playerId) + { + InfectInfos.TryGetValue(playerId, out var rate); + return rate >= 100; + } + public static string GetInfectRateCharactor(PlayerControl player) + { + if (!CanInfect(player) || !player.IsAlive()) return ""; + InfectInfos.TryGetValue(player.PlayerId, out var rate); + return rate switch + { + < 50 => "\u2581", + >= 50 and < 100 => "\u2584", + >= 100 => "\u2588", + _ => "" + }; + } + public void DirectInfect(PlayerControl player) + { + Logger.Info($"InfectRate[{player.GetNameWithRole()}]:100%", "OnCheckMurderAsKiller"); + InfectInfos[player.PlayerId] = 100; + SendRPC(player.PlayerId, 100); + Utils.NotifyRoles(); + CheckWin(); + } + public static void CheckWin() + { + if (!AmongUsClient.Instance.AmHost) return; + //だれかの勝利処理中なら無効 + if (CustomWinnerHolder.WinnerTeam != CustomWinner.Default) return; + + bool comprete = Main.AllAlivePlayerControls.All(p => p.Is(CustomRoles.PlagueDoctor) || IsInfected(p.PlayerId)); + + if (comprete) + { + InfectActive = false; + + foreach (var player in Main.AllAlivePlayerControls) + { + if (player.Is(CustomRoles.PlagueDoctor)) continue; + player.SetRealKiller(null); + player.RpcMurderPlayer(player); + var state = PlayerState.GetByPlayerId(player.PlayerId); + state.DeathReason = CustomDeathReason.Infected; + state.SetDead(); + } + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.PlagueDoctor); + foreach (var plagueDoctor in Main.AllPlayerControls.Where(p => p.Is(CustomRoles.PlagueDoctor))) + CustomWinnerHolder.WinnerIds.Add(plagueDoctor.PlayerId); + } + } +} diff --git a/Roles/Neutral/SchrodingerCat.cs b/Roles/Neutral/SchrodingerCat.cs index 02b3a84c9..17fdc815c 100644 --- a/Roles/Neutral/SchrodingerCat.cs +++ b/Roles/Neutral/SchrodingerCat.cs @@ -1,13 +1,19 @@ using System.Collections.Generic; using System.Linq; + +using UnityEngine; + using AmongUs.GameOptions; +using Hazel; +using TownOfHost.Modules; using TownOfHost.Roles.Core; using TownOfHost.Roles.Core.Interfaces; -using TownOfHost.Roles.Impostor; namespace TownOfHost.Roles.Neutral; -public sealed class SchrodingerCat : RoleBase, IAdditionalWinner + +// マッドが属性化したらマッド状態時の特別扱いを削除する +public sealed class SchrodingerCat : RoleBase, IAdditionalWinner, IDeathReasonSeeable, IKillFlashSeeable { public static readonly SimpleRoleInfo RoleInfo = SimpleRoleInfo.Create( @@ -46,104 +52,240 @@ enum OptionName static bool ChangeTeamWhenExile; static bool CanSeeKillableTeammate; + /// + /// 自分をキルしてきた人のロール + /// + private ISchrodingerCatOwner owner = null; + private TeamType _team = TeamType.None; + /// + /// 現在の所属陣営
+ /// 変更する際は特段の事情がない限りを使ってください + ///
+ public TeamType Team + { + get => _team; + private set + { + logger.Info($"{Player.GetRealName()}の陣営を{value}に変更"); + _team = value; + } + } + public bool AmMadmate => Team == TeamType.Mad; + public Color DisplayRoleColor => GetCatColor(Team); + private static LogHandler logger = Logger.Handler(nameof(SchrodingerCat)); + public static void SetupOptionItem() { OptionCanWinTheCrewmateBeforeChange = BooleanOptionItem.Create(RoleInfo, 10, OptionName.CanBeforeSchrodingerCatWinTheCrewmate, false, false); OptionChangeTeamWhenExile = BooleanOptionItem.Create(RoleInfo, 11, OptionName.SchrodingerCatExiledTeamChanges, false, false); OptionCanSeeKillableTeammate = BooleanOptionItem.Create(RoleInfo, 12, OptionName.SchrodingerCatCanSeeKillableTeammate, false, false); } - public override bool OnCheckMurderAsTarget(MurderInfo info) + public override void ApplyGameOptions(IGameOptions opt) + { + owner?.ApplySchrodingerCatOptions(opt); + } + /// + /// マッド猫用のオプション構築 + /// + public static void ApplyMadCatOptions(IGameOptions opt) { - if (Is(info.AttemptTarget)) + if (Options.MadmateHasImpostorVision.GetBool()) + { + opt.SetVision(true); + } + if (Options.MadmateCanSeeOtherVotes.GetBool()) { - (var killer, var target) = info.AttemptTuple; + opt.SetBool(BoolOptionNames.AnonymousVotes, false); + } + } + public override bool OnCheckMurderAsTarget(MurderInfo info) + { + var killer = info.AttemptKiller; - //自殺ならスルー - if (info.IsSuicide) return true; - //既に変化していたらスルー - if (!target.Is(CustomRoles.SchrodingerCat)) return true; + //自殺ならスルー + if (info.IsSuicide) return true; - //シュレディンガーの猫が切られた場合の役職変化スタート - killer.RpcGuardAndKill(target); + if (Team == TeamType.None) + { info.CanKill = false; - switch (killer.GetCustomRole()) - { - case CustomRoles.BountyHunter: - var bountyHunter = (BountyHunter)killer.GetRoleClass(); - if (bountyHunter.GetTarget() == target) - bountyHunter.ResetTarget();//ターゲットの選びなおし - break; - case CustomRoles.SerialKiller: - var serialKiller = (SerialKiller)killer.GetRoleClass(); - serialKiller.SuicideTimer = null; - break; - case CustomRoles.Sheriff: - target.RpcSetCustomRole(CustomRoles.CSchrodingerCat); - break; - case CustomRoles.Egoist: - target.RpcSetCustomRole(CustomRoles.EgoSchrodingerCat); - break; - case CustomRoles.Jackal: - target.RpcSetCustomRole(CustomRoles.JSchrodingerCat); - break; - } - if (killer.Is(CustomRoleTypes.Impostor)) - target.RpcSetCustomRole(CustomRoles.MSchrodingerCat); - - if (CanSeeKillableTeammate) - { - var roleType = killer.GetCustomRole().GetCustomRoleTypes(); - System.Func isTarget = roleType switch - { - CustomRoleTypes.Impostor => (pc) => pc.GetCustomRole().GetCustomRoleTypes() == roleType, - _ => (pc) => pc.GetCustomRole() == killer.GetCustomRole() - }; - ; - var killerTeam = Main.AllPlayerControls.Where(pc => isTarget(pc)); - foreach (var member in killerTeam) - { - NameColorManager.Add(member.PlayerId, target.PlayerId, RoleInfo.RoleColorCode); - NameColorManager.Add(target.PlayerId, member.PlayerId); - } - } - else - { - NameColorManager.Add(killer.PlayerId, target.PlayerId, RoleInfo.RoleColorCode); - NameColorManager.Add(target.PlayerId, killer.PlayerId); - } - Utils.NotifyRoles(); - Utils.MarkEveryoneDirtySettings(); - //シュレディンガーの猫の役職変化処理終了 - //ニュートラルのキル能力持ちが追加されたら、その陣営を味方するシュレディンガーの猫の役職を作って上と同じ書き方で書いてください + ChangeTeamOnKill(killer); return false; - } return true; } - public static void ChangeTeam(PlayerControl player) + /// + /// キルしてきた人に応じて陣営の状態を変える + /// + private void ChangeTeamOnKill(PlayerControl killer) { - if (!(ChangeTeamWhenExile && player.Is(CustomRoles.SchrodingerCat))) return; + killer.RpcGuardAndKill(Player); + if (killer.GetRoleClass() is ISchrodingerCatOwner catOwner) + { + catOwner.OnSchrodingerCatKill(this); + RpcSetTeam(catOwner.SchrodingerCatChangeTo); + owner = catOwner; + } + else + { + logger.Warn($"未知のキル役職からのキル: {killer.GetNameWithRole()}"); + } - var rand = IRandom.Instance; - List Rand = new() + RevealNameColors(killer); + + Utils.NotifyRoles(); + Utils.MarkEveryoneDirtySettings(); + } + /// + /// キルしてきた人とオプションに応じて名前の色を開示する + /// + private void RevealNameColors(PlayerControl killer) + { + if (CanSeeKillableTeammate) + { + var killerRoleId = killer.GetCustomRole(); + var killerTeam = Main.AllPlayerControls.Where(player => (AmMadmate && player.Is(CustomRoleTypes.Impostor)) || player.Is(killerRoleId)); + foreach (var member in killerTeam) { - CustomRoles.CSchrodingerCat, - CustomRoles.MSchrodingerCat - }; - foreach (var pc in Main.AllAlivePlayerControls) + NameColorManager.Add(member.PlayerId, Player.PlayerId, RoleInfo.RoleColorCode); + NameColorManager.Add(Player.PlayerId, member.PlayerId); + } + } + else { - if (pc.Is(CustomRoles.Egoist) && !Rand.Contains(CustomRoles.EgoSchrodingerCat)) - Rand.Add(CustomRoles.EgoSchrodingerCat); - - if (pc.Is(CustomRoles.Jackal) && !Rand.Contains(CustomRoles.JSchrodingerCat)) - Rand.Add(CustomRoles.JSchrodingerCat); + NameColorManager.Add(killer.PlayerId, Player.PlayerId, RoleInfo.RoleColorCode); + NameColorManager.Add(Player.PlayerId, killer.PlayerId); } - var Role = Rand[rand.Next(Rand.Count)]; - player.RpcSetCustomRole(Role); + } + public override void OverrideTrueRoleName(ref Color roleColor, ref string roleText) + { + // 陣営変化前なら上書き不要 + if (Team == TeamType.None) + { + return; + } + roleColor = DisplayRoleColor; + } + public override void OnExileWrapUp(GameData.PlayerInfo exiled, ref bool DecidedWinner) + { + if (exiled.PlayerId != Player.PlayerId || Team != TeamType.None || !ChangeTeamWhenExile) + { + return; + } + ChangeTeamRandomly(); + } + /// + /// ゲームに存在している陣営の中からランダムに自分の陣営を変更する + /// + private void ChangeTeamRandomly() + { + var rand = IRandom.Instance; + List candidates = new(4) + { + TeamType.Crew, + TeamType.Mad, + }; + if (CustomRoles.Egoist.IsPresent()) + { + candidates.Add(TeamType.Egoist); + } + if (CustomRoles.Jackal.IsPresent()) + { + candidates.Add(TeamType.Jackal); + } + var team = candidates[rand.Next(candidates.Count)]; + RpcSetTeam(team); } public bool CheckWin(out AdditionalWinners winnerType) { winnerType = AdditionalWinners.SchrodingerCat; - return CustomWinnerHolder.WinnerTeam == CustomWinner.Crewmate && CanWinTheCrewmateBeforeChange; + bool? won = Team switch + { + TeamType.None => CustomWinnerHolder.WinnerTeam == CustomWinner.Crewmate && CanWinTheCrewmateBeforeChange, + TeamType.Mad => CustomWinnerHolder.WinnerTeam == CustomWinner.Impostor, + TeamType.Crew => CustomWinnerHolder.WinnerTeam == CustomWinner.Crewmate, + TeamType.Jackal => CustomWinnerHolder.WinnerTeam == CustomWinner.Jackal, + TeamType.Egoist => CustomWinnerHolder.WinnerTeam == CustomWinner.Egoist, + _ => null, + }; + if (!won.HasValue) + { + logger.Warn($"不明な猫の勝利チェック: {Team}"); + return false; + } + return won.Value; + } + public void RpcSetTeam(TeamType team) + { + Team = team; + if (AmongUsClient.Instance.AmHost) + { + using var sender = CreateSender(CustomRPC.SetSchrodingerCatTeam); + sender.Writer.Write((byte)team); + } + } + public override void ReceiveRPC(MessageReader reader, CustomRPC rpcType) + { + if (rpcType != CustomRPC.SetSchrodingerCatTeam) + { + return; + } + Team = (TeamType)reader.ReadByte(); + } + + // マッド属性化までの間マッド状態時に特別扱いするための応急処置的個別実装 + // マッドが属性化したらマッド状態のシュレ猫にマッド属性を付与することで削除 + // 上にあるApplyMadCatOptions,MeetingHudPatchにある道連れ処理,ShipStatusPatchにあるサボ直しキャンセル処理も同様 - Hyz-sui + public bool CheckSeeDeathReason(PlayerControl seen) => AmMadmate && Options.MadmateCanSeeDeathReason.GetBool(); + public bool CheckKillFlash(MurderInfo info) => AmMadmate && Options.MadmateCanSeeKillFlash.GetBool(); + + /// + /// 陣営状態 + /// + public enum TeamType : byte + { + /// + /// どこの陣営にも属していない状態 + /// + None = 0, + + // 10-49 シェリフキルオプションを作成しない変化先 + + /// + /// インポスター陣営に所属する状態 + /// + Mad = 10, + /// + /// クルー陣営に所属する状態 + /// + Crew, + + // 50- シェリフキルオプションを作成する変化先 + + /// + /// ジャッカル陣営に所属する状態 + /// + Jackal = 50, + /// + /// エゴイスト陣営に所属する状態 + /// + Egoist, + } + public static Color GetCatColor(TeamType catType) + { + Color? color = catType switch + { + TeamType.None => RoleInfo.RoleColor, + TeamType.Mad => Utils.GetRoleColor(CustomRoles.Madmate), + TeamType.Crew => Utils.GetRoleColor(CustomRoles.Crewmate), + TeamType.Jackal => Utils.GetRoleColor(CustomRoles.Jackal), + TeamType.Egoist => Utils.GetRoleColor(CustomRoles.Egoist), + _ => null, + }; + if (!color.HasValue) + { + logger.Warn($"不明な猫に対する色の取得: {catType}"); + return Utils.GetRoleColor(CustomRoles.Crewmate); + } + return color.Value; } } diff --git a/Roles/RoleAssignManager.cs b/Roles/RoleAssignManager.cs index fb315e92e..db919b775 100644 --- a/Roles/RoleAssignManager.cs +++ b/Roles/RoleAssignManager.cs @@ -54,20 +54,21 @@ private enum AssignAlgorithm "AssignAlgorithm.Random" }; private static readonly CustomRoles[] AllMainRoles = CustomRolesHelper.AllRoles.Where(role => role < CustomRoles.NotAssigned).ToArray(); + public static OptionItem OptionAssignMode; private static Dictionary RandomAssignOptionsCollection = new(CustomRolesHelper.AllRoleTypes.Length); private static Dictionary AssignCount = new(CustomRolesHelper.AllRoleTypes.Length); private static List AssignRoleList = new(CustomRolesHelper.AllRoles.Length); public static void SetupOptionItem() { - var optionAssignMode = StringOptionItem.Create(idStart, "AssignMode", AssignModeSelections, 0, TabGroup.MainSettings, false) + OptionAssignMode = StringOptionItem.Create(idStart, "AssignMode", AssignModeSelections, 0, TabGroup.MainSettings, false) .SetHeader(true); - assignMode = () => (AssignAlgorithm)optionAssignMode.GetInt(); + assignMode = () => (AssignAlgorithm)OptionAssignMode.GetInt(); RandomAssignOptionsCollection.Clear(); - RandomAssignOptions.Create(10, optionAssignMode, CustomRoleTypes.Impostor, 3); - RandomAssignOptions.Create(20, optionAssignMode, CustomRoleTypes.Madmate); - RandomAssignOptions.Create(30, optionAssignMode, CustomRoleTypes.Crewmate); - RandomAssignOptions.Create(40, optionAssignMode, CustomRoleTypes.Neutral); + RandomAssignOptions.Create(10, OptionAssignMode, CustomRoleTypes.Impostor, 3); + RandomAssignOptions.Create(20, OptionAssignMode, CustomRoleTypes.Madmate); + RandomAssignOptions.Create(30, OptionAssignMode, CustomRoleTypes.Crewmate); + RandomAssignOptions.Create(40, OptionAssignMode, CustomRoleTypes.Neutral); } public static bool CheckRoleCount() { @@ -139,7 +140,7 @@ private static void SetFixedAssignRole() { if (numImpostorsLeft <= 0 && numOthersLeft <= 0) break; - var targetRoles = role.GetAssignTargetRolesArray(); + var targetRoles = role.GetAssignUnitRolesArray(); var numImpostorAssign = targetRoles.Count(role => role.IsImpostor()); var numOthersAssign = targetRoles.Length - numImpostorAssign; //アサイン枠が足りてない場合 @@ -225,7 +226,7 @@ private static void SetRandomAssignRoleList() foreach (var role in GetCandidateRoleList(100).OrderBy(x => Guid.NewGuid())) { - var targetRoles = role.GetAssignTargetRolesArray(); + var targetRoles = role.GetAssignUnitRolesArray(); //アサイン枠が足りてない場合 if (CustomRolesHelper.AllRoleTypes.Any( type => assignCount.TryGetValue(type, out var count) && @@ -261,7 +262,7 @@ private static void SetRandomAssignRoleList() while (assignCount.Any(kvp => kvp.Value > 0) && randomRoleTicketPool.Count > 0) { var selectedTicket = randomRoleTicketPool[rand.Next(randomRoleTicketPool.Count)]; - var targetRoles = selectedTicket.Item1.GetAssignTargetRolesArray(); + var targetRoles = selectedTicket.Item1.GetAssignUnitRolesArray(); //アサイン枠が足りていれば追加 if (CustomRolesHelper.AllRoleTypes.All(type => targetRoles.Count(role => role.GetCustomRoleTypes() == type) <= assignCount[type])) { @@ -284,15 +285,12 @@ private static void SetAddOnsList(bool isFixedAssign) foreach (var subRole in CustomRolesHelper.AllRoles.Where(x => x > CustomRoles.NotAssigned)) { var chance = subRole.GetChance(); - var count = subRole.GetCount(); + var count = subRole.GetAssignCount(); if (chance == 0 || count == 0) continue; - var numAssignUnit = 1; //アサインの最小単位 - if (subRole == CustomRoles.Lovers) - numAssignUnit = 2; var rnd = IRandom.Instance; - for (var i = 0; i < count / numAssignUnit; i++) //役職の単位数ごとに抽選 + for (var i = 0; i < count; i++) //役職の単位数ごとに抽選 if (isFixedAssign || rnd.Next(100) < chance) - AssignRoleList.AddRange(subRole.GetAssignTargetRolesArray()); + AssignRoleList.AddRange(subRole.GetAssignUnitRolesArray()); } } private static List GetCandidateRoleList(int availableRate) @@ -303,7 +301,7 @@ private static List GetCandidateRoleList(int availableRate) if (!role.IsAssignable()) continue; var chance = role.GetChance(); - var count = role.GetCount(); + var count = role.GetAssignCount(); if (chance < availableRate || count == 0) continue; candidateRoleList.AddRange(Enumerable.Repeat(role, count).ToList()); } @@ -316,12 +314,27 @@ private static bool IsAssignable(this CustomRoles role) CustomRoles.Egoist => Main.RealOptionsData.GetInt(Int32OptionNames.NumImpostors) > 1, _ => true, }; + /// + /// アサインの抽選回数 + /// + private static int GetAssignCount(this CustomRoles role) + { + int maximumCount = role.GetCount(); + int assignUnitCount = CustomRoleManager.GetRoleInfo(role)?.AssignUnitCount ?? + role switch + { + CustomRoles.Lovers => 2, + _ => 1, + }; + return maximumCount / assignUnitCount; + } /// ///RoleOptionのKey => 実際にアサインされる役職の配列 ///両陣営役職、コンビ役職向け /// - private static CustomRoles[] GetAssignTargetRolesArray(this CustomRoles role) - => role switch + private static CustomRoles[] GetAssignUnitRolesArray(this CustomRoles role) + => CustomRoleManager.GetRoleInfo(role)?.AssignUnitRoles ?? + role switch { CustomRoles.Lovers => new CustomRoles[2] { CustomRoles.Lovers, CustomRoles.Lovers }, _ => new CustomRoles[1] { role }, diff --git a/Roles/Vanilla/Crewmate.cs b/Roles/Vanilla/Crewmate.cs index 52aa5b899..895c05c76 100644 --- a/Roles/Vanilla/Crewmate.cs +++ b/Roles/Vanilla/Crewmate.cs @@ -10,7 +10,8 @@ public sealed class Crewmate : RoleBase SimpleRoleInfo.CreateForVanilla( typeof(Crewmate), player => new Crewmate(player), - RoleTypes.Crewmate + RoleTypes.Crewmate, + "#8cffff" ); public Crewmate(PlayerControl player) : base( diff --git a/Templates/TMPTemplate.cs b/Templates/TMPTemplate.cs new file mode 100644 index 000000000..883a00239 --- /dev/null +++ b/Templates/TMPTemplate.cs @@ -0,0 +1,44 @@ +using TMPro; +using UnityEngine; + +namespace TownOfHost.Templates; + +public sealed class TMPTemplate +{ + private static TextMeshPro baseTMP; + public static void SetBase(TextMeshPro tmp) + { + if (baseTMP != null) return; + + baseTMP = Object.Instantiate(tmp); + Object.Destroy(baseTMP.GetComponent()); + Object.DontDestroyOnLoad(baseTMP); + baseTMP.gameObject.SetActive(false); + baseTMP.gameObject.name = "TMPTemplateBase"; + } + public static TextMeshPro Create( + string name, + string text = null, + Color? color = null, + float? fontSize = null, + TextAlignmentOptions? alignment = null, + bool setActive = false, + Transform parent = null + ) + { + var replicatedObject = parent == null + ? Object.Instantiate(baseTMP) + : Object.Instantiate(baseTMP, parent); + replicatedObject.text = text ?? ""; + replicatedObject.color = color ?? Color.white; + replicatedObject.fontSize = + replicatedObject.fontSizeMax = + replicatedObject.fontSizeMin = fontSize ?? baseTMP.fontSize; + replicatedObject.alignment = alignment ?? TextAlignmentOptions.Center; + + replicatedObject.gameObject.SetActive(setActive); + replicatedObject.gameObject.name = name; + + return replicatedObject; + } +} \ No newline at end of file diff --git a/main.cs b/main.cs index 4a171661d..311e033e3 100644 --- a/main.cs +++ b/main.cs @@ -50,7 +50,7 @@ public class Main : BasePlugin // ========== //Sorry for many Japanese comments. public const string PluginGuid = "com.emptybottle.townofhost"; - public const string PluginVersion = "5.0.3"; + public const string PluginVersion = "5.1.0"; // サポートされている最低のAmongUsバージョン public static readonly string LowestSupportedVersion = "2023.7.11"; public Harmony Harmony { get; } = new Harmony(PluginGuid); @@ -86,7 +86,7 @@ public class Main : BasePlugin public static Dictionary<(byte, byte), string> LastNotifyNames; public static Dictionary PlayerColors = new(); public static Dictionary AfterMeetingDeathPlayers = new(); - public static Dictionary roleColors; + public static Dictionary roleColors; public static List ResetCamPlayerList; public static List winnerList; public static List clientIdList; @@ -172,13 +172,8 @@ public override void Load() roleColors = new Dictionary() { // マッドメイト役職 - {CustomRoles.MSchrodingerCat, "#ff1919"}, {CustomRoles.SKMadmate, "#ff1919"}, //特殊クルー役職 - {CustomRoles.CSchrodingerCat, "#ffffff"}, //シュレディンガーの猫の派生 - //ニュートラル役職 - {CustomRoles.EgoSchrodingerCat, "#5600ff"}, - {CustomRoles.JSchrodingerCat, "#00b4eb"}, //HideAndSeek {CustomRoles.HASFox, "#e478ff"}, {CustomRoles.HASTroll, "#00ff00"}, @@ -240,6 +235,7 @@ public enum CustomDeathReason Sniped, Revenge, Execution, + Infected, Disconnected, Fall, etc = -1 @@ -259,6 +255,7 @@ public enum CustomWinner Arsonist = CustomRoles.Arsonist, Egoist = CustomRoles.Egoist, Jackal = CustomRoles.Jackal, + PlagueDoctor = CustomRoles.PlagueDoctor, HASTroll = CustomRoles.HASTroll, } public enum AdditionalWinners