823 lines
31 KiB
Plaintext
823 lines
31 KiB
Plaintext
@page "/cube"
|
|
@page "/cube.html"
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
@inject BrowserBridge Browser
|
|
@inject MatchStore Store
|
|
@inject NavigationManager Navigation
|
|
|
|
<PageTitle>ChessCubing Arena | Phase Cube</PageTitle>
|
|
<PageBody Page="cube" BodyClass="phase-body" />
|
|
|
|
@{
|
|
var summary = Match is null ? null : BuildSummary();
|
|
var blackZone = Match is null ? null : BuildZone(MatchEngine.ColorBlack);
|
|
var whiteZone = Match is null ? null : BuildZone(MatchEngine.ColorWhite);
|
|
var resultView = Match is null ? null : BuildResultView();
|
|
}
|
|
|
|
@if (!_ready)
|
|
{
|
|
<main class="phase-shell cube-shell">
|
|
<div class="panel" style="padding: 1.5rem;">Chargement de la phase cube...</div>
|
|
</main>
|
|
}
|
|
else if (Match is not null && summary is not null && blackZone is not null && whiteZone is not null)
|
|
{
|
|
<main class="phase-shell cube-shell">
|
|
<header class="phase-header">
|
|
<a class="brand-link brand-link-logo" href="application.html" aria-label="Retour a l'application">
|
|
<img class="brand-link-icon" src="logo.png" alt="Icone ChessCubing" />
|
|
<img class="brand-link-mark" src="transparent.png" alt="Logo ChessCubing" />
|
|
</a>
|
|
<div class="phase-title">
|
|
<p class="eyebrow">Phase cube</p>
|
|
<h1 id="cubeTitle">@summary.Title</h1>
|
|
<p id="cubeSubtitle" class="phase-subtitle">@summary.Subtitle</p>
|
|
</div>
|
|
<button class="button ghost small utility-button" id="openCubeHelpButton" type="button" @onclick="OpenHelpModal">
|
|
Arbitre
|
|
</button>
|
|
</header>
|
|
|
|
<section class="status-strip">
|
|
<article class="status-card">
|
|
<span id="cubeBlockLabelText">Block</span>
|
|
<strong id="cubeBlockLabel">@Match.BlockNumber</strong>
|
|
</article>
|
|
<article class="status-card">
|
|
<span>Temps max</span>
|
|
<strong id="cubeElapsed">@MatchEngine.RenderCubeElapsed(Match)</strong>
|
|
</article>
|
|
<article class="status-card wide">
|
|
<span id="cubeCenterLabel">@summary.CenterLabel</span>
|
|
<strong id="cubeCenterValue">@summary.CenterValue</strong>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="faceoff-board">
|
|
<article class="player-zone opponent-zone" id="blackCubeZone">
|
|
<div class="zone-inner mirrored-mobile">
|
|
<div class="zone-head">
|
|
<div>
|
|
<span class="seat-tag dark-seat">Noir</span>
|
|
<h2 id="blackNameCube">@blackZone.Name</h2>
|
|
</div>
|
|
<div class="zone-stats">
|
|
<strong id="blackCubeResult">@blackZone.ResultText</strong>
|
|
<span id="blackCubeCap">@blackZone.MetaText</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="zone-button dark-button @(blackZone.HoldArmed && !blackZone.HoldReady ? "cube-hold-arming" : string.Empty) @(blackZone.HoldReady ? "cube-hold-ready" : string.Empty)" id="blackCubeButton" type="button" disabled="@blackZone.Disabled" style="@blackZone.ProgressStyle" @onpointerdown="() => HandleCubePointerDownAsync(MatchEngine.ColorBlack)" @onpointerup="() => HandleCubePointerUpAsync(MatchEngine.ColorBlack)" @onpointercancel="() => CancelCubeHold(MatchEngine.ColorBlack)" @onpointerleave="() => CancelCubeHold(MatchEngine.ColorBlack)">
|
|
@blackZone.ButtonText
|
|
</button>
|
|
|
|
<p class="zone-foot" id="blackHintCube">@blackZone.Hint</p>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="phase-spine">
|
|
<div class="spine-card">
|
|
<p class="micro-label" id="cubeSpineLabel">@summary.SpineLabel</p>
|
|
<strong id="cubeSpineHeadline">@summary.SpineHeadline</strong>
|
|
<p id="cubeSpineText">@summary.SpineText</p>
|
|
</div>
|
|
|
|
<button class="button primary spine-button" id="primaryCubeButton" type="button" disabled="@summary.PrimaryDisabled" @onclick="HandlePrimaryActionAsync">
|
|
@summary.PrimaryButtonText
|
|
</button>
|
|
</article>
|
|
|
|
<article class="player-zone" id="whiteCubeZone">
|
|
<div class="zone-inner">
|
|
<div class="zone-head">
|
|
<div>
|
|
<span class="seat-tag light-seat">Blanc</span>
|
|
<h2 id="whiteNameCube">@whiteZone.Name</h2>
|
|
</div>
|
|
<div class="zone-stats">
|
|
<strong id="whiteCubeResult">@whiteZone.ResultText</strong>
|
|
<span id="whiteCubeCap">@whiteZone.MetaText</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="zone-button light-button @(whiteZone.HoldArmed && !whiteZone.HoldReady ? "cube-hold-arming" : string.Empty) @(whiteZone.HoldReady ? "cube-hold-ready" : string.Empty)" id="whiteCubeButton" type="button" disabled="@whiteZone.Disabled" style="@whiteZone.ProgressStyle" @onpointerdown="() => HandleCubePointerDownAsync(MatchEngine.ColorWhite)" @onpointerup="() => HandleCubePointerUpAsync(MatchEngine.ColorWhite)" @onpointercancel="() => CancelCubeHold(MatchEngine.ColorWhite)" @onpointerleave="() => CancelCubeHold(MatchEngine.ColorWhite)">
|
|
@whiteZone.ButtonText
|
|
</button>
|
|
|
|
<p class="zone-foot" id="whiteHintCube">@whiteZone.Hint</p>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
</main>
|
|
|
|
<section class="modal @(ShowHelpModal ? string.Empty : "hidden")" id="cubeHelpModal" aria-hidden="@BoolString(!ShowHelpModal)">
|
|
<div class="modal-backdrop" @onclick="CloseHelpModal"></div>
|
|
<div class="modal-card">
|
|
<div class="modal-head">
|
|
<div>
|
|
<p class="eyebrow">Outils arbitre</p>
|
|
<h2>Phase cube</h2>
|
|
</div>
|
|
<button class="button ghost small" id="closeCubeHelpButton" type="button" @onclick="CloseHelpModal">
|
|
Fermer
|
|
</button>
|
|
</div>
|
|
|
|
<p class="section-copy" id="cubeHelpStatus">@summary.HelpStatus</p>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button secondary" id="replayCubeButton" type="button" @onclick="ReplayCubeAsync">
|
|
Rejouer la phase cube
|
|
</button>
|
|
<button class="button ghost danger" id="cubeResetButton" type="button" @onclick="ResetMatchAsync">
|
|
Reinitialiser le match
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="modal @(ShowResultModal && resultView is not null ? string.Empty : "hidden")" id="cubeResultModal" aria-hidden="@BoolString(!(ShowResultModal && resultView is not null))">
|
|
<div class="modal-backdrop" @onclick="CloseResultModal"></div>
|
|
@if (resultView is not null)
|
|
{
|
|
<div class="modal-card result-modal-card">
|
|
<div class="modal-head">
|
|
<div>
|
|
<p class="eyebrow">Fin de phase cube</p>
|
|
<h2 id="cubeResultModalTitle">@resultView.Title</h2>
|
|
</div>
|
|
<button class="button ghost small" id="closeCubeResultButton" type="button" @onclick="CloseResultModal">
|
|
Fermer
|
|
</button>
|
|
</div>
|
|
|
|
<p class="section-copy" id="cubeResultSummary">@resultView.Summary</p>
|
|
|
|
<div class="cube-result-overview">
|
|
<article class="result-pill-card">
|
|
<span>Vainqueur cube</span>
|
|
<strong id="cubeResultWinner">@resultView.Winner</strong>
|
|
</article>
|
|
<article class="result-pill-card">
|
|
<span>Suite</span>
|
|
<strong id="cubeResultOutcome">@resultView.Outcome</strong>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="cube-result-player-grid">
|
|
<article class="cube-result-player-card">
|
|
<span class="seat-tag light-seat">Blanc</span>
|
|
<strong id="cubeResultWhiteName">@resultView.WhiteName</strong>
|
|
<span id="cubeResultWhiteTime">@resultView.WhiteTime</span>
|
|
<span id="cubeResultWhiteDetail">@resultView.WhiteDetail</span>
|
|
<span id="cubeResultWhiteClock">@resultView.WhiteClock</span>
|
|
</article>
|
|
<article class="cube-result-player-card">
|
|
<span class="seat-tag dark-seat">Noir</span>
|
|
<strong id="cubeResultBlackName">@resultView.BlackName</strong>
|
|
<span id="cubeResultBlackTime">@resultView.BlackTime</span>
|
|
<span id="cubeResultBlackDetail">@resultView.BlackDetail</span>
|
|
<span id="cubeResultBlackClock">@resultView.BlackClock</span>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button primary" id="cubeResultActionButton" type="button" @onclick="ApplyResultAsync">
|
|
@resultView.ActionLabel
|
|
</button>
|
|
<button class="button ghost" id="cubeResultDismissButton" type="button" @onclick="CloseResultModal">
|
|
Revenir a la phase cube
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
@code {
|
|
private readonly Dictionary<string, CubeHoldState> _holdStates = new(StringComparer.Ordinal)
|
|
{
|
|
[MatchEngine.ColorWhite] = new(),
|
|
[MatchEngine.ColorBlack] = new(),
|
|
};
|
|
|
|
private CancellationTokenSource? _tickerCancellation;
|
|
private bool _ready;
|
|
private bool ShowHelpModal;
|
|
private bool ShowResultModal;
|
|
private string? _resultModalKey;
|
|
|
|
private MatchState? Match => Store.Current;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Store.EnsureLoadedAsync();
|
|
_ready = true;
|
|
|
|
if (Match is null)
|
|
{
|
|
Navigation.NavigateTo("/application.html", replace: true);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(Match.Result) && Match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
Navigation.NavigateTo("/chrono.html", replace: true);
|
|
return;
|
|
}
|
|
|
|
_tickerCancellation = new CancellationTokenSource();
|
|
_ = RunTickerAsync(_tickerCancellation.Token);
|
|
await TryPlayPendingAlertAsync();
|
|
UpdateResultModalState();
|
|
StateHasChanged();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_tickerCancellation is not null)
|
|
{
|
|
_tickerCancellation.Cancel();
|
|
_tickerCancellation.Dispose();
|
|
}
|
|
|
|
await Store.FlushIfDueAsync(0);
|
|
}
|
|
|
|
private async Task RunTickerAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100));
|
|
while (await timer.WaitForNextTickAsync(cancellationToken))
|
|
{
|
|
var match = Match;
|
|
if (match is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(match.Result) && match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
await Store.SaveAsync();
|
|
await InvokeAsync(() => Navigation.NavigateTo("/chrono.html", replace: true));
|
|
return;
|
|
}
|
|
|
|
UpdateResultModalState();
|
|
await Store.FlushIfDueAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private void OpenHelpModal()
|
|
=> ShowHelpModal = true;
|
|
|
|
private void CloseHelpModal()
|
|
=> ShowHelpModal = false;
|
|
|
|
private void CloseResultModal()
|
|
=> ShowResultModal = false;
|
|
|
|
private async Task HandlePrimaryActionAsync()
|
|
{
|
|
if (Match is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(Match.Result))
|
|
{
|
|
Navigation.NavigateTo("/application.html");
|
|
return;
|
|
}
|
|
|
|
if (Match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
Navigation.NavigateTo("/chrono.html", replace: true);
|
|
return;
|
|
}
|
|
|
|
if (BuildResultView() is not null)
|
|
{
|
|
ShowResultModal = true;
|
|
}
|
|
}
|
|
|
|
private async Task HandleCubePointerDownAsync(string color)
|
|
{
|
|
var match = Match;
|
|
if (match is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Browser.PrimeAudioAsync();
|
|
await TryPlayPendingAlertAsync();
|
|
|
|
if (!string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black) is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
|
if (playerState.Running)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_holdStates[color].Armed = true;
|
|
_holdStates[color].StartedAt = MatchEngine.NowUnixMs();
|
|
}
|
|
|
|
private async Task HandleCubePointerUpAsync(string color)
|
|
{
|
|
var match = Match;
|
|
if (match is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Browser.PrimeAudioAsync();
|
|
await TryPlayPendingAlertAsync();
|
|
|
|
if (!string.IsNullOrEmpty(match.Result) || match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
CancelCubeHold(color);
|
|
return;
|
|
}
|
|
|
|
if ((color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black) is not null)
|
|
{
|
|
CancelCubeHold(color);
|
|
return;
|
|
}
|
|
|
|
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
|
if (playerState.Running)
|
|
{
|
|
MatchEngine.CaptureCubeTime(match, color);
|
|
CancelCubeHold(color);
|
|
await PersistCubeAsync();
|
|
return;
|
|
}
|
|
|
|
var ready = IsHoldReady(color);
|
|
CancelCubeHold(color);
|
|
if (!ready)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MatchEngine.StartCubeTimer(match, color);
|
|
await PersistCubeAsync();
|
|
}
|
|
|
|
private void CancelCubeHold(string color)
|
|
{
|
|
_holdStates[color].Armed = false;
|
|
_holdStates[color].StartedAt = 0;
|
|
}
|
|
|
|
private async Task ApplyResultAsync()
|
|
{
|
|
var match = Match;
|
|
var resultView = BuildResultView();
|
|
if (match is null || resultView is null)
|
|
{
|
|
ShowResultModal = false;
|
|
return;
|
|
}
|
|
|
|
if (resultView.ReplayRequired)
|
|
{
|
|
ShowResultModal = false;
|
|
_resultModalKey = null;
|
|
MatchEngine.ReplayCubePhase(match);
|
|
await PersistCubeAsync();
|
|
await TryPlayPendingAlertAsync();
|
|
return;
|
|
}
|
|
|
|
ShowResultModal = false;
|
|
MatchEngine.ApplyCubeOutcome(match);
|
|
Store.MarkDirty();
|
|
await Store.SaveAsync();
|
|
Navigation.NavigateTo("/chrono.html");
|
|
}
|
|
|
|
private async Task ReplayCubeAsync()
|
|
{
|
|
var match = Match;
|
|
if (match is null || match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShowResultModal = false;
|
|
_resultModalKey = null;
|
|
MatchEngine.ReplayCubePhase(match);
|
|
await PersistCubeAsync();
|
|
await TryPlayPendingAlertAsync();
|
|
}
|
|
|
|
private async Task ResetMatchAsync()
|
|
{
|
|
await Store.ClearAsync();
|
|
Navigation.NavigateTo("/application.html", replace: true);
|
|
}
|
|
|
|
private async Task PersistCubeAsync()
|
|
{
|
|
Store.MarkDirty();
|
|
await Store.SaveAsync();
|
|
UpdateResultModalState();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task TryPlayPendingAlertAsync()
|
|
{
|
|
var match = Match;
|
|
if (match is null || !match.Cube.PhaseAlertPending)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!await Browser.PlayCubePhaseAlertAsync())
|
|
{
|
|
return;
|
|
}
|
|
|
|
match.Cube.PhaseAlertPending = false;
|
|
Store.MarkDirty();
|
|
await Store.SaveAsync();
|
|
}
|
|
|
|
private CubeSummaryView BuildSummary()
|
|
{
|
|
var match = Match!;
|
|
var blockHeading = MatchEngine.FormatBlockHeading(match, match.BlockNumber);
|
|
var subtitle = $"{blockHeading} - {MatchEngine.Modes[match.Config.Mode].Label} - {MatchEngine.RenderModeContext(match)}";
|
|
|
|
if (!string.IsNullOrEmpty(match.Result))
|
|
{
|
|
return new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Resultat",
|
|
MatchEngine.ResultText(match),
|
|
"Termine",
|
|
MatchEngine.ResultText(match),
|
|
"Retournez a la configuration pour relancer une rencontre.",
|
|
"Retour a l'accueil",
|
|
"Le match est termine.",
|
|
false);
|
|
}
|
|
|
|
if (match.Cube.Times.White is not null && match.Cube.Times.Black is not null && match.Config.Mode == MatchEngine.ModeTwice && match.Cube.Times.White == match.Cube.Times.Black)
|
|
{
|
|
return new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Decision",
|
|
"Egalite parfaite",
|
|
"Reglement",
|
|
"Rejouer la phase cube",
|
|
"Le mode Twice impose de relancer immediatement la phase cube en cas d'egalite parfaite.",
|
|
"Voir le resume du cube",
|
|
"Le mode Twice impose de relancer immediatement la phase cube en cas d'egalite parfaite.",
|
|
false);
|
|
}
|
|
|
|
if (match.Cube.Times.White is not null && match.Cube.Times.Black is not null)
|
|
{
|
|
var preview = match.Config.Mode == MatchEngine.ModeTime
|
|
? MatchEngine.GetTimeAdjustmentPreview(match, match.Cube.Times.White.Value, match.Cube.Times.Black.Value)
|
|
: null;
|
|
|
|
return preview is null
|
|
? new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Decision",
|
|
"Phase cube complete",
|
|
"Suite",
|
|
"Ouvrir la page chrono",
|
|
"Appliquer le resultat du cube pour preparer le Block suivant.",
|
|
"Voir le resume du cube",
|
|
"Appliquer le resultat du cube pour preparer le Block suivant.",
|
|
false)
|
|
: new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Vainqueur cube",
|
|
preview.Winner is not null ? MatchEngine.PlayerName(match, preview.Winner) : "Egalite",
|
|
"Impact chrono",
|
|
preview.BlockType == "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer",
|
|
$"Blanc {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)} -> {MatchEngine.FormatSignedClock(preview.WhiteAfter)}. Noir {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)} -> {MatchEngine.FormatSignedClock(preview.BlackAfter)}.",
|
|
"Voir le resume du cube",
|
|
$"Blanc {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)} -> {MatchEngine.FormatSignedClock(preview.WhiteAfter)}. Noir {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)} -> {MatchEngine.FormatSignedClock(preview.BlackAfter)}.",
|
|
false);
|
|
}
|
|
|
|
if (match.Cube.Running)
|
|
{
|
|
return new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Etat",
|
|
"Chronos lances",
|
|
"Arrets",
|
|
"Chaque joueur se chronometre",
|
|
"Chaque joueur demarre en relachant sa zone, puis retape sa zone une fois le cube termine.",
|
|
"Attendre les deux temps",
|
|
"Chaque joueur demarre en relachant sa zone, puis retape sa zone une fois le cube termine.",
|
|
true);
|
|
}
|
|
|
|
if (match.Cube.Times.White is not null || match.Cube.Times.Black is not null)
|
|
{
|
|
return new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Etat",
|
|
"Un temps saisi",
|
|
"Suite",
|
|
"Attendre l'autre joueur",
|
|
"Le deuxieme joueur peut encore maintenir puis relacher sa zone pour demarrer son propre chrono.",
|
|
"Attendre le deuxieme temps",
|
|
"Le deuxieme joueur peut encore maintenir puis relacher sa zone pour demarrer son propre chrono.",
|
|
true);
|
|
}
|
|
|
|
return new CubeSummaryView(
|
|
match.Cube.Number is not null ? $"Cube n{match.Cube.Number}" : "Phase cube",
|
|
subtitle,
|
|
"Etat",
|
|
"Pret",
|
|
"Depart libre",
|
|
"Chaque joueur lance son chrono",
|
|
"Au debut de sa resolution, chaque joueur maintient sa grande zone puis la relache pour demarrer son propre chrono.",
|
|
"En attente des joueurs",
|
|
"Au debut de sa resolution, chaque joueur maintient sa grande zone puis la relache pour demarrer son propre chrono.",
|
|
true);
|
|
}
|
|
|
|
private CubeZoneView BuildZone(string color)
|
|
{
|
|
var match = Match!;
|
|
var playerState = color == MatchEngine.ColorWhite ? match.Cube.PlayerState.White : match.Cube.PlayerState.Black;
|
|
var time = color == MatchEngine.ColorWhite ? match.Cube.Times.White : match.Cube.Times.Black;
|
|
var hold = _holdStates[color];
|
|
var holdReady = IsHoldReady(color);
|
|
var holdArmed = hold.Armed && !holdReady && !playerState.Running && time is null && match.Phase == MatchEngine.PhaseCube && string.IsNullOrEmpty(match.Result);
|
|
var holdProgress = hold.Armed && time is null && !playerState.Running
|
|
? Math.Min((MatchEngine.NowUnixMs() - hold.StartedAt) / (double)MatchEngine.CubeStartHoldMs, 1d)
|
|
: 0d;
|
|
|
|
if (!string.IsNullOrEmpty(match.Result))
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
MatchEngine.ResultText(match),
|
|
"Le match est termine.",
|
|
true,
|
|
false,
|
|
false,
|
|
"--cube-hold-progress: 0");
|
|
}
|
|
|
|
if (match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"Retour chrono",
|
|
"La page cube est terminee.",
|
|
true,
|
|
false,
|
|
false,
|
|
"--cube-hold-progress: 0");
|
|
}
|
|
|
|
if (time is not null)
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"Temps enregistre",
|
|
"Ce joueur a deja termine son cube.",
|
|
true,
|
|
false,
|
|
false,
|
|
"--cube-hold-progress: 1");
|
|
}
|
|
|
|
if (playerState.Running)
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"J'ai fini le cube",
|
|
"Tape au moment exact ou le cube est resolu.",
|
|
false,
|
|
false,
|
|
false,
|
|
"--cube-hold-progress: 1");
|
|
}
|
|
|
|
if (holdReady)
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"Relachez pour demarrer",
|
|
"Le chrono partira des que vous levez le doigt.",
|
|
false,
|
|
true,
|
|
true,
|
|
"--cube-hold-progress: 1");
|
|
}
|
|
|
|
if (holdArmed)
|
|
{
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"Maintenez 2 s...",
|
|
"Gardez le doigt pose 2 secondes, jusqu'a la fin de la barre.",
|
|
false,
|
|
true,
|
|
false,
|
|
$"--cube-hold-progress: {holdProgress.ToString("0.###", CultureInfo.InvariantCulture)}");
|
|
}
|
|
|
|
return new CubeZoneView(
|
|
MatchEngine.PlayerName(match, color),
|
|
MatchEngine.FormatCubePlayerTime(match, color),
|
|
MatchEngine.RenderCubeMeta(match, color),
|
|
"Maintenir 2 s pour demarrer",
|
|
"Maintenez la grande zone 2 secondes, puis relachez pour lancer votre chrono.",
|
|
false,
|
|
false,
|
|
false,
|
|
"--cube-hold-progress: 0");
|
|
}
|
|
|
|
private CubeResultView? BuildResultView()
|
|
{
|
|
var match = Match;
|
|
if (match is null || match.Phase != MatchEngine.PhaseCube)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var white = match.Cube.Times.White;
|
|
var black = match.Cube.Times.Black;
|
|
if (white is null || black is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var whiteName = MatchEngine.PlayerName(match, MatchEngine.ColorWhite);
|
|
var blackName = MatchEngine.PlayerName(match, MatchEngine.ColorBlack);
|
|
|
|
if (match.Config.Mode == MatchEngine.ModeTime)
|
|
{
|
|
var preview = MatchEngine.GetTimeAdjustmentPreview(match, white.Value, black.Value);
|
|
if (preview is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new CubeResultView(
|
|
$"time:{match.BlockNumber}:{match.Cube.Round}:{white}:{black}",
|
|
"Resume du cube",
|
|
preview.Winner is not null ? MatchEngine.PlayerName(match, preview.Winner) : "Egalite",
|
|
preview.BlockType == "minus" ? "Bloc - a appliquer" : "Bloc + a appliquer",
|
|
"Validez ce resume pour appliquer les impacts chrono puis revenir a la page chrono.",
|
|
"Appliquer et ouvrir la page chrono",
|
|
whiteName,
|
|
blackName,
|
|
$"Temps cube {MatchEngine.FormatStopwatch(white.Value)}",
|
|
$"Temps cube {MatchEngine.FormatStopwatch(black.Value)}",
|
|
$"Impact chrono {MatchEngine.FormatSignedStopwatch(preview.WhiteDelta)}",
|
|
$"Impact chrono {MatchEngine.FormatSignedStopwatch(preview.BlackDelta)}",
|
|
$"Chrono apres {MatchEngine.FormatSignedClock(preview.WhiteAfter)}",
|
|
$"Chrono apres {MatchEngine.FormatSignedClock(preview.BlackAfter)}",
|
|
false);
|
|
}
|
|
|
|
var winner = white < black ? MatchEngine.ColorWhite : black < white ? MatchEngine.ColorBlack : null;
|
|
var tie = winner is null;
|
|
return new CubeResultView(
|
|
$"twice:{match.BlockNumber}:{match.Cube.Round}:{white}:{black}",
|
|
tie ? "Egalite parfaite" : "Resume du cube",
|
|
winner is not null ? MatchEngine.PlayerName(match, winner) : "Egalite parfaite",
|
|
tie ? "Rejouer la phase cube" : $"{MatchEngine.PlayerName(match, winner!)} ouvrira le Block suivant",
|
|
tie ? "Le reglement Twice impose de rejouer immediatement la phase cube." : "Validez ce resultat pour preparer le Block suivant.",
|
|
tie ? "Rejouer la phase cube" : "Appliquer et ouvrir la page chrono",
|
|
whiteName,
|
|
blackName,
|
|
$"Temps cube {MatchEngine.FormatStopwatch(white.Value)}",
|
|
$"Temps cube {MatchEngine.FormatStopwatch(black.Value)}",
|
|
winner == MatchEngine.ColorWhite ? "Gagne la phase cube" : tie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
|
|
winner == MatchEngine.ColorBlack ? "Gagne la phase cube" : tie ? "Egalite parfaite" : "Ne gagne pas la phase cube",
|
|
"Aucun impact chrono en mode Twice",
|
|
"Aucun impact chrono en mode Twice",
|
|
tie);
|
|
}
|
|
|
|
private void UpdateResultModalState()
|
|
{
|
|
var resultView = BuildResultView();
|
|
if (resultView is null)
|
|
{
|
|
ShowResultModal = false;
|
|
_resultModalKey = null;
|
|
return;
|
|
}
|
|
|
|
if (_resultModalKey != resultView.Key)
|
|
{
|
|
_resultModalKey = resultView.Key;
|
|
ShowResultModal = true;
|
|
}
|
|
}
|
|
|
|
private bool IsHoldReady(string color)
|
|
=> _holdStates[color].Armed && MatchEngine.NowUnixMs() - _holdStates[color].StartedAt >= MatchEngine.CubeStartHoldMs;
|
|
|
|
private static string BoolString(bool value)
|
|
=> value ? "true" : "false";
|
|
|
|
private sealed class CubeHoldState
|
|
{
|
|
public bool Armed { get; set; }
|
|
public long StartedAt { get; set; }
|
|
}
|
|
|
|
private sealed record CubeSummaryView(
|
|
string Title,
|
|
string Subtitle,
|
|
string CenterLabel,
|
|
string CenterValue,
|
|
string SpineLabel,
|
|
string SpineHeadline,
|
|
string SpineText,
|
|
string PrimaryButtonText,
|
|
string HelpStatus,
|
|
bool PrimaryDisabled);
|
|
|
|
private sealed record CubeZoneView(
|
|
string Name,
|
|
string ResultText,
|
|
string MetaText,
|
|
string ButtonText,
|
|
string Hint,
|
|
bool Disabled,
|
|
bool HoldArmed,
|
|
bool HoldReady,
|
|
string ProgressStyle);
|
|
|
|
private sealed record CubeResultView(
|
|
string Key,
|
|
string Title,
|
|
string Winner,
|
|
string Outcome,
|
|
string Summary,
|
|
string ActionLabel,
|
|
string WhiteName,
|
|
string BlackName,
|
|
string WhiteTime,
|
|
string BlackTime,
|
|
string WhiteDetail,
|
|
string BlackDetail,
|
|
string WhiteClock,
|
|
string BlackClock,
|
|
bool ReplayRequired);
|
|
}
|