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()
|
private void UpdateAutoPhases()
|
||||||
{
|
{
|
||||||
if (_view == null || _sending || _inputLock.IsLocked)
|
if (_view == null || _sending || _reconnecting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
@ -372,12 +372,17 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
_autoFinishMulligan = false;
|
_autoFinishMulligan = false;
|
||||||
_autoFinishMulliganAt = 0f;
|
_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
|
var battle = _view.Phase == GamePhase.Battle
|
||||||
&& _view.ActivePlayer == _view.You
|
&& _view.ActivePlayer == _view.You
|
||||||
&& !_view.IsFinished;
|
&& !_view.IsFinished;
|
||||||
|
|
@ -662,6 +667,15 @@ namespace Vanguard.UI
|
||||||
_view = snapshot.View;
|
_view = snapshot.View;
|
||||||
_finished = _view.IsFinished;
|
_finished = _view.IsFinished;
|
||||||
_pendingIntents.Clear();
|
_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();
|
FinishDiscardSelection();
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
PrepareTriggerSelection();
|
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)
|
if (_sending || _connection == null || _cts == null || _view == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_inputLock.IsLocked && intent is not ConcedeIntent)
|
if (!ignoreInputLock && _inputLock.IsLocked && intent is not ConcedeIntent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_discardChoiceActive)
|
if (_discardChoiceActive)
|
||||||
|
|
@ -1028,6 +1045,13 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
if (!result.Accepted)
|
if (!result.Accepted)
|
||||||
{
|
{
|
||||||
|
if (intent is MulliganIntent || intent is FinishMulliganIntent)
|
||||||
|
{
|
||||||
|
_mulliganUsed = false;
|
||||||
|
_autoFinishMulligan = false;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
SetStatus(
|
SetStatus(
|
||||||
"Отклонено (" + result.Code + "): " + (result.Reason ?? "без причины"),
|
"Отклонено (" + result.Code + "): " + (result.Reason ?? "без причины"),
|
||||||
new Color(1f, 0.55f, 0.45f));
|
new Color(1f, 0.55f, 0.45f));
|
||||||
|
|
@ -1036,6 +1060,17 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
else
|
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);
|
SetStatus(result.Duplicate ? "Повтор уже применён." : "Принято.", Muted);
|
||||||
await RequestSnapshotAsync();
|
await RequestSnapshotAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -1977,6 +2012,9 @@ namespace Vanguard.UI
|
||||||
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||||
.Where(IsPhaseIntent)
|
.Where(IsPhaseIntent)
|
||||||
.Where(intent => !(intent is EnterBattleIntent && view.TurnNumber == 1))
|
.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();
|
.ToList();
|
||||||
if (intents.Count == 0)
|
if (intents.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -2274,6 +2312,8 @@ namespace Vanguard.UI
|
||||||
_mulliganUsed = true;
|
_mulliganUsed = true;
|
||||||
_autoFinishMulligan = true;
|
_autoFinishMulligan = true;
|
||||||
_autoFinishMulliganAt = 0f;
|
_autoFinishMulliganAt = 0f;
|
||||||
|
if (_view != null)
|
||||||
|
RebuildPhaseControls(_view);
|
||||||
OnIntentClicked(new MulliganIntent { HandIndexes = indexes });
|
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) =>
|
private static RectTransform FindRect(string name) =>
|
||||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
.Where(go => go.scene.IsValid() && go.name == name)
|
.Where(go => go.scene.IsValid() && go.name == name)
|
||||||
|
|
|
||||||
|
|
@ -1001,7 +1001,10 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
строке, чтобы видеть поле. При скрытии панель фаз и UI противника
|
строке, чтобы видеть поле. При скрытии панель фаз и UI противника
|
||||||
сдвигаются вправо.
|
сдвигаются вправо.
|
||||||
- Кнопка муллигана в панели «Фазы» динамическая: без выбора — «Закончить
|
- Кнопка муллигана в панели «Фазы» динамическая: без выбора — «Закончить
|
||||||
муллиган», с выбором — «Заменить X карт».
|
муллиган», с выбором — «Заменить X карт». После обмена муллиган
|
||||||
|
завершается автоматически, кнопка «Закончить муллиган» скрывается и не
|
||||||
|
блокируется feedback/overlay-локом; при реконнекте авто-завершение
|
||||||
|
взводится повторно из снапшота.
|
||||||
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
- Trigger-зона противника зеркальна: над его колодой в сторону центра,
|
||||||
а не за дальним краем.
|
а не за дальним краем.
|
||||||
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
- Карты на столе и в руке масштабируются общим `cardScale = 1.25`
|
||||||
|
|
@ -1024,4 +1027,4 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
(карты, которые сервер должен предложить в фазе Ride). Смотреть:
|
||||||
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
`ssh home "docker compose -f ~/VanguardReMaster/docker-compose.yml logs -f server"`.
|
||||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
- Проверки: сервер **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
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **30/30** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **31/31** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона,
|
||||||
CardListView/Detail, зоны и рука противника, InputLock, контекстное меню,
|
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`.
|
success=True`, `PLAYMAT_SMOKE success=True cards=25 hand=5/5 fieldVisible=True`.
|
||||||
|
|
||||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue