Auto-finish mulligan after exchanging cards
CI / test (push) Successful in 2m23s
Details
CI / test (push) Successful in 2m23s
Details
This commit is contained in:
parent
8388d877b3
commit
d6784b7ca9
|
|
@ -347,7 +347,7 @@ namespace Vanguard.UI
|
|||
|
||||
private void UpdateAutoPhases()
|
||||
{
|
||||
if (_view == null || _sending || _inputLock.IsLocked)
|
||||
if (_view == null || _sending || _reconnecting)
|
||||
return;
|
||||
|
||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||
|
|
@ -372,12 +372,17 @@ namespace Vanguard.UI
|
|||
{
|
||||
_autoFinishMulligan = false;
|
||||
_autoFinishMulliganAt = 0f;
|
||||
OnIntentClicked(finish);
|
||||
// After an exchange the mulligan must finish by itself;
|
||||
// feedback/overlay locks must not block that transition.
|
||||
OnIntentClicked(finish, ignoreInputLock: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_inputLock.IsLocked)
|
||||
return;
|
||||
|
||||
var battle = _view.Phase == GamePhase.Battle
|
||||
&& _view.ActivePlayer == _view.You
|
||||
&& !_view.IsFinished;
|
||||
|
|
@ -662,6 +667,15 @@ namespace Vanguard.UI
|
|||
_view = snapshot.View;
|
||||
_finished = _view.IsFinished;
|
||||
_pendingIntents.Clear();
|
||||
if (_mulliganUsed
|
||||
&& _view.Phase == GamePhase.Mulligan
|
||||
&& _view.ActivePlayer == _view.You)
|
||||
{
|
||||
// Recovery path: if the automatic finish was interrupted by a
|
||||
// reconnect, re-arm it from the authoritative snapshot.
|
||||
_autoFinishMulligan = true;
|
||||
_autoFinishMulliganAt = 0f;
|
||||
}
|
||||
FinishDiscardSelection();
|
||||
_triggerPowerSlot = null;
|
||||
PrepareTriggerSelection();
|
||||
|
|
@ -988,12 +1002,15 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private async void OnIntentClicked(IntentPayload intent)
|
||||
private void OnIntentClicked(IntentPayload intent) =>
|
||||
OnIntentClicked(intent, ignoreInputLock: false);
|
||||
|
||||
private async void OnIntentClicked(IntentPayload intent, bool ignoreInputLock)
|
||||
{
|
||||
if (_sending || _connection == null || _cts == null || _view == null)
|
||||
return;
|
||||
|
||||
if (_inputLock.IsLocked && intent is not ConcedeIntent)
|
||||
if (!ignoreInputLock && _inputLock.IsLocked && intent is not ConcedeIntent)
|
||||
return;
|
||||
|
||||
if (_discardChoiceActive)
|
||||
|
|
@ -1028,6 +1045,13 @@ namespace Vanguard.UI
|
|||
|
||||
if (!result.Accepted)
|
||||
{
|
||||
if (intent is MulliganIntent || intent is FinishMulliganIntent)
|
||||
{
|
||||
_mulliganUsed = false;
|
||||
_autoFinishMulligan = false;
|
||||
_autoFinishMulliganAt = 0f;
|
||||
}
|
||||
|
||||
SetStatus(
|
||||
"Отклонено (" + result.Code + "): " + (result.Reason ?? "без причины"),
|
||||
new Color(1f, 0.55f, 0.45f));
|
||||
|
|
@ -1036,6 +1060,17 @@ namespace Vanguard.UI
|
|||
}
|
||||
else
|
||||
{
|
||||
if (intent is MulliganIntent)
|
||||
{
|
||||
// The exchange itself completes the mulligan; finish it
|
||||
// automatically even if a feedback/overlay lock is active.
|
||||
_mulliganUsed = true;
|
||||
_autoFinishMulligan = true;
|
||||
_autoFinishMulliganAt = Time.unscaledTime + 0.3f;
|
||||
if (_view != null)
|
||||
RebuildPhaseControls(_view);
|
||||
}
|
||||
|
||||
SetStatus(result.Duplicate ? "Повтор уже применён." : "Принято.", Muted);
|
||||
await RequestSnapshotAsync();
|
||||
}
|
||||
|
|
@ -1977,6 +2012,9 @@ namespace Vanguard.UI
|
|||
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||
.Where(IsPhaseIntent)
|
||||
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
||||
// The exchange is followed by an automatic finish; showing an
|
||||
// inactive finish button after the exchange is misleading.
|
||||
.Where(intent => !(intent is FinishMulliganIntent && _mulliganUsed))
|
||||
.ToList();
|
||||
if (intents.Count == 0)
|
||||
{
|
||||
|
|
@ -2274,6 +2312,8 @@ namespace Vanguard.UI
|
|||
_mulliganUsed = true;
|
||||
_autoFinishMulligan = true;
|
||||
_autoFinishMulliganAt = 0f;
|
||||
if (_view != null)
|
||||
RebuildPhaseControls(_view);
|
||||
OnIntentClicked(new MulliganIntent { HandIndexes = indexes });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,68 @@ namespace Vanguard.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Mulligan_Finish_Button_Is_Hidden_After_Exchange()
|
||||
{
|
||||
var config = new AppConfig();
|
||||
config.Load();
|
||||
var tokens = new TokenStore();
|
||||
var api = new ApiClient(config, tokens);
|
||||
var player = new PlayerState();
|
||||
var auth = new AuthService(api, tokens, player);
|
||||
var meta = new MetaService(api, player);
|
||||
var canvas = UiKit.CreateCanvas("Mulligan Test Canvas");
|
||||
|
||||
try
|
||||
{
|
||||
var battle = new BattleView(canvas.transform, meta, () => { });
|
||||
var match = new MatchView
|
||||
{
|
||||
You = 0,
|
||||
ActivePlayer = 0,
|
||||
Phase = GamePhase.Mulligan,
|
||||
LegalIntents = new List<IntentPayload>
|
||||
{
|
||||
new FinishMulliganIntent(),
|
||||
new ConcedeIntent(),
|
||||
},
|
||||
};
|
||||
|
||||
var viewField = typeof(BattleView)
|
||||
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var usedField = typeof(BattleView)
|
||||
.GetField("_mulliganUsed", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var rebuild = typeof(BattleView)
|
||||
.GetMethod("RebuildPhaseControls", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
viewField.SetValue(battle, match);
|
||||
|
||||
usedField.SetValue(battle, true);
|
||||
rebuild.Invoke(battle, new object[] { match });
|
||||
Assert.IsFalse(
|
||||
FindRect("PhasePanel").gameObject.activeSelf,
|
||||
"Phase panel must disappear after the mulligan exchange");
|
||||
Assert.IsFalse(
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == "Phase_FinishMulliganIntent"),
|
||||
"Finish mulligan button must not remain after the exchange");
|
||||
|
||||
usedField.SetValue(battle, false);
|
||||
rebuild.Invoke(battle, new object[] { match });
|
||||
Assert.IsTrue(
|
||||
FindRect("PhasePanel").gameObject.activeSelf,
|
||||
"Finish mulligan button must be available before the exchange");
|
||||
Assert.IsTrue(
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Any(go => go.name == "Phase_FinishMulliganIntent"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(canvas.gameObject);
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private static RectTransform FindRect(string name) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Where(go => go.scene.IsValid() && go.name == name)
|
||||
|
|
|
|||
|
|
@ -1001,7 +1001,10 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
строке, чтобы видеть поле. При скрытии панель фаз и UI противника
|
||||
сдвигаются вправо.
|
||||
- Кнопка муллигана в панели «Фазы» динамическая: без выбора — «Закончить
|
||||
муллиган», с выбором — «Заменить X карт».
|
||||
муллиган», с выбором — «Заменить X карт». После обмена муллиган
|
||||
завершается автоматически, кнопка «Закончить муллиган» скрывается и не
|
||||
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
||||
взводится повторно из снапшота.
|
||||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||||
а не за дальним краем.
|
||||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||||
|
|
@ -1024,4 +1027,4 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **30/30**.
|
||||
`translations check` OK; клиент EditMode **31/31**.
|
||||
|
|
|
|||
|
|
@ -206,12 +206,13 @@ Standalone Windows:
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||
```
|
||||
|
||||
Клиентские проверки: **30/30** EditMode (сборка всех экранов, импорт арта,
|
||||
Клиентские проверки: **31/31** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк, live-переключение языка, рендер
|
||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
||||
стрелка атаки, prefab-оверлеи, раскладка на 1920×1080), `SCENE_SMOKE
|
||||
стрелка атаки, prefab-оверлеи, раскладка на 1920×1080, авто-завершение
|
||||
муллигана без кнопки после обмена), `SCENE_SMOKE
|
||||
success=True`, `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
|
|
|
|||
Loading…
Reference in New Issue