UI/DoorMenu.razor

The door interaction UI for a roleplay gamemode. Draws a label on the door being looked at and a P-key menu to buy, lock, sell or share keys, plus staff controls for title, price, buyable flag and job access. All actions are sent as requests to DoorSystem.

@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mapping;
@namespace HorizonRP
@inherits PanelComponent

@*
	Les portes RP. En regardant une porte : une étiquette (titre, propriétaire,
	prix, verrou). P ouvre le menu de cette porte : acheter, verrouiller, vendre,
	donner la clé ; le staff y règle aussi le titre, le prix et les métiers.
	Rien n'est décidé ici : chaque bouton est une demande à l'hôte (DoorSystem).
*@

<root>
	@if ( !MenuOpen && Looked is not null )
	{
		<div class="door-hint">
			<i>@(Looked.Locked ? "lock" : "door_front")</i>
			<div class="hint-text">
				<div class="hint-title">@Looked.DisplayTitle</div>
				<div class="hint-sub">@StatusLine( Looked )</div>
			</div>
			<div class="hint-key">P</div>
		</div>
	}

	@if ( MenuOpen && Current is not null )
	{
		<div class="door-backdrop">
			<div class="door-menu">
				<div class="door-head">
					<div class="door-icon @(Current.Locked ? "locked" : "")"><i>@(Current.Locked ? "lock" : "door_front")</i></div>
					<div class="door-title">
						<div class="door-name">@Current.DisplayTitle</div>
						<div class="door-sub">@StatusLine( Current )</div>
					</div>
					<div class="door-close" onclick=@(() => SetMenu( false ))><i>close</i></div>
				</div>

				<div class="door-body">
					<div class="actions">
						@if ( Current.CanBuy )
						{
							<div class="action primary @(Me.Wallet < Current.Price ? "disabled" : "")" onclick=@(() => Doors.RequestBuy( Key ))>
								<i>key</i> @($"Acheter · {(Format.Money( Current.Price ))}")
							</div>
						}
						@if ( Current.CanUse( Me ) || Me.IsStaff )
						{
							<div class="action" onclick=@(() => Doors.RequestSetLocked( Key, !Current.Locked ))>
								<i>@(Current.Locked ? "lock_open" : "lock")</i> @(Current.Locked ? "Déverrouiller" : "Verrouiller")
							</div>
						}
						@if ( Current.OwnerId == Me.SteamId )
						{
							<div class="action danger" onclick=@(() => Doors.RequestSell( Key ))>
								<i>sell</i> @($"Vendre · +{(Current.Price / 2)} $")
							</div>
						}
					</div>

					@if ( Current.OwnerId == Me.SteamId )
					{
						<div class="section-label">Ont la clé</div>
						<div class="chips">
							@if ( !Current.CoOwners.Any() )
							{
								<div class="muted">Personne d'autre que vous.</div>
							}
							@foreach ( var id in Current.CoOwners )
							{
								<div class="chip on" onclick=@(() => Doors.RequestRemoveCoOwner( Key, id ))>@NameOf( id ) <i>close</i></div>
							}
						</div>

						<div class="section-label">Donner la clé à</div>
						<div class="chips">
							@foreach ( var player in RoleplayPlayer.All.Where( x => x != Me && !Current.CoOwners.Contains( x.SteamId ) ).OrderBy( x => x.RpName ) )
							{
								<div class="chip" onclick=@(() => Doors.RequestAddCoOwner( Key, player.SteamId ))><i>add</i> @player.RpName</div>
							}
						</div>
					}

					@if ( Me.IsStaff )
					{
						<div class="staff">
							<div class="staff-title"><i>shield</i> Réglage staff (enregistré)</div>

							<div class="section-label">Nom de la porte</div>
							<TextEntry class="field" Value=@EditTitle OnTextEdited=@((string v) => { EditTitle = v; })></TextEntry>

							<div class="section-label">Prix</div>
							<div class="chips">
								@foreach ( var price in Prices )
								{
									<div class="chip @(EditPrice == price ? "on" : "")" onclick=@(() => EditPrice = price)>@Format.Money( price )</div>
								}
							</div>

							<div class="section-label">Accès</div>
							<div class="chips">
								<div class="chip @(EditBuyable ? "on" : "")" onclick=@(() => EditBuyable = true)>Achetable</div>
								<div class="chip @(!EditBuyable ? "on" : "")" onclick=@(() => EditBuyable = false)>Non achetable</div>
							</div>

							<div class="section-label">Réservée aux métiers (clé automatique)</div>
							<div class="chips">
								@foreach ( var job in Jobs.All )
								{
									<div class="chip @(EditJobs.Contains( job.Id ) ? "on" : "")" onclick=@(() => ToggleJob( job.Id ))>@job.Name</div>
								}
							</div>

							<div class="actions">
								<div class="action primary" onclick=@(() => SaveStaff())><i>save</i> Enregistrer</div>
								@if ( Current.IsOwned )
								{
									<div class="action danger" onclick=@(() => Doors.StaffRelease( Key ))><i>person_remove</i> @($"Retirer à {(Current.OwnerName)}")</div>
								}
							</div>
						</div>
					}
				</div>
			</div>
		</div>
	}
</root>

@code
{
	static readonly int[] Prices = { 0, 50, 100, 250, 500, 1000, 2500, 5000 };

	public bool MenuOpen { get; private set; }

	/// <summary>La porte ouverte dans le menu.</summary>
	string Key { get; set; }

	/// <summary>La porte regardée (pour l'étiquette), rafraîchie à chaque image.</summary>
	DoorState Looked { get; set; }

	string EditTitle { get; set; }
	int EditPrice { get; set; }
	bool EditBuyable { get; set; }
	HashSet<string> EditJobs { get; set; } = new();

	DoorSystem Doors => DoorSystem.Current;
	RoleplayPlayer Me => RoleplayPlayer.Local;
	DoorState Current => Key is null ? null : Doors?.StateOf( Key );

	protected override void OnUpdate()
	{
		if ( Me is null || Doors is null )
		{
			if ( MenuOpen ) SetMenu( false );
			Looked = null;
			return;
		}

		// Échap referme ce menu (et ne laisse pas s'ouvrir le menu pause en même temps).
		if ( MenuOpen && Input.EscapePressed )
		{
			Input.EscapePressed = false;
			SetMenu( false );
			return;
		}

		var (door, key) = Doors.LookedAt();
		Looked = key is null ? null : Doors.StateOf( key );

		if ( Input.Pressed( "DoorMenu" ) )
		{
			if ( MenuOpen )
				SetMenu( false );
			else if ( key is not null && Looked is not null )
				OpenFor( key );
			else
				HudNotifications.Push( "Regardez une porte pour ouvrir son menu.", "info" );
		}

		// On ferme si l'on s'éloigne : les demandes seraient refusées de toute façon.
		if ( MenuOpen && Current is null )
			SetMenu( false );
	}

	void OpenFor( string key )
	{
		Key = key;
		var state = Doors.StateOf( key );
		EditTitle = state.Title;
		EditPrice = state.Price;
		EditBuyable = state.Buyable;
		EditJobs = state.JobList.ToHashSet();
		SetMenu( true );
	}

	void SetMenu( bool open )
	{
		MenuOpen = open;
		UiLock.Set( "porte", open );
	}

	protected override void OnDisabled() => UiLock.Set( "porte", false );

	void ToggleJob( string id )
	{
		if ( !EditJobs.Remove( id ) )
			EditJobs.Add( id );
	}

	void SaveStaff()
	{
		Doors.StaffConfigure( Key, EditTitle, EditPrice, EditBuyable, string.Join( ",", EditJobs ) );
	}

	string NameOf( ulong steamId ) => RoleplayPlayer.All.FirstOrDefault( x => x.SteamId == steamId )?.RpName ?? "Joueur parti";

	static string StatusLine( DoorState state )
	{
		if ( state.IsJobDoor )
			return "Réservée : " + string.Join( ", ", state.JobList.Select( x => Jobs.Get( x ).Name ) ) + (state.Locked ? " · verrouillée" : "");
		if ( state.IsOwned )
			return $"Propriété de {state.OwnerName}" + (state.Locked ? " · verrouillée" : "");
		if ( !state.Buyable )
			return "Ne s'achète pas";
		return $"Libre · {Format.Money( state.Price )}";
	}

	protected override int BuildHash()
	{
		var state = Current;
		return HashCode.Combine(
			HashCode.Combine( MenuOpen, Key, Looked?.Key, Looked?.Locked, Looked?.OwnerId ),
			HashCode.Combine( state?.OwnerId, state?.Locked, state?.CoOwners.Count, state?.Title, state?.Price, state?.Buyable, state?.Jobs ),
			HashCode.Combine( EditTitle, EditPrice, EditBuyable, string.Join( ",", EditJobs ) ),
			HashCode.Combine( Me?.Wallet, RoleplayPlayer.All.Count(), Doors?.StatesJson?.Length ) );
	}
}