From 3c7f8645b2523019a313a08869ecbe9680ffed14 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Sat, 24 Oct 2026 02:05:58 +0500 Subject: [PATCH] feat: implement 3D UX movement, trigger camera, arrows, multi counter blast --- .../Vanguard.Shared/Vanguard.Shared.dll | Bin 96256 -> 96768 bytes .../Runtime/Battle/AnimationConfig.cs | 32 ++ .../Vanguard/Runtime/Battle/ArrowConfig.cs | 50 ++++ .../Runtime/Battle/ArrowConfig.cs.meta | 2 + .../Vanguard/Runtime/Battle/CameraDirector.cs | 143 +++++++++ .../Runtime/Battle/CameraDirector.cs.meta | 2 + .../Runtime/Battle/TriggerCheckSequence.cs | 153 ++++++++++ .../Battle/TriggerCheckSequence.cs.meta | 2 + .../Vanguard/Runtime/Board3D/Animation.meta | 8 + .../Board3D/Animation/ArrowDirector.cs | 135 +++++++++ .../Board3D/Animation/ArrowDirector.cs.meta | 2 + .../Board3D/Animation/CardMoveAnimator.cs | 150 ++++++++++ .../Animation/CardMoveAnimator.cs.meta | 2 + .../Board3D/Config/PlaymatLayoutConfig.cs | 2 + .../Runtime/Board3D/Data/BoardTypes.cs | 1 + .../Runtime/Board3D/View/AttackArrowView.cs | 116 ++++++- .../Vanguard/Runtime/Board3D/View/Card3D.cs | 96 +++++- .../Runtime/Board3D/View/GameBoard3D.cs | 283 ++++++++++++++++-- .../Assets/Vanguard/Runtime/UI/BattleView.cs | 156 +++++++++- .../Vanguard/Tests/Editor/GameBoard3DTests.cs | 96 ++++++ docs/3d-ux-backlog.md | 47 ++- docs/HANDOFF.md | 16 + docs/SESSION-STATE.md | 33 +- docs/protocol.md | 2 +- .../src/Vanguard.Rules/Engine/GameCommands.cs | 3 +- .../src/Vanguard.Rules/Engine/GameEngine.cs | 60 +++- .../src/Vanguard.Rules/Replay/CommandCodec.cs | 7 +- .../Projection/IntentMapping.cs | 7 +- .../src/Vanguard.Shared/Protocol/Intents.cs | 6 + .../Vanguard.Rules.Tests/GameEngineTests.cs | 44 ++- 30 files changed, 1569 insertions(+), 87 deletions(-) create mode 100644 client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs create mode 100644 client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs.meta create mode 100644 client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs create mode 100644 client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs.meta create mode 100644 client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs create mode 100644 client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs.meta create mode 100644 client/Assets/Vanguard/Runtime/Board3D/Animation.meta create mode 100644 client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs create mode 100644 client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs.meta create mode 100644 client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs create mode 100644 client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs.meta diff --git a/client/Assets/Plugins/Vanguard.Shared/Vanguard.Shared.dll b/client/Assets/Plugins/Vanguard.Shared/Vanguard.Shared.dll index 1599495fdb59528eed4fee1cbe8564a50455bf21..4efa9d93e556916e5771e83bc13083cf86119f06 100644 GIT binary patch literal 96768 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

D81ehH0^~3jw zm7M66@9wDKxlE*{bjwYPk=Xmt2}~kxFLdw!7)KRpa-H>ZEutj8)yW(z3{mXC>^`LX zCMDmMRC+vR?$PyGOWMJL${nUI9wnU`?>luxm|IP);yNUx|5lT}0b;Y0NUJSaZG<(+ zh}?Tms+JwTd@iS%k}>MMA*HUCBt_nhlG76FKyF3J@upOo2#n>y?WwdLeME4lIJ2;} zd@xlY+jMsy8z_%;jI$T4^VdQQKst?;r%p2WMB@j0Xj*u z;H$cF2eIUsDM*LyJu2oP62pv)j+7eQG2-a)$n)`C8Z-*_Ddx}9aMRSw>>e-mfs24! zch-gxVWYqV%*maS>4}?BGx*NY;gM~L5p~EJ>(9Cl z2`2ac`kIL#vlkh53`%{6QX{NBSr4Elg@JSy_wF)3Zp~&7jcOI#m+QE2W}fuAfXT*DMRrH?UB^u-@7L6?{&KfvW@b(2{WO(g5kQgWBb#0UgA1N~7nK2y{Fw5$MM6 zCw!MvbfJ>dCyAHK3XD=qOiQmR=!A?cyk1fuZN&2A6i7o)N}C+-u?(&;nwe`>c}7#^ zV;<#;hbBZ$9T7_+7kE~oTb;1lsx*uD9$jrydw$i%wddzH>$2JEc~#o4o=?E$^60V^ zi(K zbbFhpro*xZt4LY+%6qK8g_w-E)tnzqd#y+9Oyu(%Nwv2Z15A$rXV9snrNhgeNhRMa zx5L^Duk_+fToqtj_HY#k8wI9URro~Otetqd(+`W>$)Yj@ZVF{YtfY-iXPRlHZ=WNB zdL2)Fg(`!Hn`;1-9$cb>4Q_f%rMJmQ9bbK!-Ng^D%)Cr^dgctu4Zg?A@>cn2P|%C_ z8r?&9239Lj9~Qs{@Bl6tM%tbGEH3C{>ZWT13-^_wJPWAOM~s$APO`k)Ib6d>=V>FJ z!&ZjDy+ffJO`($4jmCYR7tC>Rw5kkQzI@r7zGthb2}!Z0M>$|+jJnSzy8ZlZD%SKEykM~^S=X?CYP=2@g z>srzg@?i*5@jw5dUx1Ks$e8aL2z4qb(b5Bl`y@oi#0jWOiI(qq2vz)UpGk#U(09nW za@F}LT}Pa`)$#!>83TEe6wE^v#_q~4k7p`nUS#2QWcr~AJp+Vj=-JAr5gef=&GK?xO-!IQi+2Z>~u&CU}w%rrjBY-(v!tVE{;PuK8j@; zqwAUFjcMM!P0%+D!8FSKJ4m}Z(+TSysS|c(^dr3lsh9ooNO>XJk|e5R#;BgY+|^xpy}%UgcDQ3V3#g z6$UetYlj{}M8+PW1PtpMwl=(E(OLH$W=lFxM#pKy3M(9Ixjy4t1#NDunN*nC%5PyS z&#+cHdnOIBos3w^dPcN9qZw;5ywx<5mkC$HL^flBQIM0*AXYW)U^ni7`Kzy&+{Ne% zMM-L(&kf0GiB|;u&f}%md48yE{j8B;JeP#i4;$%WL}C4p8tE4xbQ5%~ab{DV=~0ma zqmbnbmvtXQQdd8Dq_z>65i`$}!?<%%IY!^JM+Wx0C5CcM>&K7OspLe@dkU%Zz^BRO zRP=L5`WR8d;9eb1A;}sMkFjJE4=jvaxeu^-W`UZzt}IbtQHq?L#Z02Im@#B21aD`s zT32xwqbhG+sN%n8;ewR^R>gh1=etAE3)ozm)fTbx?nfA*|271@SMG?syALcD`foZ+ zheD&`-a!_l)-47XPt3z%-wgy~^SjFz%w(e)wnfavgJDh z7MeM)wEtxo!DL83zqN0YCSSJgP2E+R^llL=EP0F)SP^FoHSG5CqAwLHtaOtrJ&{qN z!X{@<%BHxYFZFzexAN?5{UDDDKI@w8lT$exsFtK5>)d!gemDCH?NZNQC#2o7e_7AN zTr*@ESa6j`hULjtEOIP4r;=mz<}-C>K}(W^$tGD>79MudC|2;sXw{b;0W-iT^rAbD z;l*uvBO!q|6&$?QeL<)_)Den>qN*!&O{g=pIdpaCT707-s#b+oles!{wTj~Xgm%1} z5LGLozc#cXv<$N7ufwqc+PA2c@L#U1`4@+}LtWsmg{C_Kpk{c*%$g<6>CI9648`hjm{f1`+{hj) zQvA3%->1)|v^g(mqJNL^ep5RKjqg6~+-!W$nDgzLZa22ZY^mRzq>~xar&K)}V0y-nb7n-8B6^>x+!24%xkhg`KEopKWDX5fgmotORR(8{LP{li~ zNE8gt;TF6m94UTdR`H2YQ>6H0MA?yD@VVQHyj!1kH6!}zk>dT0tx$Y`I6kep@xn%X zR`Js|P8X?jW?BswHeRG64yaHo5^Pcp)2A<3FtfSYhCkSg@cEla@p#kRS;bFSjp$C{ znFw@|(rz74A%do;6>%cFBs0%8Q!YLm)>$s~yF%O*MqFX95sEK_n(YO)JsUYd51ln^ z3a%?+wrXx}4BO$b)ZLA?^x&xI&YW#Y_~)&NPU9zjAucrrCMqg87hfr31_YFbj0F&6a&` zb4xhVV@G<(%n(=cO~mph-Si!_BZG9cwj$%wWJ>+#NTywfo7O(Wyw3}_*^X?c%gx;x z#Cs;d+z#e;F9d$Wsy*O;4F1OgOc6|x%nV&kx7v|g{qBMtDfmN_`jM;t022ojC*zeU z?k`c?EfI=d(q(7Ol;*)bk0GUxwYmlS95VGe(zsHs>=-Y&ehRu#zFKNcU$c`+c%LLkLN4C=K)|5gaV0LP*?aIxaXL7P`uJPaGyiBTa=ek zl$VVL#~U;&-Kum9GZA8DAjH!M@iZCa1(T5*({tdTBj4f<5YcJnx{E|zz3)KpJAoJ< z2lKcWW6afj3j9;#bz_`_d(!8A2JUCvaJY_cxC!tR(>GX>PmIy2pXcy=Z=oDBd5EJMT*etMBV5dO8z+QoU0&#&sfgypkz=%LW;Hbc@ z(rAlC;_bvch{uTc67M5^oOQgwj$p<{$;nL-UnIUve3f{H_!{wb;+ujcMGO%cL+KpK zGl_GE^N0(GQQ{)q#9qMldx2?(pT1@G?|*pZPj-bL9$#~R@0sJ@3|Y7v2+9ADe9WX@P>PA{m-hx>qtp>*rhwZljfaNb8hY zvr#P53maS7@%0#*rl=-Mn(6}a5L76z7&Su;L5iDBUVP zIJHC(Ij!4P5JqI!m<}7G`cbWZls-M$rzdQ}hFjdAVO%xDfr@Na$U2N)a~_`t+p_q8 zHeD%?Yq=Rlp4h=+?760GtDC1%|1>vdseZ|Kzs?9wN}G)c4%Q12_XUYFyPZ0_ou1B! zPiKTW?OLbZ)7k6O*~_H&>7@73F{B+sbPQ_8ARTG#NYhc!jzT%!;tO89xD?7-5LrD< zo3t=uBTj1+{p;hjSj+T%hQ2RAm(rRprGxYWLoXn0%;iWAS~H^V3FsEzG%>#!&>hse zg8|*rJ`?Z0fbM3kyE&j+JY(WL6Qpl9^zDHt+I1A|LEU3U_gIiF8oC&W;#M8StpT#2 z$wDB4QooL%)E}hdhK>hmRE1oTQbk?NGxWTGcJXtXE`BaZKWOL&gY=_@el$p*F!YHa z{ji}Q4$==9`k??_I;!c?(ICCZ(2GdBZDUAR)KGwoX)+d!AZj9r2I%7JCKs;sV58i=5zOx9E&0(26IprVh2jBY5Pi>4vEx@EYP zvsaUQgNdQRv0dmESMI5=nL@l4&@N6HdMc3IG9B}>fNp8T#5@wD3x+NP>9C>0ftYvd zn0E%qC7N6ki~z-BK`P2O$Ix?v+UFShoB&;XS<}Ur0|}xHN_AE=!I>tCnZYQYHc>ns zq@OeNb3yt$hW<{Fe%#QH2kEB_{Zx>iH1uSUe#X$x1nCJwPXy^#4gD%2^c62kDrhV?nyd&^ ziE0fkfoTLG+_*#xBS_OJbwIKytpkby1cc?Y#I=0r7bAO=a!=U!!NNB7Oen07pc@3n zh{Vy~w1ifO$Hy7I(<4Smw0p!}hTKOSA`TMM#6npSYi9W3AZ%TQuLI&URQ>jOH{=o{ zc)hH=%^xq!;AJlhdd*^Zt4;W{;PUOg{qEkbW#;vS^@mq2Ux^r|&ndrUg)N{Xco-$G zz0Hy!Q8!E!s&nue!-_XH6h3>X8u@Y;zAKE+=1m@-eB{&zCLfvn^HXhdhq5imzyxHa>-R&uIzrh+) zjTXN9C5S&D&ZtfOhXTvZ3BHu3F2grS56Kt8`t5VO@W#XuY#A)1@HHS1En6}}cqAv^ zY=PR=IsYLwiTJHz!xfXiC2l>s@6NcJ zuc6AF$qFCMWG}p;Zl3gRjKE`lb)|d`AD{WI8_55#UWzUExb=%&vHL-aG3~6ysCrtD z?|||{cJ*6R<@N%6Arzlv#wD*?hH{9&j%fD8=?J*2Zb0*_J+Ku)>RCRySHGT3DF1d1 z2_y>*^#j$|2q9H53xVoaMdhXfd|VY@uU6{Px+U^$6Orh##BrAB+2;AlFugNFE+>YK zv$RU-Re`-M5=1^mSid1xZBCIO_%`@if=DWX4KC6I@VW1^Gyxr?YF`JwH;WH?D|LC@ z3gbB8eyNQ6DkqVATe>VZ&As)ouho$w z`6QjRe;M}cw|}k9mw825#{9QbmUn1npp+5+txBn1FBWb2tFNv1-UWizyGo_)h-p;62;SRdN-`z&Uq4e7TVekLEz{fe zD#~ZCdf(Pr8SkX(>yLNU+i<0|$rY&IWdj>>rQyikK>Z6wBf+hIOhRIS)Su%-(ZBgt zrY$i->W8S@b}Yl8SRwVVu^G_JaOh?Y!=bF>*B`EHZ5)=w#2R_a#LQUgCWcBDTcmy^ z2X;f_+vqexme(KQ<#Sq<@9NPm7CWTA9U9y(Eq1GzAocZwd%ESOM5>y1C%P+wk)A?E zdI1F)Stv-$r68@6f;2S>(kv**)uy07aEu70g2_mgQQ$|AC3Pts8OgT$w|Ml?zg5<@ zostm;$FXDhu6ai1Pjo~QQ)n@cU%~HJK0i<bR8Kyp!zoB3$JUGNoVI`la7=n z+y2J)*Q74_>F<&k?TFp&mE&C^M@=RxPFsC?_@$ZdY_2Dr=BERO^8?wOaqJzp)Ct`X8T3R$2hP{4KK$0l z_4qB4-H^86cSW}I7f80_SG9If?}^Yaf2UiDTi^Qp4*KG8AI`d=8v>^u9P-O2vZ-<- zwoInc`ecWqd{0~c7BN&md}S+U9{#eWQ*ZF3f;thJiM(3KQvq5jXdSE()2m2Wej&#F z*MV)Gr~`bddK>s|q&Cd`v9c>N4}Kwbqi(>lDs&z9rOF=J^eV=37m701{_UZ% zYqkg5Nq6A<4rpEry=%F<_8Np*26h`thxc4ihJ%n~_vHY-BaUB0Lu@+4YUppo4%;1I zy0{Bg;@E+-g~OT&{$gl)Zk|V2FJrhCW`8gI&%SL{uCau7YrBVe8erXx1nTy!;%Ub$ z=EZY8;>n?G-nChY4YTd#L(-iCeRuz94p&Mx+Il%ZdwR>ULsj+)V_riml3&-s$7~pb zT)h$OW0k9a5F2!1nxRe=>Q~a+VR&g7lAc>`adAiVThy1AuC8~z>30yDf@Kr2m;1M_ z_jcKphg`{~VK;nbt$B5(@T+!Gzg=uG4)S^cxgSHx>W7i~{{MgfX=|XU&9U_Q|Fkvm IpSs}x2i5906951J diff --git a/client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs b/client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs index 1112fb2..981d25f 100644 --- a/client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs +++ b/client/Assets/Vanguard/Runtime/Battle/AnimationConfig.cs @@ -17,6 +17,25 @@ namespace Vanguard.Battle public float callMoveDuration = 0.55f; public float retireDuration = 0.8f; + [Header("3D UX: движение")] + public float moveDuration = 0.5f; + public float importantMoveDuration = 0.9f; + public float moveDelayBetweenCards = 0.18f; + public float arcHeight = 0.45f; + + [Header("3D UX: камера")] + public float cameraZoomDuration = 0.35f; + public float triggerRevealHold = 1.2f; + public float cameraReturnDuration = 0.4f; + public float cameraRezoomDuration = 0.3f; + + [Header("3D UX: стрелки")] + public float growDuration = 0.35f; + public float dashScrollSpeed = 1.5f; + public float boostGrowDuration = 0.3f; + public float boostThickness = 0.8f; + public float arrowFadeDuration = 0.2f; + [Header("Бой")] public float attackDeclare = 1.2f; public float guard = 1.9f; @@ -66,6 +85,19 @@ namespace Vanguard.Battle cardFlipDuration = Mathf.Max(0.05f, cardFlipDuration); callMoveDuration = Mathf.Max(0.05f, callMoveDuration); retireDuration = Mathf.Max(0.05f, retireDuration); + moveDuration = Mathf.Max(0.05f, moveDuration); + importantMoveDuration = Mathf.Max(0.05f, importantMoveDuration); + moveDelayBetweenCards = Mathf.Max(0f, moveDelayBetweenCards); + arcHeight = Mathf.Max(0f, arcHeight); + cameraZoomDuration = Mathf.Max(0.05f, cameraZoomDuration); + triggerRevealHold = Mathf.Max(0f, triggerRevealHold); + cameraReturnDuration = Mathf.Max(0.05f, cameraReturnDuration); + cameraRezoomDuration = Mathf.Max(0.05f, cameraRezoomDuration); + growDuration = Mathf.Max(0.05f, growDuration); + dashScrollSpeed = Mathf.Max(0f, dashScrollSpeed); + boostGrowDuration = Mathf.Max(0.05f, boostGrowDuration); + boostThickness = Mathf.Max(0.01f, boostThickness); + arrowFadeDuration = Mathf.Max(0.05f, arrowFadeDuration); attackDeclare = Mathf.Max(0.05f, attackDeclare); guard = Mathf.Max(0.05f, guard); driveCheck = Mathf.Max(0.05f, driveCheck); diff --git a/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs b/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs new file mode 100644 index 0000000..4830dce --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs @@ -0,0 +1,50 @@ +using UnityEngine; + +namespace Vanguard.Battle +{ + public enum ArrowKind + { + Attack, + Boost, + } + + [CreateAssetMenu(menuName = "Vanguard/Arrow Config", fileName = "ArrowConfig")] + public sealed class ArrowConfig : ScriptableObject + { + public Color attackColor = new Color(0.95f, 0.16f, 0.16f); + public Color boostColor = new Color(0.22f, 0.55f, 1f); + + [Min(0.01f)] public float growDuration = 0.35f; + [Min(0.01f)] public float fadeDuration = 0.2f; + [Min(0f)] public float dashScrollSpeed = 1.5f; + [Min(0.01f)] public float thickness = 0.075f; + [Min(0.01f)] public float boostThickness = 0.06f; + [Min(0.01f)] public float headLength = 0.32f; + [Min(0.01f)] public float headWidth = 0.16f; + [Min(0f)] public float yOffset = 0.22f; + [Min(0f)] public float arcHeight = 0.10f; + + private static ArrowConfig _default; + + public static ArrowConfig Default + { + get + { + if (_default == null) + { + _default = CreateInstance(); + _default.name = "ArrowConfig (runtime)"; + _default.hideFlags = HideFlags.DontSave; + } + + return _default; + } + } + + public static ArrowConfig LoadOrDefault() + { + var loaded = Resources.Load("ArrowConfig"); + return loaded != null ? loaded : Default; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs.meta b/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs.meta new file mode 100644 index 0000000..e535c93 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/ArrowConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6a3701087bff10942a9c17bee301638b \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs b/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs new file mode 100644 index 0000000..6038c22 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs @@ -0,0 +1,143 @@ +using UnityEngine; + +namespace Vanguard.Battle +{ + ///

+ /// Единственный владелец кинематографичного движения камеры. + /// Снимок исходного pose берётся один раз на цепочку и + /// восстанавливается идемпотентно. + /// + public sealed class CameraDirector + { + private struct Pose + { + public Vector3 Position; + public Quaternion Rotation; + public float FieldOfView; + } + + private readonly Camera _camera; + private Pose _origin; + private Pose _from; + private Pose _to; + private float _duration; + private float _elapsed; + private bool _moving; + private bool _hasOrigin; + + public CameraDirector(Camera camera) + { + _camera = camera; + } + + public bool IsActive => _hasOrigin && _camera != null; + public bool IsMoving => _moving; + + public void BeginChain() + { + if (_camera == null || _hasOrigin) + return; + + _origin = Capture(); + _hasOrigin = true; + } + + public void ZoomTo( + Vector3 target, + float duration, + float fieldOfView = 34f) + { + if (_camera == null) + return; + + BeginChain(); + var current = Capture(); + var targetPosition = target + + (Vector3.back * 3.4f) + + (Vector3.up * 2.6f); + var targetRotation = Quaternion.LookRotation( + target - targetPosition, + Vector3.up); + AnimateTo( + new Pose + { + Position = targetPosition, + Rotation = targetRotation, + FieldOfView = fieldOfView, + }, + duration); + } + + public void ReturnToOrigin(float duration) + { + if (!_hasOrigin) + return; + + AnimateTo(_origin, duration); + } + + public void Restore() + { + if (_camera == null || !_hasOrigin) + return; + + _moving = false; + Apply(_origin); + _hasOrigin = false; + } + + public void Tick(float deltaTime) + { + if (!_moving || _camera == null) + return; + + _elapsed += deltaTime; + var t = Mathf.Clamp01(_elapsed / Mathf.Max(0.01f, _duration)); + var eased = t * t * (3f - (2f * t)); + Apply(new Pose + { + Position = Vector3.Lerp(_from.Position, _to.Position, eased), + Rotation = Quaternion.Slerp( + _from.Rotation, + _to.Rotation, + eased), + FieldOfView = Mathf.Lerp( + _from.FieldOfView, + _to.FieldOfView, + eased), + }); + + if (t >= 1f) + _moving = false; + } + + private void AnimateTo(Pose target, float duration) + { + if (_camera == null) + return; + + _from = Capture(); + _to = target; + _duration = Mathf.Max(0.01f, duration); + _elapsed = 0f; + _moving = true; + } + + private Pose Capture() => new Pose + { + Position = _camera.transform.position, + Rotation = _camera.transform.rotation, + FieldOfView = _camera.fieldOfView, + }; + + private void Apply(Pose pose) + { + if (_camera == null) + return; + + _camera.transform.position = pose.Position; + _camera.transform.rotation = pose.Rotation; + _camera.fieldOfView = pose.FieldOfView; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs.meta b/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs.meta new file mode 100644 index 0000000..432929c --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/CameraDirector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 131f7624a241a2140b5ed6b9bc2ff0a5 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs b/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs new file mode 100644 index 0000000..5109bd9 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs @@ -0,0 +1,153 @@ +using UnityEngine; +using Vanguard.Battle; + +namespace Vanguard.Battle +{ + /// + /// Оркестратор кинематографичной проверки триггера: + /// zoom → reveal hold → return; повторный zoom в цепочке. + /// + public sealed class TriggerCheckSequence + { + private enum Stage + { + Idle, + Zooming, + Holding, + Returning, + } + + private readonly CameraDirector _camera; + private readonly InputLockService _inputLock; + + private Stage _stage; + private float _deadline; + private bool _chainActive; + private bool _restoreRequested; + private bool _lockHeld; + + public TriggerCheckSequence( + CameraDirector camera, + InputLockService inputLock) + { + _camera = camera; + _inputLock = inputLock; + } + + public bool IsBusy => _stage != Stage.Idle; + + public void ShowTrigger( + AnimationConfig config, + Vector3 worldPoint) + { + if (_camera == null) + return; + + if (!_chainActive) + { + _camera.BeginChain(); + _chainActive = true; + } + + Acquire(); + _restoreRequested = false; + var zoom = config != null + ? config.cameraZoomDuration + : 0.35f; + var hold = config != null + ? config.triggerRevealHold + : 1.2f; + + _camera.ZoomTo(worldPoint, zoom); + _stage = Stage.Zooming; + _deadline = Time.unscaledTime + zoom + hold; + } + + public void ReturnForChoice(AnimationConfig config) + { + if (!_chainActive) + return; + + var duration = config != null + ? config.cameraReturnDuration + : 0.4f; + _camera.ReturnToOrigin(duration); + _stage = Stage.Returning; + _deadline = Time.unscaledTime + duration; + } + + public void EndChain(AnimationConfig config) + { + if (!_chainActive) + { + Release(); + return; + } + + var duration = config != null + ? config.cameraReturnDuration + : 0.4f; + _restoreRequested = true; + _camera.ReturnToOrigin(duration); + _stage = Stage.Returning; + _deadline = Time.unscaledTime + duration; + } + + public void Tick(AnimationConfig config) + { + if (_stage == Stage.Idle) + return; + + if (_stage == Stage.Zooming + && Time.unscaledTime >= _deadline) + { + var duration = config != null + ? config.cameraReturnDuration + : 0.4f; + _camera.ReturnToOrigin(duration); + _stage = Stage.Returning; + _deadline = Time.unscaledTime + duration; + return; + } + + if (_stage == Stage.Returning + && Time.unscaledTime >= _deadline) + { + _stage = Stage.Idle; + Release(); + if (_restoreRequested) + { + _restoreRequested = false; + _chainActive = false; + _camera.Restore(); + } + } + } + + private void Acquire() + { + if (_lockHeld) + return; + + _inputLock?.Acquire("trigger-sequence"); + _lockHeld = true; + } + + private void Release() + { + if (!_lockHeld) + return; + + _inputLock?.Release("trigger-sequence"); + _lockHeld = false; + } + + public void Cancel() + { + _stage = Stage.Idle; + _chainActive = false; + Release(); + _camera?.Restore(); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs.meta b/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs.meta new file mode 100644 index 0000000..0df6c50 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Battle/TriggerCheckSequence.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c2a978244b3921c4898d2446f192e677 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Board3D/Animation.meta b/client/Assets/Vanguard/Runtime/Board3D/Animation.meta new file mode 100644 index 0000000..fac1b61 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Board3D/Animation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8590de763c9f81d40b1acc931836a9ac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs b/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs new file mode 100644 index 0000000..30358f1 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using UnityEngine; +using Vanguard.Battle; + +namespace Vanguard.Board3D +{ + /// + /// Единое управление красной attack-стрелкой и синими boost-стрелками. + /// Геометрия/анимация общая, различие — цвет и толщина. + /// + public sealed class ArrowDirector + { + private readonly Transform _parent; + private AnimationConfig _animation; + private ArrowConfig _config; + private AttackArrowView _attack; + private readonly List _boosts = + new List(); + + public ArrowDirector( + Transform parent, + AnimationConfig animation, + ArrowConfig config) + { + _parent = parent; + _animation = animation; + _config = config != null ? config : ArrowConfig.Default; + } + + public bool AttackVisible => + _attack != null && _attack.IsVisible; + + public void SetConfig(AnimationConfig animation, ArrowConfig config) + { + _animation = animation; + _config = config != null ? config : ArrowConfig.Default; + if (_attack != null) + { + _attack.SetConfig(_animation); + _attack.SetStyle( + _config.attackColor, + _config.thickness, + _animation != null ? _animation.growDuration : _config.growDuration, + _config.fadeDuration, + _config.arcHeight); + } + } + + public void SetAttack(Vector3 from, Vector3 to) + { + EnsureAttack(); + _attack.Show(from, to); + _attack.SetPoints(from, to); + } + + public void HideAttack() + { + _attack?.Hide(); + } + + public void SetBoosts(IReadOnlyList<(Vector3 From, Vector3 To)> boosts) + { + for (var i = 0; i < boosts.Count; i++) + { + var arrow = i < _boosts.Count + ? _boosts[i] + : null; + if (arrow == null) + { + arrow = new AttackArrowView(); + arrow.Build(_parent, _animation); + _boosts.Add(arrow); + } + + arrow.SetStyle( + _config.boostColor, + _config.boostThickness, + _animation != null + ? _animation.boostGrowDuration + : _config.growDuration, + _config.fadeDuration, + _config.arcHeight); + arrow.Show(boosts[i].From, boosts[i].To); + } + + for (var i = boosts.Count; i < _boosts.Count; i++) + _boosts[i].Hide(); + } + + public void HideAll() + { + _attack?.Hide(); + foreach (var arrow in _boosts) + arrow.Hide(); + } + + public void HideImmediate() + { + _attack?.HideImmediate(); + foreach (var arrow in _boosts) + arrow.HideImmediate(); + } + + public void Tick(float deltaTime) + { + _attack?.Tick(deltaTime); + foreach (var arrow in _boosts) + arrow?.Tick(deltaTime); + } + + public void Dispose() + { + _attack?.Dispose(); + _attack = null; + foreach (var arrow in _boosts) + arrow?.Dispose(); + _boosts.Clear(); + } + + private void EnsureAttack() + { + if (_attack != null) + return; + + _attack = new AttackArrowView(); + _attack.Build(_parent, _animation); + _attack.SetStyle( + _config.attackColor, + _config.thickness, + _animation != null ? _animation.growDuration : _config.growDuration, + _config.fadeDuration, + _config.arcHeight); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs.meta b/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs.meta new file mode 100644 index 0000000..5d2664b --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Board3D/Animation/ArrowDirector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 509a80c7422e7dc4cba2cdc07c47177a \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs b/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs new file mode 100644 index 0000000..1ea5082 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using UnityEngine; +using Vanguard.Battle; +using Vanguard.Board3D; + +namespace Vanguard.Board3D +{ + /// + /// Единый планировщик карточных перемещений. Карточки анимируются + /// Card3D, аниматор отвечает за stagger, очередь и InputLock. + /// + public sealed class CardMoveAnimator + { + private sealed class Request + { + public Card3D Card; + public Vector3 FromPosition; + public Quaternion FromRotation; + public Vector3 FromScale; + public float Duration; + public float ArcHeight; + } + + private readonly Queue _queue = new Queue(); + private readonly List _active = new List(); + private readonly InputLockService _inputLock; + private AnimationConfig _config; + private float _delay; + private bool _locked; + + public CardMoveAnimator( + InputLockService inputLock, + AnimationConfig config) + { + _inputLock = inputLock; + _config = config; + } + + public bool IsBusy => _queue.Count > 0 || _active.Count > 0; + + public void SetConfig(AnimationConfig config) + { + _config = config; + } + + public void Enqueue( + Card3D card, + Vector3 fromPosition, + Quaternion fromRotation, + Vector3 fromScale, + float duration, + float arcHeight) + { + if (card == null || card.IsDragging) + return; + + EnsureLock(); + _queue.Enqueue(new Request + { + Card = card, + FromPosition = fromPosition, + FromRotation = fromRotation, + FromScale = fromScale, + Duration = duration, + ArcHeight = arcHeight, + }); + } + + public void Tick(float deltaTime) + { + if (_active.Count > 0) + { + for (var i = _active.Count - 1; i >= 0; i--) + { + if (_active[i] == null || !_active[i].IsMoving) + _active.RemoveAt(i); + } + } + + if (_queue.Count > 0) + { + _delay -= Mathf.Max(0f, deltaTime); + if (_delay <= 0f) + StartNext(); + } + + if (!IsBusy) + ReleaseLock(); + } + + public void CancelAll(bool snapCards = false) + { + _queue.Clear(); + foreach (var card in _active) + { + if (card != null) + card.StopMove(snapCards); + } + + _active.Clear(); + _delay = 0f; + ReleaseLock(); + } + + private void StartNext() + { + if (_queue.Count == 0) + return; + + var request = _queue.Dequeue(); + var card = request.Card; + if (card == null || card.IsDragging) + { + _delay = 0f; + return; + } + + card.StartArcMove( + request.FromPosition, + request.FromRotation, + request.FromScale, + request.Duration, + request.ArcHeight); + + if (card.IsMoving) + _active.Add(card); + _delay = Mathf.Max(0.01f, _config != null + ? _config.moveDelayBetweenCards + : 0.18f); + } + + private void EnsureLock() + { + if (_locked) + return; + + _inputLock?.Acquire("card-move"); + _locked = true; + } + + private void ReleaseLock() + { + if (!_locked) + return; + + _inputLock?.Release("card-move"); + _locked = false; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs.meta b/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs.meta new file mode 100644 index 0000000..cd88354 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/Board3D/Animation/CardMoveAnimator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 783f38d9ba797aa4a93d7d48752d83bb \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs index 1307519..21259f1 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs @@ -82,6 +82,8 @@ namespace Vanguard.Board3D public float damageX = 3.0f; [Tooltip("Расстояние от центра стола до колоды и дропа.")] public float pileX = 2.95f; + [Tooltip("Показывать Drop рубашкой вверх. По Classic — false.")] + public bool dropZoneFaceDown = false; [Header("Отладка")] [Tooltip("Показывать подписи зон на 3D-столе.")] diff --git a/client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs b/client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs index 3ae312e..9f7684a 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Data/BoardTypes.cs @@ -232,6 +232,7 @@ namespace Vanguard.Board3D public bool AbilityDamageTargeting { get; set; } public int AbilityDamagePlayer { get; set; } = -1; public int AbilityDamageIndex { get; set; } = -1; + public HashSet AbilityDamageIndexes { get; } = new HashSet(); public bool MatchFinished { get; set; } } diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/AttackArrowView.cs b/client/Assets/Vanguard/Runtime/Board3D/View/AttackArrowView.cs index ede4cbd..244065e 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/AttackArrowView.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/AttackArrowView.cs @@ -17,12 +17,19 @@ namespace Vanguard.Board3D private LineRenderer _shaft; private LineRenderer _head; private Material _material; + private Texture2D _dashTexture; private AnimationConfig _config; private Vector3 _from; private Vector3 _to; private bool _visible; private float _alpha; private float _widthScale = 1f; + private float _grow = 1f; + private float _growDuration = 0.35f; + private float _fadeDuration = 0.2f; + private float _thickness = ShaftWidth; + private float _arcHeight = 0.1f; + private Color _color = new Color(1f, 0.16f, 0.16f); public bool IsVisible => _visible || _alpha > 0.001f; @@ -37,6 +44,13 @@ namespace Vanguard.Board3D shader = Shader.Find("Unlit/Color"); _material = shader != null ? new Material(shader) : null; + if (_material != null) + { + _dashTexture = CreateDashTexture(); + _material.mainTexture = _dashTexture; + _material.mainTextureScale = new Vector2(2.5f, 1f); + } + _shaft = CreateLine("Shaft", ShaftWidth); _head = CreateLine("Head", ShaftWidth * 0.7f); _head.positionCount = 3; @@ -48,10 +62,29 @@ namespace Vanguard.Board3D _config = config; } + public void SetStyle( + Color color, + float thickness, + float growDuration, + float fadeDuration, + float arcHeight) + { + _color = color; + _thickness = Mathf.Max(0.01f, thickness); + _growDuration = Mathf.Max(0.01f, growDuration); + _fadeDuration = Mathf.Max(0.01f, fadeDuration); + _arcHeight = Mathf.Max(0f, arcHeight); + ApplyColor(); + } + public void Show(Vector3 from, Vector3 to) { SetPoints(from, to); - _visible = true; + if (!_visible) + { + _grow = 0f; + _visible = true; + } if (_root != null) _root.SetActive(true); @@ -82,10 +115,16 @@ namespace Vanguard.Board3D if (_root == null) return; + if (_visible && _grow < 1f) + _grow = Mathf.MoveTowards( + _grow, + 1f, + deltaTime / _growDuration); + var target = _visible ? 1f : 0f; var duration = _visible ? (_config != null ? _config.arrowShowDuration : 0.25f) - : (_config != null ? _config.arrowHideDuration : 0.2f); + : _fadeDuration; _alpha = Mathf.MoveTowards( _alpha, target, @@ -99,6 +138,13 @@ namespace Vanguard.Board3D _root.SetActive(true); _widthScale = 0.55f + (_alpha * 0.45f); + if (_material != null && _config != null) + { + var offset = _material.mainTextureOffset; + offset.x += _config.dashScrollSpeed * deltaTime; + _material.mainTextureOffset = offset; + } + UpdateGeometry(); ApplyColor(); } @@ -114,6 +160,15 @@ namespace Vanguard.Board3D _root = null; } + if (_dashTexture != null) + { + if (Application.isPlaying) + Object.Destroy(_dashTexture); + else + Object.DestroyImmediate(_dashTexture); + _dashTexture = null; + } + if (_material != null) { if (Application.isPlaying) @@ -124,6 +179,31 @@ namespace Vanguard.Board3D } } + private static Texture2D CreateDashTexture() + { + const int width = 16; + const int height = 4; + var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) + { + wrapMode = TextureWrapMode.Repeat, + filterMode = FilterMode.Bilinear, + name = "ArrowDash", + }; + + for (var x = 0; x < width; x++) + { + var on = x < 9; + var color = on + ? new Color(1f, 1f, 1f, 1f) + : new Color(1f, 1f, 1f, 0.18f); + for (var y = 0; y < height; y++) + texture.SetPixel(x, y, color); + } + + texture.Apply(false, true); + return texture; + } + private LineRenderer CreateLine(string name, float width) { var go = new GameObject(name); @@ -135,11 +215,12 @@ namespace Vanguard.Board3D line.numCapVertices = 4; line.numCornerVertices = 4; line.alignment = LineAlignment.View; - line.textureMode = LineTextureMode.Stretch; + line.textureMode = LineTextureMode.Tile; line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; line.receiveShadows = false; if (_material != null) - line.material = _material; + line.sharedMaterial = _material; + return line; } @@ -148,30 +229,41 @@ namespace Vanguard.Board3D if (_shaft == null || _head == null) return; - var direction = _to - _from; + var end = Vector3.Lerp(_from, _to, _grow); + var direction = end - _from; + if (direction.sqrMagnitude < 0.0001f) + direction = _to - _from; if (direction.sqrMagnitude < 0.0001f) direction = Vector3.forward; direction.Normalize(); + var mid = Vector3.Lerp(_from, end, 0.5f) + + (Vector3.up * _arcHeight * Mathf.Sin(_grow * Mathf.PI)); + _shaft.positionCount = 3; _shaft.SetPosition(0, _from); - _shaft.SetPosition(1, _to); - _shaft.widthMultiplier = ShaftWidth * _widthScale; + _shaft.SetPosition(1, mid); + _shaft.SetPosition(2, end); + _shaft.widthMultiplier = _thickness * _widthScale; - var tip = _to; - var back = _to - (direction * HeadLength); + var tip = end; + var back = tip - (direction * 0.32f); var side = Vector3.Cross(Vector3.up, direction).normalized; if (side.sqrMagnitude < 0.0001f) side = Vector3.right; - var half = side * (HeadWidth * _widthScale); + var half = side * (0.16f * _widthScale); _head.SetPosition(0, back + half); _head.SetPosition(1, tip); _head.SetPosition(2, back - half); - _head.widthMultiplier = ShaftWidth * 0.7f * _widthScale; + _head.widthMultiplier = _thickness * 0.7f * _widthScale; } private void ApplyColor() { - var color = new Color(1f, 0.72f, 0.26f, _alpha * 0.92f); + var color = new Color( + _color.r, + _color.g, + _color.b, + _alpha * 0.92f); foreach (var line in new[] { _shaft, _head }) { if (line == null) diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs index daf3ed1..11fa2b8 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs @@ -27,6 +27,14 @@ namespace Vanguard.Board3D private Vector3 _baseScale = Vector3.one; private bool _immediate; + private bool _arcMoving; + private Vector3 _arcFromPosition; + private Quaternion _arcFromRotation; + private Vector3 _arcFromScale; + private float _arcDuration; + private float _arcElapsed; + private float _arcHeight; + private bool _dragging; private Vector3 _dragOriginPosition; private Quaternion _dragOriginRotation; @@ -42,9 +50,11 @@ namespace Vanguard.Board3D public Quaternion TargetRotation => _targetRotation; public bool IsAnimating => !_dragging - && (Vector3.SqrMagnitude(transform.position - _targetPosition) > 0.0000004f + && (_arcMoving + || Vector3.SqrMagnitude(transform.position - _targetPosition) > 0.0000004f || Quaternion.Angle(transform.rotation, _targetRotation) > 0.1f || Vector3.SqrMagnitude(transform.localScale - _targetScale) > 0.0000004f); + public bool IsMoving => _arcMoving; public string StatsText => _stats != null && _stats.gameObject.activeSelf ? _stats.text : null; @@ -223,6 +233,64 @@ namespace Vanguard.Board3D Snap(); } + public void PlaceImmediate( + Vector3 position, + Quaternion rotation, + Vector3 scale) + { + _arcMoving = false; + _immediate = false; + transform.position = position; + transform.rotation = rotation; + transform.localScale = scale; + } + + public void StartArcMove( + Vector3 fromPosition, + Quaternion fromRotation, + Vector3 fromScale, + float duration, + float arcHeight) + { + if (_dragging) + return; + + _arcMoving = false; + _immediate = false; + + var distance = Vector3.Distance(fromPosition, _targetPosition); + var angle = Quaternion.Angle(fromRotation, _targetRotation); + var scaleDistance = Vector3.Distance(fromScale, _targetScale); + if (duration <= 0.001f + || (distance < 0.002f && angle < 0.2f && scaleDistance < 0.002f)) + { + Snap(); + return; + } + + _arcFromPosition = fromPosition; + _arcFromRotation = fromRotation; + _arcFromScale = fromScale; + _arcDuration = Mathf.Max(0.01f, duration); + _arcElapsed = 0f; + _arcHeight = Mathf.Max(0f, arcHeight); + _arcMoving = true; + + transform.position = fromPosition; + transform.rotation = fromRotation; + transform.localScale = fromScale; + } + + public void StopMove(bool snap) + { + if (!_arcMoving) + return; + + _arcMoving = false; + if (snap) + Snap(); + } + public void SetVisible(bool visible) { gameObject.SetActive(visible); @@ -296,6 +364,7 @@ namespace Vanguard.Board3D public void Snap() { + _arcMoving = false; transform.position = _targetPosition; transform.rotation = _targetRotation; transform.localScale = _targetScale; @@ -307,6 +376,31 @@ namespace Vanguard.Board3D if (_dragging || deltaTime <= 0f) return; + if (_arcMoving) + { + _arcElapsed += deltaTime; + var t = Mathf.Clamp01(_arcElapsed / _arcDuration); + var eased = t * t * (3f - (2f * t)); + var position = Vector3.Lerp( + _arcFromPosition, + _targetPosition, + eased); + position.y += Mathf.Sin(t * Mathf.PI) * _arcHeight; + transform.position = position; + transform.rotation = Quaternion.Slerp( + _arcFromRotation, + _targetRotation, + eased); + transform.localScale = Vector3.Lerp( + _arcFromScale, + _targetScale, + eased); + + if (t >= 1f) + Snap(); + return; + } + if (_immediate) { Snap(); diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs index a4e19fd..e389193 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -25,6 +25,8 @@ namespace Vanguard.Board3D private readonly Dictionary _cards = new Dictionary(StringComparer.Ordinal); private readonly Stack _pool = new Stack(); + private CardMoveAnimator _moveAnimator; + private CameraDirector _cameraDirector; private readonly List _piles = new List(); private readonly BoardZoneView[] _damageZones = new BoardZoneView[2]; private readonly BoardZoneView[] _triggerZones = new BoardZoneView[2]; @@ -53,7 +55,7 @@ namespace Vanguard.Board3D private BoardZoneView _guardianZone; private BoardZoneView _dropIndicator; - private AttackArrowView _attackArrow; + private ArrowDirector _arrowDirector; private Pile3D _deckSelf; private Pile3D _deckOpponent; private Pile3D _dropSelf; @@ -80,9 +82,18 @@ namespace Vanguard.Board3D public PlaymatLayout Layout => _layout; public int SelfPlayer => _selfPlayer; public bool AttackArrowVisible => - _attackArrow != null && _attackArrow.IsVisible; + _arrowDirector != null && _arrowDirector.AttackVisible; + public CardMoveAnimator MoveAnimator => _moveAnimator; + public CameraDirector CameraDirector => _cameraDirector; public BoardArtProvider ArtProvider { get; set; } + public Vector3 TriggerCameraPoint(int player) + { + var localSeat = _layout.ToLocalSeat(player, _selfPlayer); + var position = _layout.TriggerPosition(localSeat, 0, 1); + return position + (Vector3.up * 0.5f); + } + public bool TryGetCardScreenPosition(BoardCardRef reference, out Vector2 screen) { screen = default; @@ -118,7 +129,13 @@ namespace Vanguard.Board3D { _animationConfig = animationConfig; _inputLock = inputLock; - _attackArrow?.SetConfig(_animationConfig); + _arrowDirector?.SetConfig( + _animationConfig, + ArrowConfig.LoadOrDefault()); + _moveAnimator ??= new CardMoveAnimator( + _inputLock, + _animationConfig); + _moveAnimator.SetConfig(_animationConfig); if (_animationConfig == null) return; @@ -181,6 +198,7 @@ namespace Vanguard.Board3D _cardsRoot.SetParent(transform, false); EnsureCamera(); + _cameraDirector = new CameraDirector(_camera); SetVisible(false); } @@ -207,8 +225,9 @@ namespace Vanguard.Board3D { CancelPointer(); _hasRendered = false; + _moveAnimator?.CancelAll(snapCards: false); _handBindings.Clear(); - _attackArrow?.HideImmediate(); + _arrowDirector?.HideImmediate(); ReleaseBoardLock(); } else @@ -241,21 +260,74 @@ namespace Vanguard.Board3D var specs = new List(40); BuildSpecs(view, state, specs); + _moveAnimator ??= new CardMoveAnimator( + _inputLock, + _animationConfig != null + ? _animationConfig + : AnimationConfig.Default); + + var previousByInstance = new Dictionary(); + var previousKeyByInstance = new Dictionary(); + foreach (var pair in _cards) + { + var previous = pair.Value; + if (previous == null || previous.Ref.InstanceId == 0) + continue; + + previousByInstance[previous.Ref.InstanceId] = previous; + previousKeyByInstance[previous.Ref.InstanceId] = pair.Key; + } + var used = new HashSet(StringComparer.Ordinal); var immediateAll = !_hasRendered; foreach (var spec in specs) { used.Add(spec.Key); - if (!_cards.TryGetValue(spec.Key, out var card) || card == null) + + var instanceId = spec.Ref.InstanceId; + var existingKey = _cards.TryGetValue(spec.Key, out var card) + && card != null; + var sameInstance = existingKey + && card.Ref.InstanceId == instanceId; + var transfer = false; + + if (!sameInstance + && instanceId != 0 + && previousByInstance.TryGetValue(instanceId, out var previous)) { + if (_cards.TryGetValue(spec.Key, out var displaced) + && displaced != null + && displaced != previous) + { + ReleaseCard(displaced); + } + + if (previousKeyByInstance.TryGetValue(instanceId, out var oldKey) + && oldKey != spec.Key) + { + _cards.Remove(oldKey); + } + + card = previous; + transfer = true; + } + else if (!existingKey || !sameInstance) + { + if (existingKey && card != null) + ReleaseCard(card); card = RentCard(); - _cards[spec.Key] = card; } - var changed = !card.gameObject.activeSelf - || card.Ref.InstanceId != spec.Ref.InstanceId - || card.Ref.Zone != spec.Ref.Zone; + _cards[spec.Key] = card; + + var wasActive = card.gameObject.activeSelf; + var fromPosition = card.transform.position; + var fromRotation = card.transform.rotation; + var fromScale = card.transform.localScale; + var fromInstance = card.Ref.InstanceId; + var fromZone = card.Ref.Zone; + card.Bind(spec.Ref); card.SetArt(spec.Art.Front, spec.Art.Accent); card.SetPose( @@ -263,9 +335,64 @@ namespace Vanguard.Board3D spec.BaseRotation, spec.FaceUp, spec.Rested, - immediateAll || changed, + immediateAll, spec.DamageLayout, spec.Scale * Mathf.Max(0.05f, _config.cardScale)); + + var moved = !immediateAll + && (!wasActive + || transfer + || !existingKey + || fromInstance != instanceId + || fromZone != spec.Ref.Zone); + + if (moved) + { + Vector3 sourcePosition; + Quaternion sourceRotation; + Vector3 sourceScale; + if (transfer + || (existingKey && fromInstance == instanceId)) + { + sourcePosition = fromPosition; + sourceRotation = fromRotation; + sourceScale = fromScale; + } + else + { + var source = SourcePose(spec); + sourcePosition = source.Position; + sourceRotation = source.Rotation; + sourceScale = Vector3.one * Mathf.Max( + 0.05f, + spec.Scale * _config.cardScale); + card.PlaceImmediate( + sourcePosition, + sourceRotation, + sourceScale); + } + + var important = spec.Ref.Zone is + BoardZone.Trigger or BoardZone.Damage or BoardZone.Drop; + var duration = important + ? _animationConfig != null + ? _animationConfig.importantMoveDuration + : 0.9f + : _animationConfig != null + ? _animationConfig.moveDuration + : 0.5f; + var arc = _animationConfig != null + ? _animationConfig.arcHeight + : 0.45f; + _moveAnimator.Enqueue( + card, + sourcePosition, + sourceRotation, + sourceScale, + duration, + arc); + } + card.SetHighlight(spec.Highlight); card.SetStats(spec.Stats, spec.StatsModified); @@ -309,36 +436,82 @@ namespace Vanguard.Board3D } public void Clear() { + _moveAnimator?.CancelAll(snapCards: false); ClearCards(); _handBindings.Clear(); _view = null; _hasRendered = false; - _attackArrow?.HideImmediate(); + _arrowDirector?.HideImmediate(); ReleaseBoardLock(); } private void UpdateAttackArrowState(MatchView view) { - if (_attackArrow == null) + if (_arrowDirector == null) return; if (view?.Battle == null || !TryResolveAttackPoints(view, out var from, out var to)) { - _attackArrow.Hide(); + _arrowDirector.HideAll(); return; } - _attackArrow.Show(from, to); + _arrowDirector.SetAttack(from, to); + UpdateBoostArrows(view); } private void UpdateAttackArrowPoints() { - if (_attackArrow == null || !_attackArrow.IsVisible || _view?.Battle == null) + if (_arrowDirector == null || _view?.Battle == null) return; if (TryResolveAttackPoints(_view, out var from, out var to)) - _attackArrow.SetPoints(from, to); + _arrowDirector.SetAttack(from, to); + + UpdateBoostArrows(_view); + } + + private void UpdateBoostArrows(MatchView view) + { + if (_arrowDirector == null || view?.Battle == null) + return; + + var battle = view.Battle; + var boosts = new List<(Vector3 From, Vector3 To)>(); + if (battle.BoosterSlot >= 0) + { + var attacker = BattleCard( + view, + battle.AttackerPlayer, + battle.AttackerSlot); + var booster = BattleCard( + view, + battle.AttackerPlayer, + battle.BoosterSlot); + if (attacker != null && booster != null) + { + var boosterCenter = ArrowAnchor( + BoardCardRef.Field( + battle.AttackerPlayer, + battle.BoosterSlot, + booster), + battle.AttackerPlayer, + battle.BoosterSlot); + var attackerCenter = ArrowAnchor( + BoardCardRef.Field( + battle.AttackerPlayer, + battle.AttackerSlot, + attacker), + battle.AttackerPlayer, + battle.AttackerSlot); + boosts.Add(( + CardEdge(boosterCenter, attackerCenter), + CardEdge(attackerCenter, boosterCenter))); + } + } + + _arrowDirector.SetBoosts(boosts); } private bool TryResolveAttackPoints( @@ -358,14 +531,16 @@ namespace Vanguard.Board3D if (attacker == null || defender == null) return false; - from = ArrowAnchor( + var attackerCenter = ArrowAnchor( BoardCardRef.Field(battle.AttackerPlayer, battle.AttackerSlot, attacker), battle.AttackerPlayer, battle.AttackerSlot); - to = ArrowAnchor( + var defenderCenter = ArrowAnchor( BoardCardRef.Field(defenderIndex, battle.TargetSlot, defender), defenderIndex, battle.TargetSlot); + from = CardEdge(attackerCenter, defenderCenter); + to = CardEdge(defenderCenter, attackerCenter); return true; } @@ -379,6 +554,20 @@ namespace Vanguard.Board3D + (Vector3.up * 0.2f); } + private Vector3 CardEdge(Vector3 center, Vector3 toward) + { + var direction = toward - center; + direction.y = 0f; + if (direction.sqrMagnitude < 0.0001f) + return center; + + direction.Normalize(); + var radius = Mathf.Max( + _config.cardWidth, + _config.cardHeight) * 0.5f * _config.cardScale; + return center + (direction * radius); + } + private CardView BattleCard(MatchView view, int playerIndex, int slot) { var player = view?.Players.FirstOrDefault(item => item.Index == playerIndex); @@ -615,12 +804,17 @@ namespace Vanguard.Board3D private void BuildAttackArrow() { - _attackArrow = new AttackArrowView(); - _attackArrow.Build(transform, _animationConfig); + _arrowDirector = new ArrowDirector( + transform, + _animationConfig, + ArrowConfig.LoadOrDefault()); } private void EnsureCamera() { + if (_cameraDirector != null && _cameraDirector.IsActive) + return; + if (_camera == null) _camera = Camera.main; if (_camera == null) @@ -794,7 +988,8 @@ namespace Vanguard.Board3D Position = _layout.TriggerPosition(localSeat, i, count), BaseRotation = Quaternion.Euler( 0f, - _layout.BaseYaw(localSeat), + _layout.BaseYaw(localSeat) + + (localSeat == 0 ? 90f : -90f), 0f), FaceUp = true, Rested = false, @@ -814,8 +1009,12 @@ namespace Vanguard.Board3D if (!damageChoice && !abilityCost) return null; - if (abilityCost && state.AbilityDamageIndex == index) + if (abilityCost + && (state.AbilityDamageIndex == index + || state.AbilityDamageIndexes.Contains(index))) + { return _config.selectedColor; + } return _config.highlightColor; } @@ -1009,7 +1208,11 @@ namespace Vanguard.Board3D : null; drop.SetCount( player.Drop?.Count ?? 0, - topDrop != null ? ResolveArt(topDrop.PrintingId).Front : null, + topDrop != null + ? (_config.dropZoneFaceDown + ? BoardTextures.Back + : ResolveArt(topDrop.PrintingId).Front) + : null, _config.showZoneLabels ? "DROP" : string.Empty, new Color(0.30f, 0.30f, 0.40f)); } @@ -1064,13 +1267,15 @@ namespace Vanguard.Board3D UpdateHandView(); var delta = Time.unscaledDeltaTime; + _moveAnimator?.Tick(delta); + _cameraDirector?.Tick(delta); foreach (var card in _cards.Values) { if (card != null) card.Tick(delta); } - _attackArrow?.Tick(delta); + _arrowDirector?.Tick(delta); UpdateAttackArrowPoints(); if (_inputLock != null && _inputLock.IsLocked) @@ -1454,6 +1659,25 @@ namespace Vanguard.Board3D ApplySlotHighlights(); } + private (Vector3 Position, Quaternion Rotation, Vector3 Scale) SourcePose( + CardSpec spec) + { + var localSeat = _layout.ToLocalSeat( + spec.Ref.Player, + _selfPlayer); + var position = _layout.DeckPosition(localSeat); + var rotation = Quaternion.Euler( + 0f, + _layout.BaseYaw(localSeat), + 0f); + return ( + position, + rotation, + Vector3.one * Mathf.Max( + 0.05f, + spec.Scale * _config.cardScale)); + } + private BoardArt ResolveArt(string printingId) { if (ArtProvider != null) @@ -1510,7 +1734,9 @@ namespace Vanguard.Board3D if (_instance == this) _instance = null; - _attackArrow?.Dispose(); + _moveAnimator?.CancelAll(snapCards: false); + _arrowDirector?.Dispose(); + _cameraDirector?.Restore(); ReleaseBoardLock(); } @@ -1525,6 +1751,9 @@ namespace Vanguard.Board3D private bool AnyCardAnimating() { + if (_moveAnimator != null && _moveAnimator.IsBusy) + return true; + foreach (var card in _cards.Values) { if (card != null && card.IsAnimating) @@ -1567,4 +1796,4 @@ namespace Vanguard.Board3D public Color? Highlight; } } -} +} \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index a5bc668..136f0c5 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -32,6 +32,7 @@ namespace Vanguard.UI private readonly Action _onExit; private readonly AnimationConfig _animationConfig; private readonly InputLockService _inputLock; + private TriggerCheckSequence _triggerSequence; private bool _feedbackLockHeld; private string _lastInputLockSummary; private readonly Dictionary _catalog = @@ -106,6 +107,7 @@ namespace Vanguard.UI private readonly HashSet _mulliganSelected = new HashSet(); private bool _mulliganUsed; private int _abilityDamageIndex = -1; + private readonly HashSet _abilityDamageIndexes = new HashSet(); private readonly HashSet _highlightSlots = new HashSet(); private GameBoard3D _board3D; @@ -256,6 +258,9 @@ namespace Vanguard.UI _board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default); _board3D.ConfigureRuntime(_animationConfig, _inputLock); + _triggerSequence = new TriggerCheckSequence( + _board3D.CameraDirector, + _inputLock); _board3D.ArtProvider = ResolveBoardArt; _board3D.CardClicked += OnBoard3DCardClicked; _board3D.CardRightClicked += OnBoard3DCardRightClicked; @@ -274,6 +279,9 @@ namespace Vanguard.UI CancelReconnect(); _view = null; _finished = false; + _triggerWasPending = false; + _lastTriggerKey = null; + _triggerSequence?.Cancel(); _confirmConcede = false; _concedeLabel.text = "Сдаться"; _concede.interactable = true; @@ -290,6 +298,7 @@ namespace Vanguard.UI _mulliganSelected.Clear(); _mulliganUsed = false; _abilityDamageIndex = -1; + _abilityDamageIndexes.Clear(); _pendingBoosterSlot = -1; _pendingBoostAttackerSlot = -2; ClearGuardSelection(); @@ -350,6 +359,9 @@ namespace Vanguard.UI } _view = null; + _triggerWasPending = false; + _lastTriggerKey = null; + _triggerSequence?.Cancel(); HideActionPrompt(); _root.gameObject.SetActive(false); _result.gameObject.SetActive(false); @@ -376,6 +388,7 @@ namespace Vanguard.UI UpdateClock(); UpdateFeedback(); + _triggerSequence?.Tick(_animationConfig); RefreshActionPrompt(); SanitizeInputLocks(); SyncAbilityPrompt(); @@ -767,10 +780,45 @@ namespace Vanguard.UI } Render(); + SyncTriggerSequence(); SyncAbilityPrompt(); SyncCardChoicePrompt(); } + private bool _triggerWasPending; + private string _lastTriggerKey; + + private void SyncTriggerSequence() + { + if (_triggerSequence == null || _view == null) + return; + + var trigger = _view.Trigger; + if (trigger != null && _view.ActivePlayer == _view.You) + { + var key = trigger.Player + ":" + trigger.PrintingId + + ":" + _view.EventSeq; + if (!_triggerWasPending + || !string.Equals(_lastTriggerKey, key, StringComparison.Ordinal)) + { + _triggerWasPending = true; + _lastTriggerKey = key; + _triggerSequence.ShowTrigger( + _animationConfig, + _board3D.TriggerCameraPoint(trigger.Player)); + } + + return; + } + + if (_triggerWasPending) + { + _triggerWasPending = false; + _lastTriggerKey = null; + _triggerSequence.EndChain(_animationConfig); + } + } + private void OnEvents(ServerEvents events) { if (events?.Events != null) @@ -1118,6 +1166,8 @@ namespace Vanguard.UI } _abilityDamageIndex = -1; + + _abilityDamageIndexes.Clear(); _pendingIntents.Clear(); _highlightSlots.Clear(); _pendingBoosterSlot = -1; @@ -1691,6 +1741,12 @@ namespace Vanguard.UI MatchFinished = _view != null && _view.IsFinished, }; + if (IsAbilityDamageTargeting()) + { + foreach (var index in _abilityDamageIndexes) + state.AbilityDamageIndexes.Add(index); + } + foreach (var key in _highlightSlots) state.Highlights.Add(key); @@ -2220,6 +2276,56 @@ namespace Vanguard.UI || text.Contains("встречн"); } + private int CounterBlastAmount(ActivateAbilityIntent intent) + { + if (intent == null || _view == null) + return 1; + + var card = InstanceAt(_view.You, intent.InstanceId); + var meta = ResolveCatalogItem(card?.PrintingId ?? string.Empty); + if (meta == null) + return 1; + + var text = ((meta.TextEn ?? string.Empty) + " " + + (meta.TextRu ?? string.Empty)).ToLowerInvariant(); + var markers = new[] + { + "counter-blast", + "counter blast", + "контр-бласт", + }; + foreach (var marker in markers) + { + var start = text.IndexOf(marker, StringComparison.Ordinal); + if (start < 0) + continue; + + var index = start + marker.Length; + while (index < text.Length + && (text[index] == ' ' + || text[index] == ':' + || text[index] == '(' + || text[index] == '[')) + { + index++; + } + + var value = 0; + while (index < text.Length + && text[index] >= '0' + && text[index] <= '9') + { + value = (value * 10) + (text[index] - '0'); + index++; + } + + if (value > 0) + return value; + } + + return 1; + } + private void ActivateAbility(ActivateAbilityIntent intent) { if (intent == null || _view == null || _pendingAbilityCost != null) @@ -2235,6 +2341,7 @@ namespace Vanguard.UI _pendingAbilityCost = null; _abilityCostDiscardInstanceId = 0; _abilityDamageIndex = -1; + _abilityDamageIndexes.Clear(); StartDiscardSelection( "Выберите карту в руке для сброса.", variants, @@ -2257,6 +2364,7 @@ namespace Vanguard.UI _pendingAbilityCost = intent; _abilityCostDiscardInstanceId = 0; _abilityDamageIndex = -1; + _abilityDamageIndexes.Clear(); _pendingIntents.Clear(); _highlightSlots.Clear(); SetStatus( @@ -2271,10 +2379,13 @@ namespace Vanguard.UI return; if (RequiresCounterBlast(_pendingAbilityCost) - && _abilityDamageIndex < 0) + && _abilityDamageIndexes.Count + != CounterBlastAmount(_pendingAbilityCost)) { SetStatus( - "Выберите карту урона для Counter Blast.", + "Выберите карты урона для Counter Blast: " + + _abilityDamageIndexes.Count + " / " + + CounterBlastAmount(_pendingAbilityCost) + ".", new Color(1f, 0.70f, 0.35f)); return; } @@ -2284,6 +2395,9 @@ namespace Vanguard.UI InstanceId = _pendingAbilityCost.InstanceId, DiscardPrintingId = _pendingAbilityCost.DiscardPrintingId, DamageIndex = _abilityDamageIndex, + DamageIndexes = _abilityDamageIndexes + .OrderBy(index => index) + .ToArray(), }; ClearAbilityCostSelection(); OnIntentClicked(activate); @@ -2294,6 +2408,7 @@ namespace Vanguard.UI _pendingAbilityCost = null; _abilityCostDiscardInstanceId = 0; _abilityDamageIndex = -1; + _abilityDamageIndexes.Clear(); } private void PruneAbilityCostSelection() @@ -2319,8 +2434,11 @@ namespace Vanguard.UI if (_pendingAbilityCost == null) return false; - return !RequiresCounterBlast(_pendingAbilityCost) - || _abilityDamageIndex >= 0; + if (!RequiresCounterBlast(_pendingAbilityCost)) + return true; + + return _abilityDamageIndexes.Count + == CounterBlastAmount(_pendingAbilityCost); } private void ToggleGuardFromDrop(BoardCardRef source) @@ -2727,6 +2845,7 @@ namespace Vanguard.UI _pendingAbilityCost = ability; _abilityCostDiscardInstanceId = discardInstanceId; _abilityDamageIndex = -1; + _abilityDamageIndexes.Clear(); Render(); return; } @@ -3129,11 +3248,18 @@ namespace Vanguard.UI else if (_pendingAbilityCost != null) { title = "ОПЛАТА"; - hint = RequiresCounterBlast(_pendingAbilityCost) - ? _abilityDamageIndex < 0 - ? "Выберите карту урона для Counter Blast." - : "Оплата выбрана. Подтвердите в панели действий." - : "Оплата выбрана. Подтвердите в панели действий."; + if (RequiresCounterBlast(_pendingAbilityCost)) + { + var required = CounterBlastAmount(_pendingAbilityCost); + hint = "Выберите карты урона для Counter Blast: " + + _abilityDamageIndexes.Count + " / " + required + "."; + if (_abilityDamageIndexes.Count >= required) + hint = "Оплата выбрана. Подтвердите в панели действий."; + } + else + { + hint = "Оплата выбрана. Подтвердите в панели действий."; + } } else if (_pendingBoosterSlot >= 0 && _pendingIntents.Count > 0) { @@ -3525,9 +3651,13 @@ namespace Vanguard.UI if (!IsAbilityDamageTargeting()) return; - _abilityDamageIndex = displayIndex; + if (!_abilityDamageIndexes.Remove(displayIndex)) + _abilityDamageIndexes.Add(displayIndex); + _abilityDamageIndex = _abilityDamageIndexes.Count > 0 + ? _abilityDamageIndexes.Min() + : -1; SetStatus( - "Для Counter Blast выбрана карта урона " + (displayIndex + 1) + ".", + "Выбрано карт урона: " + _abilityDamageIndexes.Count + " / " + CounterBlastAmount(_pendingAbilityCost) + ".", new Color(0.45f, 0.95f, 0.62f)); Render(); } @@ -4283,4 +4413,4 @@ namespace Vanguard.UI } } } -} +} \ No newline at end of file diff --git a/client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs b/client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs index 952d998..c9454a2 100644 --- a/client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs @@ -184,6 +184,61 @@ namespace Vanguard.Tests } } + [Test] + public void Trigger_Zone_Uses_Local_Plus_And_Minus_Ninety_Degrees() + { + var config = PlaymatLayoutConfig.CreateDefault(); + var board = GameBoard3D.Create(null, config); + try + { + board.SetVisible(true); + var view = BuildView(you: 0); + var self = view.Players.Single(player => player.Index == 0); + var opponent = view.Players.Single(player => player.Index == 1); + self.Trigger = new List + { + new CardView + { + PrintingId = "BT01/002EN", + InstanceId = 31, + FaceUp = true, + }, + }; + opponent.Trigger = new List + { + new CardView + { + PrintingId = "BT01/003EN", + InstanceId = 32, + FaceUp = true, + }, + }; + + board.Render(view, new BoardRenderState()); + + var own = ActiveCards().Single(card => + card.Ref.Zone == BoardZone.Trigger + && card.Ref.Player == 0); + var enemy = ActiveCards().Single(card => + card.Ref.Zone == BoardZone.Trigger + && card.Ref.Player == 1); + var layout = new PlaymatLayout(config); + + Assert.AreEqual( + 90f, + Mathf.DeltaAngle(layout.BaseYaw(0), own.TargetYaw), + 0.1f); + Assert.AreEqual( + -90f, + Mathf.DeltaAngle(layout.BaseYaw(1), enemy.TargetYaw), + 0.1f); + } + finally + { + Object.DestroyImmediate(board.gameObject); + } + } + [Test] public void Pool_Reuses_Cards_Between_Snapshots() { @@ -256,6 +311,47 @@ namespace Vanguard.Tests } } + [Test] + public void Card_Arc_Move_Interpolates_And_Finishes_At_Target() + { + var config = PlaymatLayoutConfig.CreateDefault(); + var root = new GameObject("ArcRoot"); + var card = Card3D.Create(root.transform, config); + try + { + var from = new Vector3(-1f, 0.1f, 0f); + var to = new Vector3(1f, 0.1f, 0f); + card.PlaceImmediate(from, Quaternion.identity, Vector3.one); + card.SetPose( + to, + Quaternion.identity, + true, + false, + false, + scale: 1f); + card.StartArcMove( + from, + Quaternion.identity, + Vector3.one, + 0.5f, + 0.4f); + + card.Tick(0.25f); + Assert.IsTrue(card.IsMoving); + Assert.Greater(card.transform.position.y, from.y); + + card.Tick(0.5f); + Assert.IsFalse(card.IsMoving); + Assert.Less( + Vector3.Distance(card.transform.position, to), + 0.001f); + } + finally + { + Object.DestroyImmediate(root); + } + } + private static List ActiveCards() => Resources.FindObjectsOfTypeAll() .Where(card => card.gameObject.activeInHierarchy) diff --git a/docs/3d-ux-backlog.md b/docs/3d-ux-backlog.md index 7653684..4a85370 100644 --- a/docs/3d-ux-backlog.md +++ b/docs/3d-ux-backlog.md @@ -1,9 +1,25 @@ # 3D UX backlog -Анализ требований по UX-долгам 3D-поля. **Это план доработок, не реализация.** +Анализ требований по UX-долгам 3D-поля и статус реализации. Дата анализа: **2026-10-24**. -## 0. Фактическое состояние +## Статус реализации (2026-10-24) + +- **P0 готово:** `CardMoveAnimator` + дуга/scale/rotation, stagger и + `InputLock`; diff по `InstanceId` с переносом пула; `AnimationConfig` + расширен; ориентация Trigger Zone ±90°; `DropZoneFaceDown`. +- **P0 готово:** `CameraDirector` и `TriggerCheckSequence` (zoom → hold → + return, цепочка, восстановление pose). +- **P1 частично:** `ArrowConfig`, общий `ArrowDirector`, красная attack и + синяя boost-стрелка, grow/arc/dash (UV-tile fallback), якоря у края карт. +- **P1 частично:** multi-target Counter Blast доведён до сервера + (`DamageIndexes` в `ActivateAbilityIntent`) и клиентского выбора; overlay + заменён счётчиком в панели «Действия» (по правилам проекта). +- **Не сделано:** multi-target Soul Blast и другие типы стоимости, + `EffectConfig`, отдельный `CostPaymentOverlay`, `AttackResolutionQueue`, + world-space Power/Shield, world-space dash shader. + +## 0. Состояние до реализации (историческая часть) Промпт предполагает наличие `CardMoveAnimator`, `CameraDirector`, `TriggerCheckSequence`, `OnCardMoved`/`OnTargetChanged`, `CostPaymentController`, @@ -221,16 +237,19 @@ deck / Drop (если faceDown) показывают рубашку; flip про 5. **P2**: World-Space Power/Shield, удаление/скрытие `CombatStats` при необходимости, polish и профилирование. -## 8. Открытые решения перед кодом +## 8. Принятые решения -- UniTask или Coroutine (в проекте нет ни DOTween, ни UniTask). -- `DropZoneFaceDown` default: `false` по Classic, но нужен ли рубашкой вверх - для Drop в текущем UX? -- Dash: world-space shader или сегментированный mesh (рекомендация — - shader, fallback — mesh). -- Судьба верхнего `CombatStats`: скрыть/оставить debug/перенести к картам. -- Протокол multi-target cost: новый `CostPaymentIntent` или расширение - `ActivateAbilityIntent` массивами (влияет на `Vanguard.Shared` и - обратную совместимость). -- Камера: кто владеет `Camera` во время trigger sequence и как это - сочетается с `EnsureCamera()`. +- **Асинхронность:** без новых пакетов; `CardMoveAnimator` и + `TriggerCheckSequence` работают через `Tick`/корутинный эквивалент. +- **Drop:** `DropZoneFaceDown = false` по Classic; флаг доступен в + `PlaymatLayoutConfig`. +- **Dash:** UV-tile fallback (`LineTextureMode.Tile` + прокрутка offset); + world-space shader оставлен как будущая замена. +- **`CombatStats`:** верхний strip пока оставлен как HUD; world-space + Power/Shield — P2. +- **Multi-target cost:** расширен `ActivateAbilityIntent.DamageIndexes` + (`[Key(3)]`, обратная совместимость); `CounterBlast` принимает массив. + `Soul Blast` и остальные типы — следующий шаг. +- **Камера:** владелец — `CameraDirector`; `EnsureCamera()` не перезаписывает + pose, пока `CameraDirector.IsActive`. `TriggerCheckSequence` снимает + `InputLock` перед выбором цели и возвращает камеру после hold. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 19f74f2..fd92aeb 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1091,3 +1091,19 @@ Battle; Boot и общие сервисы остаются персистент кнопок выбора в боковой панели нет. - Проверки: сервер **144** правил + **45** серверных, `validate` и `translations check` OK; клиент EditMode **36/36**. + +## 2026-10-24 — 3D UX P0/P1 + +- Добавлены `CardMoveAnimator` (дуга, stagger, InputLock, diff по InstanceId) + и расширенный `AnimationConfig`; новые карты/переходы больше не телепортятся. +- Добавлены `CameraDirector` и `TriggerCheckSequence`: zoom → reveal → return, + восстановление исходной камеры, владение камерой во время цепочки. +- Trigger Zone: своя +90°, противника −90° относительно базы; Drop получил + `DropZoneFaceDown`. +- `ArrowDirector`/`ArrowConfig`: attack/boost стрелки, grow/arc/dash, + наконечники у края карт. +- Multi-target Counter Blast: `DamageIndexes` в `ActivateAbilityIntent` и + `GameEngine.CounterBlast`; клиентский выбор нескольких карт урона и счётчик + в панели «Действия». +- Проверки: правила 151, сервер 45, клиентские EditMode 40/40; `validate`, + `translations check`, standalone build; сервер задеплоен. \ No newline at end of file diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index cd69c85..5a4070a 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -1,6 +1,6 @@ # Vanguard ReMaster — состояние сессии -**Дата:** 2026-10-23 +**Дата:** 2026-10-24 **HEAD:** см. `git log -1` **Коротко:** сервер готов, клиент — рабочий каркас с 3D-сценами. @@ -19,7 +19,7 @@ матчмейкинг, WebSocket-матч, история, реплеи. - Orleans включается `Orleans:Enabled`; в compose включён. Один silo, in-process реестр соединений. -- Тесты: **149** правил + **45** серверных, CI зелёный. +- Тесты: **151** правил + **45** серверных, CI зелёный. - Стенд: `http://192.168.31.158:18080`, `/health` → ok. - Диагностика Ride в `MatchGrain`: входящие интенты, результат, события, снапшоты и `rideReady` по руке/рангам; логи через `docker compose logs`. @@ -159,6 +159,19 @@ обновляются на каждом снимке. Зависшие причины `InputLockService` снимаются, когда reconnect/overlay/ability/card-menu/feedback уже неактивны; `board`-лок больше не накапливается на каждом снапшоте. +- 3D UX (P0/P1): `CardMoveAnimator` переносит карты по дуге с + stagger/`InputLock`; diff идёт по `InstanceId`, пул сохраняет карту при + смене зоны. `AnimationConfig` расширен (`moveDuration`, + `importantMoveDuration`, `moveDelayBetweenCards`, `arcHeight`, + camera/grow/dash). Trigger Zone: своя +90°, противника −90° относительно + базы; `DropZoneFaceDown` в конфиге. `CameraDirector` + + `TriggerCheckSequence`: zoom → hold → return, цепочка, точный Restore; + `EnsureCamera` не перезаписывает камеру во время цепочки. + `ArrowDirector`: красная attack и синяя boost-стрелка, grow/arc/dash + (UV-tile), якоря у края карт. Multi-target Counter Blast: выбранные карты + урона уходят массивом `DamageIndexes`, сервер снимает именно их; счётчик + выбора показывается в панели «Действия». Детали — + `docs/3d-ux-backlog.md`. - Опциональные AUTO-способности (Barcgal после рейда Marron) открывают модальный оверлей «Способность» с «Активировать»/«Пропустить»; в боковой панели этих кнопок нет. После подтверждения призыва сервер предлагает @@ -204,8 +217,10 @@ открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`). - Не готово: 3D-модели карт/стола (пока процедурные примитивы и арты), звук, настройки (язык уже есть), DOTween, drag&drop и live-валидация - декбилдера. Камера боя фиксирована; анимации retire/ghost для - 3D-карт ещё не перенесены. + декбилдера. Камера боя в покое фиксирована, но `CameraDirector` временно + ведёт trigger-последовательность. Не перенесены отдельные retire/ghost + анимации, `AttackResolutionQueue`, multi-target Soul Blast, world-space + Power/Shield. ## Запуск @@ -255,7 +270,7 @@ Standalone Windows: -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat ``` -Клиентские проверки: **38/38** EditMode (сборка всех экранов, импорт арта, +Клиентские проверки: **40/40** EditMode (сборка всех экранов, импорт арта, перевод статических/динамических строк, live-переключение языка, рендер 3D-доски с уроном и отдыхающей картой, геометрия поля, правила перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона, @@ -338,10 +353,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack ## Следующие шаги -1. UX-план 3D-поля: Phase 2–4 закрыты, overlay-экраны вынесены в prefab - (`Resources/Overlays`), раскладка проверена на 1920×1080 и масштабируется - CanvasScaler'ом. Осталось при желании отполировать позиции на конкретном - мониторе. +1. 3D UX: P0 (движение, ориентации, камера/триггер) и базовый P1 (стрелки, + multi-target Counter Blast) готовы. Осталось: `AttackResolutionQueue`, + multi-target Soul Blast/прочие стоимости, world-space Power/Shield, + world-space dash shader; полировка позиций на конкретном мониторе. 2. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты. 3. 3D-наполнение: модели карт/стола, prefab-экраны сцен, анимации retire/ghost. 4. Звук и настройки, DOTween. diff --git a/docs/protocol.md b/docs/protocol.md index e2a3309..e5c258f 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -169,7 +169,7 @@ WebSocket-транспорта. Логика handshake, кадров и тайм | 12 | `SelectTriggerIntent` | `index` | | 13 | `ResolveAbilityIntent` | `accept`, `damageIndex?` | | 14 | `ChooseIntent` | `index` | -| 15 | `ActivateAbilityIntent` | `instanceId`, `discardPrintingId?`, `damageIndex?` | +| 15 | `ActivateAbilityIntent` | `instanceId`, `discardPrintingId?`, `damageIndex?`, `damageIndexes?` | | 16 | `EndTurnIntent` | — | | 17 | `ConcedeIntent` | — | diff --git a/server/src/Vanguard.Rules/Engine/GameCommands.cs b/server/src/Vanguard.Rules/Engine/GameCommands.cs index 2878b87..abdf75e 100644 --- a/server/src/Vanguard.Rules/Engine/GameCommands.cs +++ b/server/src/Vanguard.Rules/Engine/GameCommands.cs @@ -20,7 +20,8 @@ public sealed record ChooseCommand(int Index) : GameCommand; public sealed record ActivateAbilityCommand( int InstanceId, string? DiscardPrintingId = null, - int DamageIndex = -1) : GameCommand; + int DamageIndex = -1, + int[]? DamageIndexes = null) : GameCommand; public sealed record EndTurnCommand : GameCommand; public sealed record CommandResult(bool Accepted, string? Reason) diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs index 3f23651..9c5a9d5 100644 --- a/server/src/Vanguard.Rules/Engine/GameEngine.cs +++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs @@ -75,6 +75,7 @@ public sealed class GameEngine /// Глубина разрешения эффекта: пока > 0, новые триггеры только ставятся в очередь. private int _resolvingDepth; private int _counterBlastDisplayIndex = -1; + private int[]? _counterBlastDisplayIndexes; private readonly bool _enforceFirstTurnAttackRule; /// @@ -336,7 +337,11 @@ public sealed class GameEngine } } - private CommandResult Activate(int instanceId, string? discardPrintingId, int damageIndex) + private CommandResult Activate( + int instanceId, + string? discardPrintingId, + int damageIndex, + int[]? damageIndexes) { if (State.Phase != Phase.Main) return CommandResult.Reject("Активируемые способности доступны только в Основной фазе."); @@ -347,6 +352,17 @@ public sealed class GameEngine var player = State.ActivePlayer; var p = State.Players[player]; + if (damageIndexes is { Length: > 0 }) + { + var faceUp = FaceUpDamage(player); + if (damageIndexes.Any(index => index < 0 || index >= faceUp) + || damageIndexes.Distinct().Count() != damageIndexes.Length) + { + return CommandResult.Reject( + "Выбранные карты Counter Blast недоступны."); + } + } + var unit = AllUnits(player).FirstOrDefault(u => u.Id == instanceId); var fromHand = false; var fromDrop = false; @@ -439,6 +455,7 @@ public sealed class GameEngine } _counterBlastDisplayIndex = damageIndex; + _counterBlastDisplayIndexes = damageIndexes; try { ability.Resolve(context); @@ -446,6 +463,7 @@ public sealed class GameEngine finally { _counterBlastDisplayIndex = -1; + _counterBlastDisplayIndexes = null; } _events.Add(new AbilityResolvedEvent(player, unit.PrintingId, true)); @@ -734,9 +752,28 @@ public sealed class GameEngine // Клиент передаёт индекс в том же порядке, в котором рисует урон: // сначала закрытые, затем открытые карты. - if (_counterBlastDisplayIndex >= 0) + var order = DamageDisplayOrder(player); + var selected = _counterBlastDisplayIndexes; + if (selected is { Length: > 0 }) + { + foreach (var displayIndex in selected) + { + if (flipped >= amount) + break; + + if (displayIndex < 0 || displayIndex >= order.Count) + continue; + + var chosen = order[displayIndex].Card; + if (!chosen.FaceUp) + continue; + + chosen.FaceUp = false; + flipped++; + } + } + else if (_counterBlastDisplayIndex >= 0) { - var order = DamageDisplayOrder(player); if (_counterBlastDisplayIndex < order.Count) { var chosen = order[_counterBlastDisplayIndex].Card; @@ -750,6 +787,8 @@ public sealed class GameEngine _counterBlastDisplayIndex = -1; } + _counterBlastDisplayIndexes = null; + foreach (var card in damage) { if (flipped >= amount) @@ -758,6 +797,15 @@ public sealed class GameEngine if (!card.FaceUp) continue; + if (selected is { Length: > 0 } + && selected.Any(index => + index >= 0 + && index < order.Count + && ReferenceEquals(order[index].Card, card))) + { + continue; + } + card.FaceUp = false; flipped++; } @@ -1783,7 +1831,11 @@ public sealed class GameEngine SelectTriggerCommand c => SelectTrigger(c.Index), ResolveAbilityCommand c => ResolvePendingAbility(c.Accept, c.DamageIndex), ChooseCommand c => Choose(c.Index), - ActivateAbilityCommand c => Activate(c.InstanceId, c.DiscardPrintingId, c.DamageIndex), + ActivateAbilityCommand c => Activate( + c.InstanceId, + c.DiscardPrintingId, + c.DamageIndex, + c.DamageIndexes), EndTurnCommand => EndTurn(), _ => CommandResult.Reject("Неизвестная команда."), }; diff --git a/server/src/Vanguard.Rules/Replay/CommandCodec.cs b/server/src/Vanguard.Rules/Replay/CommandCodec.cs index 0b4a071..047869b 100644 --- a/server/src/Vanguard.Rules/Replay/CommandCodec.cs +++ b/server/src/Vanguard.Rules/Replay/CommandCodec.cs @@ -28,7 +28,7 @@ public static class CommandCodec SelectTriggerCommand c => $"selectTrigger{Separator}{c.Index}", ResolveAbilityCommand c => $"resolveAbility{Separator}{(c.Accept ? 1 : 0)}{Separator}{c.DamageIndex}", ChooseCommand c => $"choose{Separator}{c.Index}", - ActivateAbilityCommand c => $"activate{Separator}{c.InstanceId}{Separator}{c.DiscardPrintingId}{Separator}{c.DamageIndex}", + ActivateAbilityCommand c => $"activate{Separator}{c.InstanceId}{Separator}{c.DiscardPrintingId}{Separator}{c.DamageIndex}{Separator}{(c.DamageIndexes == null ? string.Empty : string.Join(',', c.DamageIndexes))}", EndTurnCommand => "endTurn", _ => throw new ArgumentOutOfRangeException(nameof(command), command, "Неизвестная команда."), }; @@ -63,7 +63,10 @@ public static class CommandCodec "activate" => new ActivateAbilityCommand( ParseInt(parts[1]), Nullable(parts[2]), - parts.Length > 3 ? ParseInt(parts[3]) : -1), + parts.Length > 3 ? ParseInt(parts[3]) : -1, + parts.Length > 4 && parts[4].Length > 0 + ? parts[4].Split(',').Select(ParseInt).ToArray() + : null), "endTurn" => new EndTurnCommand(), _ => throw new FormatException($"Неизвестная команда: {encoded}"), }; diff --git a/server/src/Vanguard.Shared/Projection/IntentMapping.cs b/server/src/Vanguard.Shared/Projection/IntentMapping.cs index 9f9d20a..aca88f4 100644 --- a/server/src/Vanguard.Shared/Projection/IntentMapping.cs +++ b/server/src/Vanguard.Shared/Projection/IntentMapping.cs @@ -48,6 +48,7 @@ public static class IntentMapping InstanceId = c.InstanceId, DiscardPrintingId = c.DiscardPrintingId, DamageIndex = c.DamageIndex, + DamageIndexes = c.DamageIndexes ?? System.Array.Empty(), }, EndTurnCommand => new EndTurnIntent(), _ => throw new ArgumentOutOfRangeException(nameof(command), command, "Неизвестная команда."), @@ -73,7 +74,11 @@ public static class IntentMapping SelectTriggerIntent c => new SelectTriggerCommand(c.Index), ResolveAbilityIntent c => new ResolveAbilityCommand(c.Accept, c.DamageIndex), ChooseIntent c => new ChooseCommand(c.Index), - ActivateAbilityIntent c => new ActivateAbilityCommand(c.InstanceId, c.DiscardPrintingId, c.DamageIndex), + ActivateAbilityIntent c => new ActivateAbilityCommand( + c.InstanceId, + c.DiscardPrintingId, + c.DamageIndex, + c.DamageIndexes), EndTurnIntent => new EndTurnCommand(), ConcedeIntent => null, _ => throw new ArgumentOutOfRangeException(nameof(intent), intent, "Неизвестный интент."), diff --git a/server/src/Vanguard.Shared/Protocol/Intents.cs b/server/src/Vanguard.Shared/Protocol/Intents.cs index d248adc..41e16d5 100644 --- a/server/src/Vanguard.Shared/Protocol/Intents.cs +++ b/server/src/Vanguard.Shared/Protocol/Intents.cs @@ -138,6 +138,12 @@ public sealed class ActivateAbilityIntent : IntentPayload /// Индекс карты в зоне урона для оплаты Counter Blast; -1 — авто. [Key(2)] public int DamageIndex { get; set; } = -1; + + /// + /// Все выбранные карты урона для multi-target Counter Blast. + /// Пустой массив означает старый одиночный DamageIndex. + /// + [Key(3)] public int[] DamageIndexes { get; set; } = System.Array.Empty(); } [MessagePackObject] diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs index d6ddfe4..f59f81e 100644 --- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs +++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Vanguard.Rules.Cards; using Vanguard.Rules.Decks; using Vanguard.Rules.Engine; @@ -231,6 +231,46 @@ public class GameEngineTests Assert.False(second.FaceUp); } + [Fact] + public void CounterBlast_Can_Use_Multiple_Selected_Damage_Cards() + { + var engine = InMainPhase(); + var gancelot = MakeCard(Gancelot); + engine.State.Players[0].Vanguard = gancelot; + engine.State.Players[0].Soul.Add(MakeCard(BlasterBladePrint)); + GiveDamage(engine, 0, 3); + + var first = engine.State.Players[0].Damage[0]; + var middle = engine.State.Players[0].Damage[1]; + var last = engine.State.Players[0].Damage[2]; + + var result = engine.Execute(new ActivateAbilityCommand( + gancelot.Id, + null, + -1, + new[] { 0, 2 })); + Assert.True(result.Accepted, result.Reason); + + Assert.False(first.FaceUp); + Assert.True(middle.FaceUp); + Assert.False(last.FaceUp); + } + + [Fact] + public void CounterBlast_Rejects_Invalid_Multi_Selection() + { + var engine = InMainPhase(); + var overlord = MakeCard(Overlord); + engine.State.Players[0].Vanguard = overlord; + GiveDamage(engine, 0, 2); + + Assert.False(engine.Execute(new ActivateAbilityCommand( + overlord.Id, + null, + -1, + new[] { 0, 2 })).Accepted); + } + [Fact] public void Called_RearGuard_Enters_Standing_And_Replaces_Occupied_Circle() { @@ -2459,4 +2499,4 @@ public class GameEngineTests /// Карта, которой нет в тестовой колоде: кладём в руку или на поле вручную. private static CardInstance MakeCard(string printingId) => new() { Id = Interlocked.Increment(ref _nextInstanceId), PrintingId = printingId }; -} +} \ No newline at end of file