Compare commits

..

216 commits

Author SHA1 Message Date
ebbit1q
b1fe4c85d3
fix sending decks to tappedout (#6832)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-04-23 02:28:12 +02:00
Vorliz
a20f3c0fb4
Fix #6659: Correct logging for bottom-of-library card moves (#6764)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
* Fix #6659:  Correct logging for bottom-of-library card moves

Cause:
- This issue happens due to logic of moving the card from the top of the
  deck being reused when moving from the bottom of the deck, in a way
  that makes it impossible to check if the card came from the bottom.

Resolution:
- Updated the logging logic in the client for card moves.
- Added a gRPC parameter ('is_from_bottom') for card moves.
- Updates the server logic to reverse the order of the card move if the
'is_from_bottom' parameter is true.
- Added a test to show the expected behaviour of the fix.

NOTE: While the changes in this patch seem big, this is due to changing
the loop in the moveCard function to a helper function, in order to make
the bug fix change. The only change to the loop was to pass a
variable attribution to the moveCard function because it was redundant
to be in the loop.

* chore: run format on test

* refactor: new way to check if a move is from the bottom of the deck

* refactor: change isFromBottom check to static function

* update comments

Co-authored-by: ebbit1q <ebbit1q@gmail.com>

---------

Co-authored-by: ebbit1q <ebbit1q@gmail.com>
2026-04-21 19:05:31 +02:00
BruebachL
9226bc9ddd
[TabArchidekt] Place sideboard categories into the sideboard. (#6824)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
Took 35 minutes

Took 3 seconds

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-21 08:09:39 +02:00
ebbit1q
501c4b96d4
clear all players when closing game tab (#6828)
this prevents issues caused by items in play when using the player
destructor in the wrong order
2026-04-21 08:09:20 +02:00
ebbit1q
6ab947418c
add an isEmpty method to printing info (#6805)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
no reason, I just find it easier to understand
2026-04-21 01:26:08 +02:00
BruebachL
6765831b92
Change button colors to be palette aware. (#6821)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Change button colors to be palette aware.

Took 13 minutes


Took 41 seconds

Took 15 seconds

* Change button style.

Took 24 minutes

Took 4 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-20 17:47:15 +02:00
BruebachL
98c4e829f8
[GameSelector/Filters] Properly sync toolbar and dialog. (#6822)
Took 59 minutes

Took 2 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-20 13:19:29 +02:00
BruebachL
ccb9901b28
[DeckAnalytics] Add a checkbox to include/exclude sideboard for calculation (#6823)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [DeckAnalytics] Add a checkbox to include/exclude sideboard for calculation

Took 15 minutes

* default to false, actually

Took 2 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-19 23:08:56 +02:00
tooomm
58a8c7d3df
CI: Use ubuntu-slim for simple jobs (#6798)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-04-19 02:47:14 +02:00
ebbit1q
655a8e52a1 update release name 2026-04-19 01:15:58 +02:00
ebbit1q
db235ecae8 update version number to 3.0.0 2026-04-19 01:11:00 +02:00
BruebachL
682ac4ed0c
Add -R option in Windows NSIS script for silent upgrade (#6818)
* Add -R option in Windows NSIS script

Took 23 minutes

* Small fix.

Took 3 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-19 01:07:38 +02:00
ebbit1q
77978c7178
stop prepending the adventure side (#6819)
the adventure side of adventure cards used to be the first entry in
mtgjson, at some point this changed to the second entry, better
reflecting its secondary nature, however cockatrice has hardcoded the
treatment of the card to assume the second part was the "main" part,
when implementing the treatment of prepare layout cards (#6792) I
copied the behavior over which was then already incorrect, this pr
removes the special treatment of this card layout bringing the result
back in line with expectation: the main part of the card provides the
main type used in cockatrice for sorting and behavior
2026-04-19 01:03:49 +02:00
ebbit1q
e918856fa4
clear player before removing it from the scene (#6817)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
revert #6740
2026-04-18 12:50:53 +02:00
github-actions[bot]
87c4216e80
Update translation files (#6813)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Co-authored-by: github-actions <github-actions@github.com>
2026-04-15 20:41:45 +02:00
BruebachL
832f70496a
[EDHRec] Fix unintentional navigation on card right click (#6814)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
Don't double emit cardClicked and also pass along the mouseEvent so we can check if it's a left or right click.

Took 26 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-15 14:14:30 +02:00
ebbit1q
f97864b72b
set the minimum qt6 version to 6.4 (#6811)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
* set the minimum qt6 version to 6.4

this will be separate from the qt5 removal after release

* add include for optional
2026-04-14 00:59:46 +02:00
ebbit1q
338c56678a
add workaround for windows11 theme (#6810)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-04-13 14:49:31 +02:00
Jeremy Letto
8cc65b8967
Initial implementation completion and refactor (#6806)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Web / React (Node 16) (push) Has been cancelled
Build Web / React (Node lts/*) (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-04-11 23:51:10 -04:00
ebbit1q
2e10b2f5d5
make general settings scrollable (#6800) 2026-04-12 02:38:04 +02:00
ebbit1q
92fe406c22
remove break statement when closing views (#6801)
it's possible that multiple views need to be closed
this is an oversight of #4570
2026-04-12 02:37:43 +02:00
tooomm
d677e2bb70
Print success msg if cache deletion did succeed (#6802) 2026-04-12 02:37:30 +02:00
ebbit1q
e977f123ce
add ccache eviction for files older than 7 days (#6795)
* add ccache eviction for files older than 7 days

also clean up some of the scripts to be more internally consistent

* move name build into the docker container again

* remove one extra empty line [skip ci]

* allow canceling concurrent builds on master for both desktop and docker

* add ccache eviction age to macos as well
2026-04-11 19:18:12 +02:00
tooomm
f56b672307
CI: Allow failing of ccache deletion step (#6799)
* Don't fail cache delete step

* Use `continue-on-error`

* remove leftover
2026-04-11 18:20:35 +02:00
tooomm
29c1d7f3e4
add timeout to job matrixes (#6797) 2026-04-11 18:19:11 +02:00
ebbit1q
36aba81b1b
adds eternal to prioritisation list (#6793)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-04-10 18:18:08 +02:00
ebbit1q
f9fb03b26b
update all runners to use qt6.11 (#6794)
* update all runners to use qt6.11

* use aqt version directly from repo
2026-04-10 18:17:43 +02:00
ebbit1q
d2732ac742
fix prepare cards (#6792)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-04-10 13:42:05 +02:00
transifex-integration[bot]
9aa5702e14
Translate cockatrice_en@source.ts in en_US (#6783)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-04-09 21:27:14 +02:00
ebbit1q
2d412bfe52
fix cardloading on qt5 (#6790) 2026-04-09 21:16:38 +02:00
RickyRister
ac06fb9d1c
[Game] Fix crash by properly parenting QObjects (#6788)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
2026-04-09 15:02:00 +02:00
BruebachL
d7b31f2f9d
Set OracleWizard style to "Modern" instead of "Aero" (#6778)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
* Use fusions own palette.

Took 6 minutes

* Start from default palette always.

Took 4 minutes

* Add modern style.

Took 24 seconds

* Scope this fix to Windows.

Took 4 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-07 15:58:42 +02:00
BruebachL
635fae101d
Use palette colors for resizable panel stylesheets. (#6782)
Took 7 minutes

Took 5 seconds

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-07 15:57:03 +02:00
BruebachL
335022c4aa
Include themes (#6781)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Properly reset default theme.

Took 9 minutes

* Descend in preference on windows.

Took 9 minutes

* Also reset style for custom themes.

Took 3 minutes

* Try things

Took 9 minutes

* Add modern windows style.

Took 8 minutes

* Update theme_manager.cpp

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-04-07 15:02:00 +02:00
Bruno Alexandre Rosa
dbed6890da
feat: support expressions when setting card counters (#6753)
* feat: enable expressions in card counters

* fix includes

* fix multiple selection

* cleanup useless conversions

* const ref where possible

* do not use const, be consistent with local patterns in the file
2026-04-06 22:36:45 -07:00
ebbit1q
3ec9ae9772
fix compiling on arch (#6768)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
* fix compiling on arch

* redo all the logging in affected files
2026-04-05 21:52:46 +02:00
RickyRister
a46ab5cd68
[Game] Allow uppercase X in expressions (#6767)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-04-04 11:40:38 +02:00
SlightlyCircuitous
690a00aa6c
Add Ubuntu 26.04 "Resolute Racoon" build (#6766)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* Create 26.04 Dockerfile

* Update desktop-build.yml

* Update release_template.md

* Add ca-certificates package to build
2026-04-03 20:40:42 +02:00
tooomm
ba786a0289
Update dependabot.yml (#6756)
Some checks failed
Build Docker Image / amd64 & arm64 (push) Has been cancelled
2026-04-01 14:54:27 +02:00
github-actions[bot]
9dfac77ba2
Update translation source strings (#6762)
Some checks failed
Build Docker Image / amd64 & arm64 (push) Waiting to run
Build Desktop / Configure (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-04-01 05:49:17 +02:00
dependabot[bot]
f3370a4f52
Bump doc/doxygen/theme from 1f36200 to d52eafe (#6751)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Bumps [doc/doxygen/theme](https://github.com/jothepro/doxygen-awesome-css) from `1f36200` to `d52eafe`.
- [Release notes](https://github.com/jothepro/doxygen-awesome-css/releases)
- [Commits](1f3620084f...d52eafe3e9)

---
updated-dependencies:
- dependency-name: doc/doxygen/theme
  dependency-version: d52eafe3e9303399fda15661f3d7bb8fe3d7eabc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-28 07:05:24 +01:00
Bruno Alexandre Rosa
7caa88bc58
fix: solve deprecated literal operator error by updating peglib (#6745)
* fix: solve deprecated literal operator error

* update peglib

dcabc63cf4
2026-03-27 18:37:30 +01:00
RickyRister
34a5b8b9ce
[SettingsManager] Make setting getters const (#6748)
* [SettingsManager] Make setting getters const

* remove hashGameType from header
2026-03-27 18:13:25 +01:00
tooomm
d8e3807ec5
Dependabot: Enable git submodules tracking (#6727)
* Enable gitsubmodules

* update comment
2026-03-27 18:11:56 +01:00
ebbit1q
42bd8164a0
remove hardcoded white in vde banner widget (#6684)
* remove hardcoded white in vde banner widget

* set text color to white on higher opacities
2026-03-27 18:10:29 +01:00
ebbit1q
abf6e72ad1
add a nulcheck in the card item animation timer (#6740) 2026-03-27 18:08:51 +01:00
RickyRister
74cce5ccb2
[SettingsManager] Properly handle multithreaded access (#6747) 2026-03-27 17:12:49 +01:00
DawnFire42
dd053c76df
[Game] Improve context menus and fix face-down play from stack (#6739)
Reorganize card context menus across table, stack, and graveyard/exile zones for better consistency: promote Draw Arrow and Clone actions, move related card entries to the bottom, add Play/Play Face Down to the stack menu, and flatten if/else blocks with early returns. Also fix playCard() ignoring the faceDown flag when routing instants/sorceries from the stack, which sent them to the graveyard instead of the table.
2026-03-25 15:03:59 -07:00
scotland0208
5ef428b9d0
Add visual indicator to toggle untap button (#6737)
* Add visual indicator to toggle untap button

* Rename button to match tooltip

* Change name of string in shortcut settings
2026-03-25 14:15:08 -07:00
DawnFire42
94ea574c76
Add moveToTable context menu action and extract tableRowToGridY helper (#6738)
Adds a Table option to the Move menu, allowing cards to be moved directly to the battlefield from any zone. Extracts the repeated tableRow-to-grid-Y conversion logic into TableZone::tableRowToGridY(), consolidating five call sites and fixing a latent bug where cards with tableRow > 2 could land on the wrong row.
2026-03-24 13:45:52 -07:00
DawnFire42
70b41c2095
refactor: extract AbstractPlayerComponent interface for polymorphic player component management. (#6696)
Non-QObject polymorphic interface with setShortcutsActive(), setShortcutsInactive(), and retranslateUi(). Uses regular multiple inheritance to avoid diamond inheritance with Qt's MOC.

All zone menus, SayMenu, and AbstractCounter implement this interface. PlayerMenu manages them via a managedComponents list with two template helpers (addManagedMenu/registerManagedComponent), replacing individual if-guarded lifecycle calls with a single polymorphic loop.

SayMenu now owns its shortcut and translation lifecycle instead of having PlayerMenu manage its title and shortcuts externally.
Counters are iterated via Player::getCounters() rather than managedComponents to avoid duplicating the authoritative owner's map.
2026-03-24 12:31:34 -07:00
ebbit1q
aa85a39d6a
expand local game life total limits (#6730) 2026-03-24 00:16:48 +01:00
tooomm
51c684251f
Add Docker to release template (#6732) 2026-03-24 00:16:26 +01:00
dependabot[bot]
fa2934373c
Bump microsoft/setup-msbuild from 2 to 3 (#6735) 2026-03-23 21:44:31 +01:00
dependabot[bot]
414567f8b6
Bump docker/login-action from 3 to 4 (#6736) 2026-03-23 21:43:42 +01:00
RickyRister
bc219191db
[Game] Refactor options in DlgMoveTopCardsUntil into struct (#6718)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-03-22 12:32:42 +01:00
tooomm
652c8464a7
Docker compose: Link to our GHCR hosted servatrice image (#6728)
* Point to our own image

* Point to our own image

* Readd build

* Readd build
2026-03-22 10:28:24 +01:00
transifex-integration[bot]
067fe9b534
Translate cockatrice/cockatrice_en@source.ts in it (#6724)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-03-21 10:13:43 +01:00
RickyRister
38c85e6db1
[Dialog] Reduce spacing in create local game dialog (#6719)
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
2026-03-18 10:24:34 -07:00
RickyRister
c5cd7d8700
[ShortcutsSettings] Fix duplicate aPlay shortcut; change play shortcut's group (#6716)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 11 (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Fedora 42 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 11 (push) Blocked by required conditions
Build Desktop / Ubuntu 22.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [ShortcutsSettings] Fix duplicate aPlay shortcut; change play shortcut's group

* update description
2026-03-18 10:13:36 +01:00
DawnFire42
fc453c68a7
Add card selection counter (#6685)
* feat(game): add drag selection counter overlay
   Display count of selected cards inside the lasso during drag selection.
   Count appears near cursor, repositioning to stay within selection bounds.

   Includes SelectionRubberBand subclass to allow label to appear above band.
   QRubberBand calls raise() in showEvent/changeEvent to stay on top - this
   subclass suppresses that behavior so dragCountLabel can be visible.

   Adds user setting to enable/disable the drag count overlay.

* feat(game): add persistent selection counter overlay.
Display total count of selected cards in bottom-right corner when multiple cards are selected.
Updates on selection changes and window resize.
The counter connects to QGraphicsScene::selectionChanged to stay up-to-date without requiring manual refresh.
Adds user setting to enable/disable the total count overlay.

---------

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2026-03-16 23:44:29 +01:00
ebbit1q
69c046cca4
remove all separate storing of widget sizes, rely on geometry (#6712) 2026-03-16 23:38:47 +01:00
tooomm
dd8164611b
revert env name (#6711) 2026-03-16 23:38:04 +01:00
dependabot[bot]
b4a5c863d7
Bump docker/metadata-action from 5 to 6 (#6713)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 21:11:32 +01:00
dependabot[bot]
4a3d79d00b
Bump docker/build-push-action from 6 to 7 (#6714)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 21:10:22 +01:00
tooomm
96f436b65e
CI: Resolve to latest Qt version in range for Windows as well (#6700)
* CI: Resolve to latest Qt version in range for Windows as well

* install aqt

* Check dedicated win version
2026-03-15 17:47:48 +01:00
tooomm
cf01dc770b
CI: Adjustments to ccache usage (#6703)
* Increase ccache size

* Fix Arch ccache usage

* more cleanup

* harmonize names
2026-03-15 17:41:17 +01:00
DawnFire42
ce652de272
Fix/cmake test only build (#6709)
* fix(cmake): guard filter_string_test behind WITH_ORACLE or WITH_CLIENT. filter_string_test links against libcockatrice_filters, which is only built when WITH_ORACLE or WITH_CLIENT is enabled. Without this guard, test-only builds fail at configure time because the target doesn't exist. The guard condition mirrors the one in the root CMakeLists.txt that controls whether libcockatrice_filters is built.

* fix(cmake): centralize TEST_QT_MODULES in FindQtRuntime.cmake  Each test CMakeLists.txt was independently defining TEST_QT_MODULES with its own subset of Qt modules. This duplicated knowledge that already lives in FindQtRuntime.cmake (which handles module discovery for all other targets: SERVATRICE, COCKATRICE, ORACLE). Consolidate into a single definition using the union of all test requirements (Concurrent Network Svg Widgets), matching the existing pattern for application-target modules. This ensures test-only builds (-DTEST=ON without application targets) discover all necessary Qt components.

* fix(cmake): guard libcockatrice_network behind application targets. libcockatrice_network is only needed by the client, server, and oracle targets. Other application-specific libraries (settings, models, filters) already have similar guards. This was an oversight that caused test-only builds to fail when network dependencies weren't available.
2026-03-15 17:24:50 +01:00
DawnFire42
9bb399606c
refactor: extract shared card insertion algorithm from hand/stack zones (#6701)
Hand and stack zones had near-identical addCardImpl() implementations, differing only in whether resetState() preserves annotations.
Extract the shared pattern into a template function (CardZoneAlgorithms::addCardToList) to eliminate duplication and enable isolated testing without Qt dependencies.
Pile, table, and zone-view logic are intentionally excluded — their post-add behavior (signals, coordinate placement, hidden cards) is materially different.
2026-03-15 00:39:44 -07:00
tooomm
8180d2e3b0
CI: Update compiler cache on hit (#6691)
* update ccache

* Update desktop-build.yml

* Update desktop-build.yml
2026-03-14 15:12:10 +01:00
RickyRister
33d5721490
[Game] Fix not using zone-specific card menu for opponent's cards (#6695) 2026-03-14 11:41:38 +01:00
transifex-integration[bot]
2b2a6db081
Translate oracle/oracle_en@source.ts in it (#6692)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-03-13 21:22:48 +01:00
DawnFire42
aa4592dc9e
Add local game options (#6669)
* Add local game options dialog. Introduces LocalGameOptions struct and DlgLocalGameOptions dialog to replace the previous QInputDialog for starting local games. Encapsulates game configuration with a simple interface that prevents parameter explosion as options are added. The dialog provides UI with settings persistence via SettingsCache

* integrate local game options into main window. Replaces QInputDialog with DlgLocalGameOptions in actSinglePlayer(). The startLocalGame() function now accepts LocalGameOptions, enabling configuration of starting life total and spectator visibility in addition to player count. Also adds user documentation for the local game options flow.

* Removed superfluous documentation file

* removed spectator option and moved structure definition

* Now remember settings separately and & shortcuts removed

* re-run checks
2026-03-12 14:30:01 -07:00
RickyRister
20ad9af989
[CacheSettings] Refactor country list creation (#6687) 2026-03-12 11:00:32 -07:00
DawnFire42
9e2276a59f
Refactor zone names (#6686)
* Add ZoneNames constants for protocol zone identifiers. Introduce a centralized ZoneNames namespace providing constexpr constants for zone identifiers used in the client-server protocol. This establishes a single source of truth for zone names like TABLE, GRAVE, EXILE, HAND, DECK, SIDEBOARD, and STACK. The protocol values remain unchanged (e.g., EXILE maps to rfg for backwards compatibility) while providing meaningful constant names.

* refactor(server): use ZoneNames constants in server game logic

 Replace hardcoded zone name strings with ZoneNames:: constants in:
 - server_player.cpp: zone setup, draw, shuffle, mulligan operations
 - server_abstract_player.cpp: card movement and token destruction
 - server_game.cpp: returning cards when players leave

 No functional changes - purely mechanical string literal replacement.

* refactor(client): use ZoneNames constants in core player/zone logic

 Update the foundational player and zone classes to use ZoneNames::
 constants instead of string literals. Changes include:
 - player.h/cpp: zone initialization and builtinZones set
 - card_zone_logic.cpp: zone name translation for UI display
 - table_zone.cpp: table zone operations

 No functional changes - purely mechanical string literal replacement.

* refactor(client): use ZoneNames constants in player actions and events

 Replace zone name strings with ZoneNames:: constants in the player
 action and event handling code. player_actions.cpp contains the most
 extensive changes (~90+ replacements) covering all card movement
 commands.

 No functional changes - purely mechanical string literal replacement.

* refactor(client): use ZoneNames constants in zone menu handlers

 Update all zone-specific menu files to use ZoneNames:: constants
 for QAction data values and zone targeting. This covers context menus
 for cards, graveyard, hand, and exile (RFG) zones.

 No functional changes - purely mechanical string literal replacement.

* refactor(client): use ZoneNames constants in game scene components

 Update remaining game scene components to use ZoneNames:: constants:
 - arrow_item.cpp: arrow drawing between cards
 - game_scene.cpp: zone view positioning
 - message_log_widget.cpp: removes duplicate local static constants
   that were previously defining zone names redundantly
 - phases_toolbar.cpp: phase actions (untap all)

 Notable: message_log_widget.cpp previously had its own local constants
 (TABLE_ZONE_NAME, GRAVE_ZONE_NAME, etc.) which are now removed in favor
 of the centralized ZoneNames:: constants.

* formatting fix
2026-03-12 00:34:05 +01:00
tooomm
42cec10457
CI: Cleanup (#6677)
* Cleanup vcpkg matrix

* Add filename with extension as output

* fix name -> fullname, cleanup
2026-03-09 21:32:47 +01:00
dependabot[bot]
95d7e027fc
Bump docker/setup-buildx-action from 3 to 4 (#6682)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 21:30:47 +01:00
dependabot[bot]
8268311fab
Bump docker/setup-qemu-action from 3 to 4 (#6683)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 21:22:26 +01:00
tooomm
413b4b637b
sign+notarize only releases (#6678) 2026-03-09 21:20:38 +01:00
RickyRister
e79bbc67b9
[DeckEditor] Fix undo/redo clearing legality (#6675) 2026-03-08 15:57:39 -07:00
RickyRister
15a1d5440b
[DeckEditor] Fix undo/redo resetting deck sorting (#6673) 2026-03-08 23:50:54 +01:00
tooomm
fd293444c5
tweaks (#6674) 2026-03-08 23:50:33 +01:00
tooomm
852429f248
remove unused zip command (#6676) 2026-03-08 23:48:30 +01:00
tooomm
4606fcdbd5
Add description for code docs (#6679) 2026-03-08 23:27:15 +01:00
tooomm
c375cdbb1a
CI: Upload artifact files directly (#6654)
* Upload artifact files directly

* match pdbs name

* Update name variable
2026-03-08 18:03:01 +01:00
tooomm
f15b70e4ae
Use new attest action (#6671) 2026-03-08 17:53:16 +01:00
RickyRister
2f10634ca2
[DeckList] Fix double-faced cards not importing correctly (#6665)
* [DeckList] Fix double-faced cards not importing correctly

* make tests compile
2026-03-06 11:48:17 -08:00
RickyRister
dead993639
[DeckList] Refactor load from plaintext to take normalizer as param (#6664)
* [DeckList] Refactor load from plaintext to take normalizer as param

* update usages

* weaken unit test

* weaken unit test more

* revert unit test

* move CardNameNormalizer to libcockatrice_card

* update unit test

* formatting
2026-03-06 10:39:04 -08:00
DawnFire42
bd5cbb89d4
refactor: extract CARD_HEIGHT to shared CardDimensions header (#6668)
* refactor: extract CARD_HEIGHT to shared CardDimensions header

  Move duplicated CARD_WIDTH/CARD_HEIGHT constants to card_dimensions.h.
  Fixed documentation in z_value_layer_manager.h.

* WIDTH_F used directly instead of casting

* Improved consistency and added missing newlines at end of files
2026-03-05 19:13:58 -08:00
tooomm
14f1925edc
Add icon to exe (#6655) 2026-03-06 01:50:15 +01:00
ebbit1q
e39bbd2b31
take default theme name from startup instead of using empty (#6663) 2026-03-06 00:19:10 +01:00
DawnFire42
04f06206b7
Refactor/z value constants (#6651)
* feat(z-values): add centralized Z-value constants infrastructure

Magic numbers scattered across the codebase make Z-value layering
hard to understand and maintain. Centralizing them provides:
- Self-documenting layer hierarchy
- Validation utilities for development
- Single source of truth for Z-value ranges

Two-tier header design:
- z_value_layer_manager.h: Foundation with constants and validation
- z_values.h: User-facing namespace with semantic constants

* refactor(z-values): replace magic Z-value numbers with ZValues constants

Magic numbers like 2000000007 are impossible to understand without
context. Named constants (ZValues::DRAG_ITEM) are self-documenting.

This intentionally renumbers overlay Z-values to use a cleaner offset
sequence. The relative stacking order is preserved, which is what
matters for correct rendering.

Each consumer now includes z_values.h and uses semantic constants
instead of magic numbers.

* refactor(z-values): removed redundant inline with contexpr, Updated doc, magic numbers removed from TableZone.
2026-03-05 11:47:14 -08:00
RickyRister
1bcea27a44
[Game] Add face down versions of move cards from library actions (#6661)
* implement actions

* add new actions to menu

* update shortcuts
2026-03-04 18:19:41 -08:00
RickyRister
e7a3ad86eb
[Game] Refactor move cards from library actions (#6658)
* refactor move top/bottom cards actions

* minor cleanup

* translate zone display names
2026-03-04 15:00:18 +01:00
ebbit1q
b36ab66583
nullcheck printing's set in home tab background art crop (#6646)
* nullcheck printing's set in home tab background art crop

* set warning and properly set timer

* fix merge
2026-03-04 00:49:50 +01:00
ebbit1q
566c876bdc
fix scaling for background images on home tab (#6656) 2026-03-04 00:16:45 +01:00
ebbit1q
43acac5f5d
empty card info when switching back to theme background (#6657) 2026-03-04 00:16:19 +01:00
RickyRister
846ecb7e8d
[Client] Support face-down cards in all public zones (#6602)
* [Server] Support face-down cards in all public zones

* add null check

* Check using zone names instead

* add comment

* Rename properties and only pass forceFaceDown

* [Game] Refactor CardDragItem faceDown logic

* revert refactor

* leave face_down unset unless forced

* [Client] Support face-down cards in all public zones

* leave face_down unset unless forced

* log face down

* update remaining logs
2026-03-03 23:19:26 +01:00
RickyRister
2fba5dcd20
[Game] Refactor CardDragItem faceDown logic (#6552)
* Rename properties and only pass forceFaceDown

* [Game] Refactor CardDragItem faceDown logic

* revert refactor

* leave face_down unset unless forced
2026-03-03 23:18:21 +01:00
RickyRister
2828854d32
[Server] Support face-down cards in all public zones (#6539)
* [Server] Support face-down cards in all public zones

* add null check

* Check using zone names instead

* add comment
2026-03-03 23:17:35 +01:00
dependabot[bot]
9794893b63
Bump actions/attest-build-provenance from 3 to 4 (#6653)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3 to 4.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 23:06:35 +01:00
ebbit1q
e57ee8e9c9
fix set sorting (#6630) 2026-03-02 23:06:05 +01:00
dependabot[bot]
f978407a19
Bump actions/upload-artifact from 6 to 7 (#6652)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 23:04:15 +01:00
tooomm
59ce70afc5
Add Code Docs link to issue template (#6649) 2026-03-02 01:59:18 +01:00
DawnFire42
208ccc3a1a
fix(docs): correct typos in README (#6640)
Fix 'projet' -> 'project' and 'invovled' -> 'involved'.
2026-02-26 21:53:39 -08:00
tooomm
6f8f9f016a
CI: windows-2025 runner (#6632)
* `windows-2025` runner

* Install NSIS

* fix naming
2026-02-26 22:31:18 +01:00
Rob Blanckaert
7ad2481e3d
Update UnescapedStringListPart to include parentheses (#6631)
* Update UnescapedStringListPart to include parentheses

* also update deck_filter_string

* add unit test

---------

Co-authored-by: RickyRister <ricky.rister.wang@gmail.com>
2026-02-25 23:55:34 -08:00
RickyRister
12c667afd7
[Search] Fix OR usage in examples (#6628) 2026-02-24 01:34:23 +01:00
RickyRister
2cb16c9fd0
[CardDatabaseDisplay] Reduce width by using icons (#6603)
* [CardDatabaseDisplay] Reduce width by using icons

* use public domain filter svg icon
2026-02-22 20:54:58 -08:00
tooomm
9f00c6f955
Update vcpkg submodule (#6627)
* Update vcpkg

* Target 2025.10.17
2026-02-23 01:44:14 +01:00
RickyRister
a90997353b
[TabGame] Fix concede removing player without waiting for server (#6622) 2026-02-23 01:33:08 +01:00
tooomm
c6dc7eee64
CI: Remove Windows 7 build (#6625)
* Update release_template.md

* Remove Win7
2026-02-22 22:11:58 +01:00
RickyRister
0f2899b5c7
[DeckEditor] Alternate row colors in history list (#6626) 2026-02-22 22:11:10 +01:00
Bruno Alexandre Rosa
71cf3fabbf
build: use qt 6.10 on macOS and win10 (#6491)
* build: use qt 6.8.* LTS in Windows 10 and macOS

* bump msvc

* bump to 6.10

* reset style to the default instead of the first key

---------

Co-authored-by: ebbit1q <ebbit1q@gmail.com>
2026-02-22 18:52:07 +01:00
RickyRister
f0da3cff40
[PictureLoader] Add hash of new gatherer card back image to blacklist (#6621) 2026-02-22 12:12:28 +01:00
ebbit1q
485e4d56aa
Update CONTRIBUTING.md (#6615)
I keep forgetting this so I'm making the warning bigger
2026-02-21 21:38:05 +01:00
tooomm
99424e460b
CI: Fix artifact digest sha for attestation (#6614)
* Fix artifact digest sha for attestation

* linux, too

* Update desktop-build.yml
2026-02-21 15:39:48 +01:00
tooomm
006abf79b1
Add oracle to win pdb's (#6611) 2026-02-21 15:37:26 +01:00
RickyRister
5c3c3bfdba
[ChatView] Fix extra blank line at beginning (#6613) 2026-02-21 15:36:35 +01:00
RickyRister
189f3a7bbc
[ChatView] Fix game log first line incorrect background color (#6612) 2026-02-21 04:40:47 -08:00
ebbit1q
6ab558dd58
reset style to the default instead of the first key (#6596) 2026-02-19 08:36:35 +01:00
RickyRister
a7bb5254a3
[VDE] Fix Qt warnings in log (#6605)
* [VDE] Fix Qt warnings in log

* fix button size
2026-02-19 08:32:56 +01:00
BruebachL
ef87b54b43
[PictureLoader] Set accept header so we don't get CloudFlare'd (#6607)
Took 34 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-02-19 08:32:38 +01:00
RickyRister
88d0ebb12d
[TabDeckEditor] Fix printingSelector dock close button not working (#6604) 2026-02-19 00:50:11 +01:00
RickyRister
bdb42bbbbd
[VDE] Separate layout settings for visual deck editor (#6595) 2026-02-08 13:37:56 -08:00
RickyRister
ac7ff3a0e9
[LayoutSettings] Refactor how widgetSize settings are managed (#6594) 2026-02-08 05:07:53 -08:00
RickyRister
1eb6027443
clean up freeDocksSize in tabs with dockWidgets (#6593) 2026-02-08 05:07:40 -08:00
RickyRister
edc8691731
[LayoutSettings] Don't return by const value (#6592)
* [LayoutSettings] Don't return by const value

* fix compile failure
2026-02-07 20:42:23 -08:00
RickyRister
804a60f1ea
[LayoutSettings] Move over layout settings in global.ini (#6587)
* [LayoutSettings] Move over some settings from general

* remove unused setting
2026-02-07 19:51:12 -08:00
transifex-integration[bot]
a80a0531a6
Translate oracle/oracle_en@source.ts in fr (#6544)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-07 21:48:00 -05:00
RickyRister
24bc713ba8
[DeckListModel] Fix deck hash not updating on card node add/remove (#6584) 2026-02-07 21:47:50 -05:00
dependabot[bot]
4884640070
Bump ssciwr/doxygen-install from 1 to 2 (#6585)
Bumps [ssciwr/doxygen-install](https://github.com/ssciwr/doxygen-install) from 1 to 2.
- [Release notes](https://github.com/ssciwr/doxygen-install/releases)
- [Commits](https://github.com/ssciwr/doxygen-install/compare/v1...v2)

---
updated-dependencies:
- dependency-name: ssciwr/doxygen-install
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 21:47:41 -05:00
RickyRister
8d7535c039
[LayoutSettings] Reorganize hierarchy in settings file (#6586)
* [LayoutSettings] Reorganize hierarchy in settings file

* rename stuff since we're moving settings later
2026-02-07 21:45:33 -05:00
RickyRister
32aa60bb14
[Oracle] Move oracle settings to separate file (#6588) 2026-02-07 21:44:49 -05:00
dependabot[bot]
3ada27eae1
Bump webpack from 5.99.9 to 5.105.0 in /webclient (#6590)
Bumps [webpack](https://github.com/webpack/webpack) from 5.99.9 to 5.105.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.99.9...v5.105.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-07 21:43:51 -05:00
RickyRister
a096a0e3bb
[VDE] Reduce spacing in deck view toolbar (#6591) 2026-02-07 21:43:41 -05:00
BruebachL
d410078673
Refresh chat view colors on theme changed. (#6581)
Took 35 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-28 21:54:11 +01:00
BruebachL
bf5891a910
[ThemeManager] Proper Fusion Palettes (#6580) 2026-01-28 15:36:28 -05:00
RickyRister
c7249dfbd9
[PrintingSelector] Don't change font size (#6573)
* [PrintingSelector] Don't change font size

* remove connection to slider

* update comments
2026-01-28 09:39:54 -08:00
RickyRister
1b29e0bfa8
[PrintingSelector] Reduce spacing (#6574)
* [PrintingSelector] Reduce spacing

* align top

* reduce spacing in flowLayout
2026-01-27 20:50:46 -05:00
RickyRister
5cc5767c87
[CardInfoPictureWidget] Refactor constant fields to static const (#6575)
* [CardInfoPictureWidget] Refactor constant fields to static const

* rename constants

* reformat

* comment out unused
2026-01-27 20:50:19 -05:00
RickyRister
165c4ddd2a
[PrintingSelector] Properly clamp text size to picture on load (#6576) 2026-01-27 20:49:54 -05:00
Rob Blanckaert
7b64970e97
[App] Add Fusion Theme (#6577)
Adds a new default theme that causes the QT Fusion theme to be
selected. This theme looks a bit nicer than 'Windows' and supports
both light and dark mode out of the box.
2026-01-27 20:49:40 -05:00
ebbit1q
5309dd17be
fix typo (#6572) 2026-01-25 20:44:03 -05:00
transifex-integration[bot]
364470b3c8
Updates for project Cockatrice and language en_US (#6543)
* Translate oracle/oracle_en@source.ts in en_US

100% translated source file: 'oracle/oracle_en@source.ts'
on 'en_US'.

* Translate cockatrice_en@source.ts in en_US

100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-25 16:51:03 -05:00
transifex-integration[bot]
915da79cad
Translate cockatrice/cockatrice_en@source.ts in de (#6541)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-25 16:50:40 -05:00
BruebachL
630c71f123
[VDE] Insert at correct index onDataChanged() instead of just appending. (#6556)
Took 25 minutes

Took 3 seconds

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-25 16:06:06 -05:00
ebbit1q
0b4e7be596
followup to #6563 (#6569)
* mess with the font rendering of the home screen until it works

* add more fonts

* increase font weight

* fix outline on the text
2026-01-25 16:05:53 -05:00
ebbit1q
303bd8b607
detect recursive redirects (#6570)
* detect recursive redirects

* handle failure with normal failure handler
2026-01-25 16:05:19 -05:00
RickyRister
c02cf5e89e
[VDE] Fix crash from alt-click when card has unknown set (#6566) 2026-01-25 01:36:10 -08:00
ebbit1q
92f02fa4ee
mess with the font rendering of the home screen until it works (#6563)
* mess with the font rendering of the home screen until it works

* add more fonts
2026-01-25 01:37:19 -05:00
ebbit1q
49e6cf95c4
fix package description (#6565) 2026-01-24 22:34:53 -05:00
ebbit1q
8a126263a9
do not ignore return value of opening log in servatrice when rotating (#6564) 2026-01-24 15:05:26 -05:00
ebbit1q
afdb385770
move returning cards to server_game (#6561)
lock the game mutex instead of player mutex when returning cards
2026-01-24 12:54:29 -05:00
tooomm
5b8897231d
CI: Include submodules in repo checkout (Docs deployment) (#6557)
* Enable submodule checkout in documentation build

* Change submodule checkout to recursive
2026-01-24 13:24:19 +01:00
BruebachL
ffc55aff10
[VDE] Add shortcut to increment cards [Alt + LMB] (#6555)
Took 13 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-24 11:28:06 +01:00
tooomm
2b372c14e4
Docs: Use doxygen-awesome-css theme (#6512)
* add doxygen-theme-css submodule

* enable theme and disable not needed code references

* hide nav sync button

* css and cleanup

* Move comments to dedicated README to not fail config check
2026-01-24 11:22:43 +01:00
BruebachL
12b5525a2d
[TabArchidekt] Cleaner filters, infinite scrolling, and a "go back button" (#6545)
* [TabArchidekt] Cleaner filters, infinite scrolling, and a "go back button"

Took 46 minutes

Took 5 seconds

* Fix infinite scroll triggering in detail view.

Took 25 minutes

Took 3 seconds

* Use setLabelText() so it's white

Took 2 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-24 11:21:12 +01:00
RickyRister
3c48d92663
[DeckEditor] Show info in PrintingSelector dock when override printings enabled (#6554)
* don't hide printing selector dock

* extract warning message to separate file

* create printing disabled info widget
2026-01-24 02:20:16 -08:00
BruebachL
948ec9e042
[VDE] Accurately represent card amounts (#6547) 2026-01-23 08:47:08 -05:00
RickyRister
5a274fdbed
[DeckListModel] Mark all cards in token zone as legal (#6551) 2026-01-23 02:35:36 -08:00
RickyRister
bfeb3a7ca9
[DeckListModel] Refactor to use forEachCard in legality check (#6550) 2026-01-23 01:05:25 -08:00
RickyRister
d363ec5154
[VDS] Fix crash when tab is opened before card database is loaded (#6553) 2026-01-23 09:58:46 +01:00
RickyRister
999733fc0f
[VDE] Fix right click to remove card not working (#6549)
* fix typo

* fix crash
2026-01-23 09:18:05 +01:00
RickyRister
8d274c1924
[DeckListModel] Correctly refresh legality on add card (#6537) 2026-01-22 23:34:24 -05:00
RickyRister
2e1a0bec93
[CardInfo] Refactor: add getLegalityProp method (#6536) 2026-01-22 23:33:21 -05:00
RickyRister
39ddaa0c35
[VDS] Reload deck on hover if file has been modified since last load (#6507)
* add reload to DeckLoader

* [VDS] Reload deck on hover if file has been modified since last load

* fix version incompatibility
2026-01-22 23:31:39 -05:00
RickyRister
d9b9c79112
[VDS] Add option to hide color identity (#6533) 2026-01-19 00:38:48 -08:00
RickyRister
485d5a8b48
[DeckListModel] Fix exception precedence in legality check (#6535) 2026-01-19 00:28:13 -08:00
RickyRister
f7e71a0868
[DeckList] Add optional restrictToZone param to getZoneNodes (#6534) 2026-01-19 00:27:58 -08:00
RickyRister
af2995ba96
[VDS] Ignore tokens when calculating color identity (#6532) 2026-01-18 20:51:57 -05:00
BruebachL
f7ffcc58fe
[Sample hand widget] Create container widget before declaring it as parent (#6530) 2026-01-17 12:18:13 -05:00
RickyRister
792f077071
[VDE] Use splitter in sample hand widget (#6528)
* [VDE] Use splitter in sample hand widget

* remove unused code
2026-01-16 17:23:41 -08:00
RickyRister
9c07c7a963
[TabGame] Automatically sync view menu actions (#6529) 2026-01-16 17:22:48 -08:00
RickyRister
d579c82cb9
[DeckLoader] Make save/load methods static (#6476)
* const

* [DeckLoader] make methods static

* use static methods

* add docs

* add docs
2026-01-16 13:20:36 -05:00
RickyRister
c7c7bf550a
[TabGame] Don't create replay dock if not replay tab (#6527)
* [TabGame] Don't create replay dock if not replay tab

* use replayDock to determine if replay tab

* null check replayManager in dtor
2026-01-16 09:45:10 -08:00
RickyRister
84483c56d7
[TabDeckEditor] Generalize visibility filter and extract it to a separate file (#6526)
* create class

* use new class in old code
2026-01-16 10:12:46 -05:00
RickyRister
1b71519ec6
[VDE] Make sample hand widget look nicer (#6525) 2026-01-16 10:11:39 -05:00
RickyRister
154b9ace92
[TabDeckEditor] Move cardDatabase dock action to top of menu (#6523) 2026-01-16 10:10:36 -05:00
RickyRister
93f0715d02
[TabDeckEditor] Save cardDatabase dock size in settings (#6524) 2026-01-15 21:54:50 -05:00
RickyRister
57e6c91689
[TabDeckEditor] Automatically sync view menu actions (#6522) 2026-01-15 21:05:19 -05:00
github-actions[bot]
6213ccff48
Update translation files (#6521)
Co-authored-by: github-actions <github-actions@github.com>
2026-01-15 06:53:55 +01:00
BruebachL
c075deeb2d
[Placeholder images] Update color. (#6519)
Took 19 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 14:47:27 +01:00
BruebachL
29f60c4a67
[VDE] Placeholder image for deck view if deck is empty (#6516)
* [VDE] A stab at things

Took 14 minutes

Took 10 minutes

Took 5 minutes

Took 4 minutes


Took 41 seconds

Took 10 minutes

Took 3 minutes

* [VDE] Use placeholder image for deck view if deck is empty.

Took 15 minutes

Took 9 seconds

Took 5 seconds

* Sort CMakeList correctly.

Took 35 seconds

Took 23 seconds

* Visibility updates got lost in the rebase.

Took 7 minutes

* Same treatment for printing selector.

Took 42 minutes

* Actually add file.

Took 4 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 14:41:54 +01:00
RickyRister
c553e15036
[TabDeckEditor] Fix bug in #6499 causing view menu actions to sometimes not work (#6518)
* remove a special case

* fix
2026-01-14 14:20:12 +01:00
BruebachL
a4eef648bc
[VDD] Move main type and format filter to quick settings (#6511)
* [VDD] Reorder quick filters

Took 1 hour 10 minutes

Took 5 seconds


Took 49 seconds

* [VDD] Use Font Awesome Icons

Took 49 minutes

Took 5 seconds

* [VDD] Shuffle some widgets around, label things.

Took 31 minutes

Took 5 seconds

* Change buttons to be push rather than toggle.

Took 17 minutes

Took 9 seconds

* Reduce margins, retranslate button texts.

Took 15 minutes

Took 9 seconds

* Actually do it, don't commit the commented out testing version lol

Took 3 minutes

* Start sets in include, correct subtype include/exact match logic.

Took 12 minutes

* Block sync.

Took 16 minutes

Took 8 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 11:56:09 +01:00
RickyRister
47720ff286
[ColorIdentityWidget] Refactor (#6506)
* [ColorIdentityWidget] Refactor and add setter

* rename manaCost field

* nvm, just refactor for now

* use QtUtils

* move clearLayout into populate

* add back cardInfo constructor
2026-01-14 02:41:03 -08:00
BruebachL
289b139be9
[DeckAnalytics] Enforce WUBRGC ordering for analytics. (#6509)
* [DeckAnalytics] Enforce WUBRGC ordering for analytics.

Took 6 minutes

Took 7 seconds

* Include QSet

Took 51 seconds

* Move include out of namespace.

Took 6 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 11:25:45 +01:00
RickyRister
21d60ec3f1
Reduce padding in settings popup (#6504)
* Reduce padding in settings popup

* reduce padding in PrintingSelectors settings

* reduce padding in one of the VDD filter settings
2026-01-14 01:49:07 -08:00
BruebachL
ed1115f4c0
[HomeTab] Add setting to display card info in bottom right for non-theme backgrounds (#6513)
* [HomeTab] Add setting to display card info in bottom right for non-theme backgrounds

Took 43 minutes

Took 9 seconds

* [HomeTab] Also hide shuffle frequency setting on theme background source.

Took 3 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 10:05:45 +01:00
BruebachL
cc5e2ab10a
[VDE] Change sort quick settings button icon from gear to sort arrow (#6514)
* [VDE] Change sort quick settings button icon from gear to sort arrow

Took 12 minutes

* Actually include the icon.

Took 4 minutes

Took 13 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-14 10:05:38 +01:00
RickyRister
b19312be70
[TabDeckEditor] Consolidate dockWidget management (#6499) 2026-01-14 09:48:26 +01:00
BruebachL
a0d1359860
[VDE] Minor cleanups, possibly fullscreen width-lock fix (#6438)
* Refactor some constructor things to their own methods.

* Saner size policies, no manual resize management.


Took 15 seconds

Took 23 seconds

* VDE doesn't need to manually resize either.

Took 6 minutes

* Add plate comments and re-order .cpp to be more structured.

Took 9 minutes

Took 30 seconds

* Add plate comments and re-order DeckCardZoneDisplay.cpp to be more structured

Took 7 minutes

Took 5 seconds

* Add plate comments and re-order CardGroupDisplayWidget.cpp to be more structured

Took 7 minutes

Took 4 minutes

* Include declaration.

Took 3 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-13 09:08:03 +01:00
transifex-integration[bot]
52547bbfe8
Updates for project Cockatrice and language de (#6508)
* Translate oracle/oracle_en@source.ts in de

100% translated source file: 'oracle/oracle_en@source.ts'
on 'de'.

* Translate oracle/oracle_en@source.ts in de

100% translated source file: 'oracle/oracle_en@source.ts'
on 'de'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-11 14:41:16 +01:00
BruebachL
9ab398f08d
[Doxygen] Add documentation for beta channel. (#6510)
* [Doxygen] Add documentation for beta channel.

Took 36 minutes

* [Doxygen] Use default theme.

Took 13 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-11 12:48:31 +01:00
RickyRister
0deaa9d9b4
[DeckEditor] Don't change widget focus when adding card (#6503) 2026-01-09 18:27:54 -08:00
RickyRister
7c7755b61d
[VDE] Fix crash vy adding null check for card in PrintingSelector (#6500) 2026-01-06 22:41:40 -08:00
github-actions[bot]
6340c4a6b7
Update translation source strings (#6465) 2026-01-06 19:35:53 +01:00
RickyRister
0a2fdb05ad
[VDS] Try to fix memory leak by properly parenting widgets (#6498)
* [VDS] Try to fix memory leak by properly parenting widgets

* format
2026-01-06 11:42:35 +01:00
dependabot[bot]
b86853b65c
Bump actions/cache from 4 to 5 (#6496) 2026-01-05 21:18:21 +01:00
RickyRister
192dac0396
[DeckListModel] Consolidate methods and signals for card change (#6466) 2026-01-05 18:28:59 +01:00
RickyRister
85c9d8a9ff
[DeckEditor] Fix tokens being added to maindeck (#6495) 2026-01-05 01:18:38 -08:00
RickyRister
ee2699413c
[TabDeckEditor] Make card database a dock widget (#6472)
* [TabDeckEditor] Make card database a dock widget

* delete eventFilter implementation in abstract
2026-01-05 00:06:22 -08:00
RickyRister
d50297bbe6
[AnalyticsPanel] Use cogwheel icon for configure button (#6494) 2026-01-05 00:03:22 -08:00
RickyRister
489ce416c3
[VDS] Add search query option for comments (#6477) 2026-01-05 08:31:10 +01:00
RickyRister
731c487ccb
[ServerGame] null check participant in getPlayer (#6493) 2026-01-05 01:43:40 -05:00
RickyRister
2d5e8deb75
[Server_AbstractParticipant] Rename bool getters (#6492)
* [Server_AbstractParticipant] Rename bool getters

* reformat
2026-01-05 00:34:32 -05:00
RickyRister
746f2af044
[DeckListModel] optimize by iterating over cardNodes instead of ExactCards (#6485)
* [DeckListModel] optimize by iterating over cardNodes instead of ExactCards

* fix build failure

* another optimization

* fix build failure
2026-01-03 19:19:04 -08:00
RickyRister
f16c552d97
[PrintingSelector] Don't refresh display if "bump cards to top" is off (#6486) 2026-01-04 01:08:39 +01:00
Bruno Alexandre Rosa
72a85b58cf
ci: make fat qt libs thin (#6281)
* ci: strip fat qt binaries

* parallelize

* cache thin qt

* print libs

* change qt install dir in the action

* move qt install logic to separate job

* lookup only

* debug: show contents of QTDIR

* enableCrossOsArchive also when saving

* check one dir up

* change install dir

* keep debugging

* try deleting cache

* force delete cache

* pass gh_token

* pass missing params

* use api

* change cache key, disable cross os archive

* move job directly to steps

* add comments

* set cache param directly

* address comments

* fixup

* Update .ci/thin_macos_qtlib.sh

* resolve qt version

* move resolution to separate script

* use single line for run:

* improve error handling in new scripts

---------

Co-authored-by: ebbit1q <ebbit1q@gmail.com>
2026-01-04 01:00:05 +01:00
transifex-integration[bot]
b88a98b09a
Translate cockatrice/cockatrice_en@source.ts in fr (#6488)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-03 18:58:59 +01:00
RickyRister
4fbb9d9682
[PrintingSelector] optimize amount calculation (#6478) 2026-01-03 01:04:56 -08:00
RickyRister
84aefda486
[DeckListModel] add getCardNodes method (#6484)
* [DeckListModel] add getCardNodes method

* Update one usage
2026-01-02 18:55:27 -08:00
BruebachL
73cc0541f5
[Game] Add shortcuts for same size and hand size - 1 mulligans (#6483)
Took 21 minutes

Took 3 seconds

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-03 00:03:11 +01:00
transifex-integration[bot]
bcf3939fee
Translate cockatrice/cockatrice_en@source.ts in fr (#6480) 2026-01-02 16:26:16 +01:00
tooomm
2e6f1128bb
Docs: Fix search help rendering and open external link in new tab (#6440)
* Use HTML link to open webpage in new tab

* Fix rendering for doxygen
2026-01-02 14:38:25 +01:00
462 changed files with 81396 additions and 54389 deletions

View file

@ -9,20 +9,18 @@ RUN apt-get update && \
file \
g++ \
git \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
libqt6svg6-dev \
libqt6websockets6-dev \
libqt5multimedia5-plugins \
libqt5sql5-mysql \
libqt5svg5-dev \
libqt5websockets5-dev \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt5-image-formats-plugins \
qtmultimedia5-dev \
qttools5-dev \
qttools5-dev-tools \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

View file

@ -0,0 +1,29 @@
FROM ubuntu:26.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
ccache \
clang-format \
cmake \
file \
g++ \
git \
libgl-dev \
liblzma-dev \
libmariadb-dev-compat \
libprotobuf-dev \
libqt6multimedia6 \
libqt6sql6-mysql \
ninja-build \
protobuf-compiler \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-svg-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt6-websockets-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

View file

@ -10,9 +10,11 @@
# --test runs tests
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
# --ccache [<size>] uses ccache and shows stats, optionally provide size
# --evict-ccache <age> runs ccache eviction based on given age after build
# --dir <dir> sets the name of the build dir, default is "build"
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
# --target-macos-version <version> sets the min os version - only used for macOS builds
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
# exitcode: 1 for failure, 3 for invalid arguments
@ -71,6 +73,15 @@ while [[ $# != 0 ]]; do
shift
fi
;;
'--evict-ccache')
shift
if [[ $# == 0 ]]; then
echo "::error file=$0::--evict-ccache expects an argument"
exit 3
fi
CCACHE_EVICTION_AGE=$1
shift
;;
'--vcpkg')
USE_VCPKG=1
shift
@ -84,6 +95,15 @@ while [[ $# != 0 ]]; do
BUILD_DIR="$1"
shift
;;
'--cmake-generator')
shift
if [[ $# == 0 ]]; then
echo "::error file=$0::--cmake-generator expects an argument"
exit 3
fi
export CMAKE_GENERATOR=$1
shift
;;
'--target-macos-version')
shift
if [[ $# == 0 ]]; then
@ -156,6 +176,18 @@ function ccachestatsverbose() {
# Compile
if [[ $RUNNER_OS == macOS ]]; then
# QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action,
# which sets a few environment variables
if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then
echo "found QTDIR at $QTDIR"
else
echo "could not find QTDIR!"
exit 2
fi
# the qtdir is located at Qt/[qtversion]/macos
# we use find to get the first subfolder with the name "macos"
# this works independent of the qt version as there should be only one version installed on the runner at a time
export QTDIR
if [[ $TARGET_MACOS_VERSION ]]; then
# CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version
@ -239,9 +271,16 @@ cmake --build . "${buildflags[@]}"
echo "::endgroup::"
if [[ $USE_CCACHE ]]; then
if [[ $CCACHE_EVICTION_AGE ]]; then
echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE"
ccache --evict-older-than "$CCACHE_EVICTION_AGE"
echo "::endgroup::"
fi
echo "::group::Show ccache stats again"
ccachestatsverbose
echo "::endgroup::"
elif [[ $CCACHE_EVICTION_AGE ]]; then
echo "::error file=$0::ccache eviction is enabled while ccache is disabled!"
fi
if [[ $RUNNER_OS == macOS ]]; then

View file

@ -3,17 +3,28 @@
# This script is to be used by the ci environment from the project root directory, do not use it from somewhere else.
# Creates or loads docker images to use in compilation, creates RUN function to start compilation on the docker image.
# <arg> sets the name of the docker image, these correspond to directories in .ci
#
# usage: source <script> <name> [--get] [--build] [--save] [--interactive] [--set-cache <location>]
# <name> sets the name of the docker image, these correspond to directories in .ci
# --get loads the image from a previously saved image cache, will build if no image is found
# --build builds the image from the Dockerfile in .ci/$NAME
# --save stores the image, if an image was loaded it will not be stored
# --interactive immediately starts the image interactively for debugging
# --set-cache <location> sets the location to cache the image or for ccache
#
# requires: docker
# uses env: NAME CACHE BUILD GET SAVE INTERACTIVE
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
#
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
# note that the docker container will not inherit any environment variables!
#
# usage: RUN [arguments for build script]
# roughly equivalent to `docker run $IMAGE_NAME bash $BUILD_SCRIPT $@`
# uses env: CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
# exitcode: 3 for invalid arguments, returns the returncode of docker run
export BUILD_SCRIPT=".ci/compile.sh"
project_name="cockatrice"
@ -41,12 +52,17 @@ while [[ $# != 0 ]]; do
shift
;;
'--set-cache')
CACHE=$2
shift
if [[ $# == 0 ]]; then
echo "--set-cache expects an argument" >&2
exit 3
fi
CACHE=$1
shift
if ! [[ -d $CACHE ]]; then
echo "could not find cache path: $CACHE" >&2
return 3
fi
shift 2
;;
*)
if [[ ${1:0:1} == - ]]; then
@ -149,10 +165,11 @@ function RUN ()
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
args+=(--env "CCACHE_DIR=/.ccache")
fi
if [[ -n "$CMAKE_GENERATOR" ]]; then
args+=(--env "CMAKE_GENERATOR=$CMAKE_GENERATOR")
if [[ $GITHUB_OUTPUT ]]; then
args+=(--mount "type=bind,source=$GITHUB_OUTPUT,target=/gh_output")
args+=(--env "GITHUB_OUTPUT=/gh_output")
fi
# shellcheck disable=2086
# shellcheck disable=2086
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
return $?
else

View file

@ -2,7 +2,6 @@
# used by the ci to rename build artifacts
# renames the file to [original name][SUFFIX].[original extension]
# where SUFFIX is either available in the environment or as the first arg
# if MAKE_ZIP is set instead a zip is made
# expected to be run in the build directory unless BUILD_DIR is set
# adds output to GITHUB_OUTPUT
builddir="${BUILD_DIR:=.}"
@ -22,8 +21,8 @@ set -e
# find file
found="$(find "$builddir" -maxdepth 1 -type f -name "$findrx" -print -quit)"
path="${found%/*}" # remove all after last /
file="${found##*/}" # remove all before last /
path="${found%/*}" # remove all including first "/" from right side
file="${found##*/}" # remove all including last "/" from left side
if [[ ! $file ]]; then
echo "::error file=$0::could not find package"
exit 1
@ -35,21 +34,16 @@ if ! cd "$path"; then
fi
# set filename
name="${file%.*}" # remove all after last .
new_name="$name$SUFFIX."
if [[ $MAKE_ZIP ]]; then
filename="${new_name}zip"
echo "creating zip '$filename' from '$file'"
zip "$filename" "$file"
else
extension="${file##*.}" # remove all before last .
filename="$new_name$extension"
echo "renaming '$file' to '$filename'"
mv "$file" "$filename"
fi
name="${file%.*}" # remove all including first "." from right side
new_name="$name$SUFFIX"
extension="${file##*.}" # remove all including last "." from left side
filename="$new_name.$extension"
echo "renaming '$file' to '$filename'"
mv "$file" "$filename"
cd "$oldpwd"
relative_path="$path/$filename"
ls -l "$relative_path"
echo "path=$relative_path" >>"$GITHUB_OUTPUT"
echo "name=$filename" >>"$GITHUB_OUTPUT"
echo "name=$new_name" >>"$GITHUB_OUTPUT"
echo "fullname=$filename" >>"$GITHUB_OUTPUT"

View file

@ -10,7 +10,6 @@ Available pre-compiled binaries for installation:
<b>Windows</b>
<kbd>Windows 10+</kbd>
<kbd>Windows 7+</kbd>
<b>macOS</b>
<kbd>macOS 15+</kbd> <sub><i>Sequoia</i></sub> <sub>Apple M</sub>
@ -18,6 +17,7 @@ Available pre-compiled binaries for installation:
<kbd>macOS 13+</kbd> <sub><i>Ventura</i></sub> <sub>Intel</sub>
<b>Linux</b>
<kbd>Ubuntu 26.04 LTS</kbd> <sub><i>Resolute Racoon</i></sub>
<kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
<kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
<kbd>Debian 13</kbd> <sub><i>Trixie</i></sub>
@ -28,6 +28,8 @@ Available pre-compiled binaries for installation:
<sub>We are also packaged in <kbd>Arch Linux</kbd>'s <a href="https://archlinux.org/packages/extra/x86_64/cockatrice">official extra repository</a>, courtesy of @FFY00.</sub>
<sub>General Linux support is available via a <kbd>flatpak</kbd> package at <a href="https://flathub.org/apps/io.github.Cockatrice.cockatrice">Flathub</a>!</sub>
<sub>We provide a <kbd>Docker</kbd> image for "Servatrice" in <a href="https://github.com/Cockatrice/Cockatrice/pkgs/container/servatrice">GHCR</a>. You can docker pull it or use our Docker Compose files!</sub>
</pre>

View file

@ -0,0 +1,49 @@
#!/bin/bash
# This script is used to resolve the latest patch version of Qt using aqtinstall.
# It interprets wildcards to get the latest patch version. E.g. "6.6.*" -> "6.6.3".
# This script is meant to be used by the ci enironment.
# It uses the runner's GITHUB_OUTPUT env variable.
# Usage example: .ci/resolve_latest_aqt_qt_version.sh "6.6.*"
qt_spec=$1
if [[ ! $qt_spec ]]; then
echo "usage: $0 [version]"
exit 2
fi
# If version is already specific (no wildcard), use it as-is
if [[ $qt_spec != *"*" ]]; then
echo "version $qt_spec is already resolved"
echo "version=$qt_spec" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! hash aqt; then
echo "aqt could not be found, has aqtinstall been installed?"
exit 2
fi
# Resolve latest patch
if [[ $RUNNER_OS == macOS ]]; then
if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then
exit 1
fi
elif [[ $RUNNER_OS == Windows ]]; then
if ! qt_resolved=$(aqt list-qt windows desktop --spec "$qt_spec" --latest-version); then
exit 1
fi
else
echo "aqt command for $RUNNER_OS not defined."
exit 1
fi
echo "resolved $qt_spec to $qt_resolved"
if [[ ! $qt_resolved ]]; then
echo "Error: Could not resolve Qt version for $qt_spec"
exit 1
fi
echo "version=$qt_resolved" >> "$GITHUB_OUTPUT"

25
.ci/thin_macos_qtlib.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# The macos binaries from aqt are fat (universal), so we thin them to the target architecture to reduce the size of
# the packages and caches using lipo.
# This script is meant to be used by the ci enironment on macos runners only.
# It uses the runner's GITHUB_WORKSPACE env variable.
arch=$(uname -m)
nproc=$(sysctl -n hw.ncpu)
function thin() {
local libfile=$1
if [[ $(file -b --mime-type "$libfile") == application/x-mach-binary* ]]; then
echo "Processing $libfile"
lipo "$libfile" -thin "$arch" -output "$libfile"
fi
return 0
}
export -f thin # export to allow use in xargs
export arch
set -eo pipefail
echo "::group::Thinning Qt libraries to $arch using $nproc cores"
find "$GITHUB_WORKSPACE/Qt" -type f -print0 | xargs -0 -n1 -P"$nproc" -I{} bash -c "thin '{}'"
echo "::endgroup::"

View file

@ -461,7 +461,11 @@ revoke the tag by doing the following:
git push --delete upstream $TAG_NAME
git tag -d $TAG_NAME
```
You can also do this on GitHub, you'll also want to delete the false release.
You can also do this on GitHub.
> [!NOTE]
> If you want to push a new release to replace it immediately with the same
> name you have to delete the automatically created release first!
In the first lines of [CMakeLists.txt](
https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt)

View file

@ -1,9 +1,12 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Discord Community (Get help with server issues, e.g. Login)
url: https://discord.gg/3Z9yzmA
url: https://discord.com/invite/3Z9yzmA
about: Need help with using the client? Want to find some games? Try the Discord server!
- name: 🌐 Translations (Help improve the localization of the app)
url: https://explore.transifex.com/cockatrice/cockatrice/
# it is not possible to add a link to the wiki to this description
about: For more information and guidance check our Translation FAQ on our wiki!
- name: 📖 Code Documentation
url: https://cockatrice.github.io/docs/
about: Helpful source focusing on developers, but there are also references for users!

View file

@ -2,19 +2,21 @@
version: 2
updates:
# # Enable version updates for git submodules
# Not yet possible to bump only on tags or releases, see:
# Enable version updates for git submodules
# If SemVer is used, updates will happen to new releases only (not HEAD)
# https://github.com/dependabot/dependabot-core/issues/1639
# https://github.com/dependabot/dependabot-core/issues/2192
# Alternative: Action that updates submodule and can be manually run on demand (workflow_dispatch)
# - package-ecosystem: "gitsubmodule"
# # Look for `.gitmodules` in the `root` directory
# directory: "/"
# # Check for updates once a month
# schedule:
# interval: "monthly"
# # Limit the amout of open PR's (default = 5, disabled = 0, security updates are not impacted)
# open-pull-requests-limit: 1
- package-ecosystem: "gitsubmodule"
# Look for `.gitmodules` in the `root` directory
directory: "/"
ignore:
# Ignore updates for vcpkg (Bump to latest tag not working (no SemVer used) & macOS Intel triplet broken with newer releases)
- dependency-name: "vcpkg"
# Check for updates once a month
schedule:
interval: "monthly"
# Limit the amout of open PR's (default = 5, disabled = 0, security updates are not impacted)
open-pull-requests-limit: 2
# # Enable version updates for Docker
# Not yet possible to bump from one LTS version to the next and skip others, see:

View file

@ -38,15 +38,15 @@ on:
- 'vcpkg.json'
- 'vcpkg'
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on master)
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
concurrency:
group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.ref_name != 'master' }}
cancel-in-progress: ${{ github.ref_type != 'tag' }}
jobs:
configure:
name: Configure
runs-on: ubuntu-latest
runs-on: ubuntu-slim
outputs:
tag: ${{steps.configure.outputs.tag}}
sha: ${{steps.configure.outputs.sha}}
@ -142,21 +142,28 @@ jobs:
- distro: Ubuntu
version: 22.04
package: DEB
test: skip # Running tests on all distros is superfluous
- distro: Ubuntu
version: 24.04
package: DEB
- distro: Ubuntu
version: 26.04
package: DEB
name: ${{matrix.distro}} ${{matrix.version}}
needs: configure
runs-on: ubuntu-latest
continue-on-error: ${{matrix.allow-failure == 'yes'}}
timeout-minutes: 70
env:
NAME: ${{matrix.distro}}${{matrix.version}}
CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache
# Cache size over the entire repo is 10Gi:
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 500M
CCACHE_SIZE: 550M
CCACHE_EVICTION_AGE: 7d
CMAKE_GENERATOR: 'Ninja'
steps:
@ -180,29 +187,45 @@ jobs:
- name: Build debug and test
if: matrix.test != 'skip'
shell: bash
env:
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
run: |
source .ci/docker.sh
RUN --server --debug --test --ccache "$CCACHE_SIZE"
RUN --server --debug --test --ccache "$CCACHE_SIZE" \
--cmake-generator "$CMAKE_GENERATOR"
- name: Build release package
id: build
if: matrix.package != 'skip'
shell: bash
env:
BUILD_DIR: build
SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
package: '${{matrix.package}}'
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
NO_CLIENT: ${{matrix.server_only == 'yes' && '--no-client' || '' }}
server_only: '${{matrix.server_only}}'
run: |
source .ci/docker.sh
RUN --server --release --package "$package" --dir "$BUILD_DIR" \
--ccache "$CCACHE_SIZE" $NO_CLIENT
.ci/name_build.sh
args=()
if [[ $server_only == yes ]]; then
args+=(--no-client)
fi
if [[ $GITHUB_REF == "refs/heads/master" ]]; then
args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
fi
args+=(--ccache "$CCACHE_SIZE")
args+=(--cmake-generator "$CMAKE_GENERATOR")
args+=(--suffix "$SUFFIX")
RUN --server --release --package "$package" "${args[@]}"
- name: Save compiler cache (ccache)
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: Delete remote compiler cache (ccache)
if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then
echo "Cache deleted successfully"
fi
- name: Save updated compiler cache (ccache)
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v5
with:
@ -212,10 +235,10 @@ jobs:
- name: Upload artifact
id: upload_artifact
if: matrix.package != 'skip'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{matrix.distro}}${{matrix.version}}-package
path: ${{steps.build.outputs.path}}
archive: false
if-no-files-found: error
- name: Upload to release
@ -225,24 +248,24 @@ jobs:
env:
GH_TOKEN: ${{github.token}}
tag_name: ${{needs.configure.outputs.tag}}
asset_name: ${{steps.build.outputs.fullname}}
asset_path: ${{steps.build.outputs.path}}
asset_name: ${{steps.build.outputs.name}}
run: gh release upload "$tag_name" "$asset_path#$asset_name"
- name: Attest binary provenance
id: attestation
if: steps.upload_release.outcome == 'success'
uses: actions/attest-build-provenance@v3
uses: actions/attest@v4
with:
subject-name: ${{steps.build.outputs.name}}
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
subject-path: ${{steps.build.outputs.path}}
show-summary: false
- name: Verify binary attestation
if: steps.attestation.outcome == 'success'
shell: bash
env:
GH_TOKEN: ${{github.token}}
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
run: gh attestation verify ${{steps.build.outputs.path}} --repo Cockatrice/Cockatrice
build-vcpkg:
strategy:
@ -258,13 +281,12 @@ jobs:
override_target: 13
make_package: 1
package_suffix: "-macOS13_Intel"
artifact_name: macOS13_Intel-package
qt_version: 6.6.*
qt_version: 6.11.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false # qt caches take too much space for macOS (1.1Gi)
cmake_generator: Ninja
use_ccache: 1
ccache_eviction_age: 7d
- os: macOS
target: 14
@ -274,13 +296,12 @@ jobs:
type: Release
make_package: 1
package_suffix: "-macOS14"
artifact_name: macOS14-package
qt_version: 6.6.*
qt_version: 6.11.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
ccache_eviction_age: 7d
- os: macOS
target: 15
@ -290,13 +311,12 @@ jobs:
type: Release
make_package: 1
package_suffix: "-macOS15"
artifact_name: macOS15-package
qt_version: 6.6.*
qt_version: 6.11.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
ccache_eviction_age: 7d
- os: macOS
target: 15
@ -304,48 +324,34 @@ jobs:
soc: Apple
xcode: "16.4"
type: Debug
qt_version: 6.6.*
qt_version: 6.11.*
qt_arch: clang_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: false
cmake_generator: Ninja
use_ccache: 1
- os: Windows
target: 7
runner: windows-2022
type: Release
make_package: 1
package_suffix: "-Win7"
artifact_name: Windows7-installer
qt_version: 5.15.*
qt_arch: win64_msvc2019_64
cache_qt: true
cmake_generator: "Visual Studio 17 2022"
cmake_generator_platform: x64
ccache_eviction_age: 7d
- os: Windows
target: 10
runner: windows-2022
runner: windows-2025
type: Release
make_package: 1
package_suffix: "-Win10"
artifact_name: Windows10-installer
qt_version: 6.6.*
qt_arch: win64_msvc2019_64
qt_version: 6.11.*
qt_arch: win64_msvc2022_64
qt_modules: qtimageformats qtmultimedia qtwebsockets
cache_qt: true
cmake_generator: "Visual Studio 17 2022"
cmake_generator_platform: x64
name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
needs: configure
runs-on: ${{matrix.runner}}
timeout-minutes: 100
env:
CCACHE_DIR: ${{github.workspace}}/.cache/
# Cache size over the entire repo is 10Gi:
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 500M
CCACHE_SIZE: 550M
steps:
- name: Checkout
@ -356,7 +362,7 @@ jobs:
- name: Add msbuild to PATH
if: matrix.os == 'Windows'
id: add-msbuild
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: x64
@ -375,13 +381,61 @@ jobs:
key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-
- name: Install Qt ${{matrix.qt_version}}
- name: Install aqtinstall
run: pipx install aqtinstall
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
- name: Resolve latest Qt patch version
id: resolve_qt_version
shell: bash
run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}"
- name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS'
id: restore_qt
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/Qt
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
# Using jurplel/install-qt-action to install Qt without using brew
# qt build using vcpkg either just fails or takes too long to build
- name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4
with:
version: ${{matrix.qt_version}}
version: ${{ steps.resolve_qt_version.outputs.version }}
arch: ${{matrix.qt_arch}}
modules: ${{matrix.qt_modules}}
cache: ${{matrix.cache_qt}}
cache: false
dir: ${{github.workspace}}
- name: Thin Qt libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
run: .ci/thin_macos_qtlib.sh
- name: Cache thin Qt libraries (${{ matrix.soc }} macOS)
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ github.workspace }}/Qt
key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
- name: Install Qt ${{matrix.qt_version}} (Windows)
if: matrix.os == 'Windows'
uses: jurplel/install-qt-action@v4
with:
# qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
aqtsource: git+https://github.com/miurahr/aqtinstall.git
version: ${{ steps.resolve_qt_version.outputs.version }}
arch: ${{matrix.qt_arch}}
modules: ${{matrix.qt_modules}}
cache: true
- name: Install NSIS
if: matrix.os == 'Windows'
shell: bash
run: choco install nsis
- name: Setup vcpkg cache
id: vcpkg-cache
@ -409,19 +463,30 @@ jobs:
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
DEVELOPER_DIR: '/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer'
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
run: .ci/compile.sh --server --test --vcpkg
- name: Save compiler cache (ccache)
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: Delete remote compiler cache (ccache)
if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then
echo "Cache deleted successfully"
fi
- name: Save updated compiler cache (ccache)
if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1
uses: actions/cache/save@v5
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
path: ${{env.CCACHE_DIR}}
key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
- name: Sign app bundle
if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null
id: sign_macos
env:
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
@ -433,7 +498,7 @@ jobs:
fi
- name: Notarize app bundle
if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
if: steps.sign_macos.outcome == 'success'
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
@ -465,46 +530,47 @@ jobs:
fi
- name: Upload artifact
id: upload_artifact
if: matrix.make_package
uses: actions/upload-artifact@v6
id: upload_artifact
uses: actions/upload-artifact@v7
with:
name: ${{matrix.artifact_name}}
path: ${{steps.build.outputs.path}}
archive: false
if-no-files-found: error
- name: Upload pdb database
if: matrix.os == 'Windows'
uses: actions/upload-artifact@v6
- name: Upload PDBs (Program Databases)
if: matrix.os == 'Windows' && github.ref_type != 'tag'
uses: actions/upload-artifact@v7
with:
name: Windows${{matrix.target}}-debug-pdbs
name: ${{steps.build.outputs.name}}-PDBs
path: |
build/cockatrice/Release/*.pdb
build/oracle/Release/*.pdb
build/servatrice/Release/*.pdb
if-no-files-found: error
- name: Upload to release
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
id: upload_release
if: needs.configure.outputs.tag != null
shell: bash
env:
GH_TOKEN: ${{github.token}}
tag_name: ${{needs.configure.outputs.tag}}
asset_name: ${{steps.build.outputs.fullname}}
asset_path: ${{steps.build.outputs.path}}
asset_name: ${{steps.build.outputs.name}}
run: gh release upload "$tag_name" "$asset_path#$asset_name"
- name: Attest binary provenance
id: attestation
if: steps.upload_release.outcome == 'success'
uses: actions/attest-build-provenance@v3
id: attestation
uses: actions/attest@v4
with:
subject-name: ${{steps.build.outputs.name}}
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
subject-path: ${{steps.build.outputs.path}}
show-summary: false
- name: Verify binary attestation
if: steps.attestation.outcome == 'success'
shell: bash
env:
GH_TOKEN: ${{github.token}}
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
run: gh attestation verify ${{steps.build.outputs.path}} --repo Cockatrice/Cockatrice

View file

@ -20,13 +20,13 @@ on:
jobs:
format:
runs-on: ubuntu-22.04
runs-on: ubuntu-slim
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 20 # should be enough to find merge base
fetch-depth: 20 # should be enough to find merge base
- name: Install dependencies
shell: bash

View file

@ -13,6 +13,11 @@ on:
- '.github/workflows/docker-release.yml'
- 'Dockerfile'
# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release)
concurrency:
group: "${{ github.workflow }} @ ${{ github.ref_name }}"
cancel-in-progress: ${{ github.ref_type != 'tag' }}
jobs:
docker:
name: amd64 & arm64
@ -27,7 +32,7 @@ jobs:
- name: Docker metadata
id: metadata
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
ghcr.io/cockatrice/servatrice
@ -41,26 +46,27 @@ jobs:
org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
if: github.ref_type == 'tag'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
annotations: ${{ steps.metadata.outputs.annotations }}
cache-from: type=gha,scope=servatrice
cache-to: type=gha,mode=max,scope=servatrice

View file

@ -22,6 +22,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Graphviz
run: |
@ -29,7 +31,7 @@ jobs:
dot -V
- name: Install Doxygen
uses: ssciwr/doxygen-install@v1
uses: ssciwr/doxygen-install@v2
with:
version: "1.14.0"

View file

@ -16,7 +16,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice'
name: Pull languages
runs-on: ubuntu-latest
runs-on: ubuntu-slim
steps:
- name: Checkout repo

View file

@ -16,7 +16,7 @@ jobs:
if: github.event_name != 'schedule' || github.repository_owner == 'Cockatrice'
name: Push strings
runs-on: ubuntu-latest
runs-on: ubuntu-slim
steps:
- name: Checkout repo
@ -46,7 +46,7 @@ jobs:
- name: Render template
id: template
uses: chuhlomin/render-template@v1
uses: chuhlomin/render-template/binary@v1
with:
template: .ci/update_translation_source_strings_template.md
vars: |

View file

@ -10,7 +10,7 @@ on:
jobs:
ESLint:
runs-on: ubuntu-latest
runs-on: ubuntu-slim
defaults:
run:

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "vcpkg"]
path = vcpkg
url = https://github.com/microsoft/vcpkg.git
[submodule "doxygen-awesome-css"]
path = doc/doxygen/theme
url = https://github.com/jothepro/doxygen-awesome-css.git

View file

@ -74,11 +74,11 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
project("Cockatrice" VERSION 2.11.0)
project("Cockatrice" VERSION 3.0.0)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
set(GIT_TAG_RELEASENAME "Omenpath")
set(GIT_TAG_RELEASENAME "Graduation Day")
endif()
# Use c++20 for all targets
@ -254,7 +254,9 @@ endif()
set(CPACK_PACKAGE_CONTACT "Zach Halpern <zach@cockatrice.us>")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_NAME}")
set(CPACK_PACKAGE_VENDOR "Cockatrice Development Team")
set(CPACK_PACKAGE_DESCRIPTION_FILE "${PROJECT_SOURCE_DIR}/README.md")
set(CPACK_PACKAGE_DESCRIPTION
"Cockatrice is an open-source, multiplatform application for playing tabletop card games over a network. The program's server design prevents users from manipulating the game for unfair advantage. The client also provides a single-player mode, which allows users to brew while offline."
)
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}")
@ -328,7 +330,12 @@ include(CPack)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_interfaces ${CMAKE_BINARY_DIR}/libcockatrice_interfaces)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_protocol ${CMAKE_BINARY_DIR}/libcockatrice_protocol)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_network ${CMAKE_BINARY_DIR}/libcockatrice_network)
if(WITH_CLIENT
OR WITH_SERVER
OR WITH_ORACLE
)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_network ${CMAKE_BINARY_DIR}/libcockatrice_network)
endif()
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_deck_list ${CMAKE_BINARY_DIR}/libcockatrice_deck_list)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_rng ${CMAKE_BINARY_DIR}/libcockatrice_rng)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_card ${CMAKE_BINARY_DIR}/libcockatrice_card)

View file

@ -54,7 +54,7 @@ PROJECT_NUMBER = $(COCKATRICE_REF)
# for a project that appears at the top of each page and should give viewers a
# quick idea about the purpose of the project. Keep the description short.
PROJECT_BRIEF = "A cross-platform virtual tabletop for multiplayer card games"
PROJECT_BRIEF = "A virtual tabletop for multiplayer card games"
# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
# in the documentation. The maximum height of the logo should not exceed 55
@ -1068,6 +1068,8 @@ RECURSIVE = YES
EXCLUDE = build/ \
cmake/ \
doc/doxygen/theme/docs/ \
doc/doxygen/theme/include/ \
vcpkg/ \
webclient/
@ -1430,7 +1432,9 @@ HTML_STYLESHEET =
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET = doc/doxygen/css/doxygen_style.css
HTML_EXTRA_STYLESHEET = doc/doxygen/theme/doxygen-awesome.css \
doc/doxygen/css/hide_nav_sync.css \
doc/doxygen/css/cockatrice_docs_style.css
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
@ -1453,7 +1457,7 @@ HTML_EXTRA_FILES = doc/doxygen/js/graph_toggle.js
# The default value is: AUTO_LIGHT.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE = AUTO_DARK
HTML_COLORSTYLE = LIGHT
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to
@ -1764,7 +1768,7 @@ ECLIPSE_DOC_ID = org.doxygen.Project
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
DISABLE_INDEX = YES
DISABLE_INDEX = NO
# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
# structure should be generated to display hierarchical information. If the tag

View file

@ -52,7 +52,7 @@ Latest <kbd>beta</kbd> version:
# Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA)
Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out.
Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other project contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out.
- [Official Website](https://cockatrice.github.io)
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
- [Official Discord](https://discord.gg/3Z9yzmA)
@ -109,7 +109,7 @@ Cockatrice tries to use the [Google Developer Documentation Style Guide](https:/
Cockatrice uses Transifex to manage translations. You can help us bring <kbd>Cockatrice</kbd>, <kbd>Oracle</kbd> and <kbd>Webatrice</kbd> to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).<br>
Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting invovled, and join a group of hundreds of others!<br>
Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting involved, and join a group of hundreds of others!<br>
# Build [![CI Desktop](https://github.com/Cockatrice/Cockatrice/actions/workflows/desktop-build.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/desktop-build.yml?query=branch%3Amaster+event%3Apush) [![CI Docker](https://github.com/Cockatrice/Cockatrice/actions/workflows/docker-release.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/docker-release.yml?query=branch%3Amaster+event%3Apush) [![CI Web](https://github.com/Cockatrice/Cockatrice/actions/workflows/web-build.yml/badge.svg?branch=master&event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/web-build.yml?query=branch%3Amaster+event%3Apush)

View file

@ -29,7 +29,9 @@ if(WITH_ORACLE)
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
endif()
if(TEST)
set(_TEST_NEEDED Widgets)
# Union of Qt modules required across all test targets (independent of application targets).
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
set(_TEST_NEEDED Concurrent Network Svg Widgets)
endif()
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
@ -40,7 +42,7 @@ list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS)
if(NOT FORCE_USE_QT5)
# Linguist is now a component in Qt6 instead of an external package
find_package(
Qt6 6.2.3
Qt6 6.4.2
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
QUIET HINTS ${Qt6_DIR}
)

View file

@ -11,6 +11,7 @@ SetCompressor LZMA
Var NormalDestDir
Var PortableDestDir
Var PortableMode
Var ReinstallMode
!include LogicLib.nsh
!include FileFunc.nsh
@ -26,14 +27,25 @@ Var PortableMode
!define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of Cockatrice.$\r$\n$\r$\nClick Next to continue."
!define MUI_FINISHPAGE_RUN "$INSTDIR/cockatrice.exe"
!define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now"
!define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_WELCOME
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE"
Page Custom PortableModePageCreate PortableModePageLeave
!define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre
!insertmacro MUI_PAGE_COMPONENTS
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_DIRECTORY
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_INSTFILES
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
@ -72,6 +84,7 @@ ${IfNot} ${Errors}
MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\
/PORTABLE : Install in portable mode$\n\
/S : Silent install$\n\
/R : Silent upgrade$\n\
/D=%directory% : Specify destination directory$\n"
Quit
${EndIf}
@ -89,6 +102,16 @@ ${Else}
${EndIf}
${EndIf}
ClearErrors
${GetOptions} $9 "/R" $8
${IfNot} ${Errors}
StrCpy $ReinstallMode 1
SetSilent silent
SetAutoClose true
${Else}
StrCpy $ReinstallMode 0
${EndIf}
${If} $InstDir == ""
; User did not use /D to specify a directory,
; we need to set a default based on the install mode
@ -96,6 +119,22 @@ ${If} $InstDir == ""
${EndIf}
Call SetModeDestinationFromInstdir
; --- Detect portable install when using /R ---
${If} $ReinstallMode = 1
IfFileExists "$InstDir\portable.dat" 0 not_portable
StrCpy $PortableMode 1
Goto portable_done
not_portable:
StrCpy $PortableMode 0
portable_done:
${EndIf}
${If} $ReinstallMode = 1
Call AutoUninstallIfNeeded
${EndIf}
FunctionEnd
Function un.onInit
@ -125,8 +164,46 @@ ${Else}
${EndIf}
FunctionEnd
Function SkipIfReinstall
${If} $ReinstallMode = 1
Abort
${EndIf}
FunctionEnd
Function AutoUninstallIfNeeded
SetShellVarContext all
; --- 32-bit uninstall ---
SetRegView 32
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
StrCmp $R0 "" done32
DetailPrint "Removing previous version (32-bit)..."
ExecWait '$R0'
done32:
; --- 64-bit uninstall ---
${If} ${RunningX64}
SetRegView 64
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
StrCmp $R0 "" done64
DetailPrint "Removing previous version (64-bit)..."
ExecWait '$R0'
done64:
${EndIf}
FunctionEnd
Function PortableModePageCreate
${If} $ReinstallMode = 1
Abort
${EndIf}
Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory
!insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice."
nsDialogs::Create 1018
@ -158,6 +235,11 @@ ${EndIf}
FunctionEnd
Function componentsPagePre
${If} $ReinstallMode = 1
Return
${EndIf}
${If} $PortableMode = 0
SetShellVarContext all
@ -167,8 +249,12 @@ ${If} $PortableMode = 0
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
StrCmp $R0 "" done32
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
Abort
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
Abort
${Else}
Goto uninst32
${EndIf}
uninst32:
ClearErrors
@ -183,8 +269,12 @@ ${If} $PortableMode = 0
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
StrCmp $R0 "" done64
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
Abort
${If} $ReinstallMode = 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
Abort
${Else}
Goto uninst64
${EndIf}
uninst64:
ClearErrors
@ -276,6 +366,12 @@ ${Else}
FileWrite $0 "PORTABLE"
FileClose $0
${EndIf}
${If} $ReinstallMode = 1
IfFileExists "$INSTDIR\cockatrice.exe" 0 +2
Exec '"$INSTDIR\cockatrice.exe"'
${EndIf}
SectionEnd
Section "Start menu item" SecStartMenu

View file

@ -39,6 +39,7 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
src/interface/widgets/dialogs/dlg_local_game_options.cpp
src/interface/widgets/dialogs/dlg_manage_sets.cpp
src/interface/widgets/dialogs/dlg_register.cpp
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
@ -47,6 +48,7 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
src/interface/widgets/dialogs/dlg_update.cpp
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
src/filters/deck_filter_string.cpp
src/filters/filter_builder.cpp
@ -170,6 +172,7 @@ set(cockatrice_SOURCES
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
@ -177,6 +180,7 @@ set(cockatrice_SOURCES
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
src/interface/widgets/deck_editor/deck_state_manager.cpp
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
src/interface/widgets/general/background_sources.cpp
src/interface/widgets/general/display/background_plate_widget.cpp
src/interface/widgets/general/display/banner_widget.cpp
@ -202,6 +206,7 @@ set(cockatrice_SOURCES
src/interface/widgets/printing_selector/printing_selector.cpp
src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp
src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp
src/interface/widgets/printing_selector/printing_selector_card_search_widget.cpp
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
@ -225,8 +230,11 @@ set(cockatrice_SOURCES
src/interface/widgets/utility/custom_line_edit.cpp
src/interface/widgets/utility/get_text_with_max.cpp
src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/utility/visibility_change_listener.cpp
src/interface/widgets/utility/visibility_change_listener.h
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp
@ -235,6 +243,7 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp
src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp
src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp
src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
@ -555,6 +564,9 @@ if(WIN32)
PATTERN "styles/qopensslbackend.dll"
PATTERN "styles/qschannelbackend.dll"
PATTERN "styles/qwindowsvistastyle.dll"
PATTERN "styles/qwindows11style.dll"
PATTERN "styles/qmodernwindowsstyle.dll"
PATTERN "styles/qmodernwindowsstyled.dll"
PATTERN "tls/qcertonlybackend.dll"
PATTERN "tls/qopensslbackend.dll"
PATTERN "tls/qschannelbackend.dll"

View file

@ -15,18 +15,24 @@
<file>resources/icons/arrow_top_green.svg</file>
<file>resources/icons/arrow_up_green.svg</file>
<file>resources/icons/arrow_undo.svg</file>
<file>resources/icons/circle_half_stroke.svg</file>
<file>resources/icons/clearsearch.svg</file>
<file>resources/icons/cogwheel.svg</file>
<file>resources/icons/conceded.svg</file>
<file>resources/icons/decrement.svg</file>
<file>resources/icons/delete.svg</file>
<file>resources/icons/dragon.svg</file>
<file>resources/icons/dropdown_collapsed.svg</file>
<file>resources/icons/dropdown_expanded.svg</file>
<file>resources/icons/filter.svg</file>
<file>resources/icons/floppy_disk.svg</file>
<file>resources/icons/forgot_password.svg</file>
<file>resources/icons/gear.svg</file>
<file>resources/icons/increment.svg</file>
<file>resources/icons/info.svg</file>
<file>resources/icons/lock.svg</file>
<file>resources/icons/not_ready_start.svg</file>
<file>resources/icons/pen_to_square.svg</file>
<file>resources/icons/pencil.svg</file>
<file>resources/icons/pin.svg</file>
<file>resources/icons/player.svg</file>
@ -34,10 +40,13 @@
<file>resources/icons/reload.svg</file>
<file>resources/icons/remove_row.svg</file>
<file>resources/icons/rename.svg</file>
<file>resources/icons/scale_balanced.svg</file>
<file>resources/icons/scales.svg</file>
<file>resources/icons/scroll.svg</file>
<file>resources/icons/search.svg</file>
<file>resources/icons/settings.svg</file>
<file>resources/icons/share.svg</file>
<file>resources/icons/sort_arrow_down.svg</file>
<file>resources/icons/spectator.svg</file>
<file>resources/icons/swap.svg</file>
<file>resources/icons/sync.svg</file>
@ -52,6 +61,8 @@
<file>resources/icons/mana/W.svg</file>
<file>resources/backgrounds/home.png</file>
<file>resources/backgrounds/card_triplet.svg</file>
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
<file>resources/config/general.svg</file>
<file>resources/config/appearance.svg</file>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="250"
id="svg13"
height="231.66667"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg">
<defs
id="defs13" />
<g
transform="matrix(1.705559,0,0,1.705559,-18.310328,-4.2419088)"
id="g13">
<path
d="M 90.069854,3.479957 C 89.356513,1.2235709 86.980392,-0.01102897 84.723451,0.70218215 L 3.4767601,26.377781 C 1.2199188,27.090982 -0.01486587,29.46663 0.69839437,31.723116 L 33.512365,135.52112 c 0.713341,2.25639 3.089462,3.49099 5.346403,2.77777 l 81.246672,-25.6756 c 2.25684,-0.71319 3.49163,-3.08884 2.77837,-5.34533 L 90.074852,3.479957 Z"
style="display:none;fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path1" />
<path
d="m 110.61293,7.4983294 c -0.36657,-2.337853 -2.53055,-3.9150142 -4.86886,-3.5484627 L 21.563382,17.14452 c -2.338314,0.366502 -3.915784,2.529976 -3.549207,4.867929 L 34.876507,129.55893 c 0.366577,2.33786 2.530549,3.91502 4.868863,3.54847 l 84.18069,-13.19466 c 2.33831,-0.3665 3.91578,-2.52997 3.5492,-4.86793 L 110.61093,7.4983294 Z"
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path4" />
<path
d="m 130.53623,15.555064 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 41.067426 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 124.41102 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208344 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path7" />
<path
d="m 149.43988,26.480639 c 0.38018,-2.335754 -1.1846,-4.508374 -3.52082,-4.88852 L 61.817351,7.9076636 C 59.481136,7.5275576 57.308066,9.0920839 56.927894,11.427736 L 39.439773,118.87426 c -0.380182,2.33576 1.184602,4.50838 3.520816,4.88852 l 84.102711,13.68346 c 2.33622,0.38011 4.50929,-1.18442 4.88946,-3.52007 L 149.43688,26.479639 Z"
style="display:inline;fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.-->
<svg
version="1.1"
width="172.65051"
id="svg13"
height="213.30714"
xml:space="preserve"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"><defs
id="defs13" /><g
transform="matrix(1.705559,0,0,1.705559,-97.653345,-68.741256)"
id="g13"><path
d="m 151.48519,45.063813 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 62.016385 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 153.91977 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208345 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path7" /><path
d="m 154.70135,48.441704 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 65.232545 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 V 157.29767 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208345 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path7-5" /><path
d="m 157.98403,51.75453 c 0,-2.366441 -1.89356,-4.259575 -4.26046,-4.259575 H 68.515228 c -2.366905,0 -4.260468,1.893134 -4.260468,4.259575 v 108.85596 c 0,2.36644 1.893563,4.25957 4.260468,4.25957 h 85.208342 c 2.3669,0 4.26046,-1.89313 4.26046,-4.25957 z"
style="fill:#c0c0c0;fill-opacity:1;stroke:#989898;stroke-width:1;stroke-opacity:1"
id="path7-6" /></g><path
d="m 196.24576,207.42361 c 0,0.11213 0,0.22413 0,0.33621 -0.0498,4.54511 -4.18399,7.63329 -8.72909,7.63329 h -12.19086 c -3.29988,0 -5.97713,2.67727 -5.97713,5.97712 0,0.4234 0.0498,0.83433 0.12449,1.23279 0.26149,1.27014 0.80939,2.49046 1.34485,3.72325 0.75959,1.71843 1.50674,3.4244 1.50674,5.22998 0,3.95986 -2.68971,7.55859 -6.64956,7.72046 -0.43583,0.0128 -0.87166,0.025 -1.31995,0.025 -17.60761,0 -31.878,-14.27038 -31.878,-31.878 0,-17.60761 14.28284,-31.878 31.89046,-31.878 17.60762,0 31.87801,14.27039 31.87801,31.878 z m -47.81703,3.98475 c 0,-2.20407 -1.78067,-3.98475 -3.98473,-3.98475 -2.20407,0 -3.98477,1.78068 -3.98477,3.98475 0,2.20406 1.7807,3.98475 3.98477,3.98475 2.20406,0 3.98473,-1.78069 3.98473,-3.98475 z m 0,-11.95426 c 2.20407,0 3.98477,-1.78068 3.98477,-3.98475 0,-2.20407 -1.7807,-3.98475 -3.98477,-3.98475 -2.20405,0 -3.98473,1.78068 -3.98473,3.98475 0,2.20407 1.78068,3.98475 3.98473,3.98475 z m 19.92376,-11.95424 c 0,-2.20408 -1.78068,-3.98477 -3.98473,-3.98477 -2.20407,0 -3.98477,1.78069 -3.98477,3.98477 0,2.20405 1.7807,3.98474 3.98477,3.98474 2.20405,0 3.98473,-1.78069 3.98473,-3.98474 z m 11.95426,11.95424 c 2.20407,0 3.98475,-1.78068 3.98475,-3.98475 0,-2.20407 -1.78068,-3.98475 -3.98475,-3.98475 -2.20406,0 -3.98475,1.78068 -3.98475,3.98475 0,2.20407 1.78069,3.98475 3.98475,3.98475 z"
id="path1"
style="display:none;fill:#3b3b3b;fill-opacity:1;stroke:#000000;stroke-width:2.53798;stroke-dasharray:none;stroke-opacity:1" /><path
d="M 126.20915,54.574783 82.324247,83.8512 c -5.76807,3.845383 -9.435059,10.089163 -10.029703,16.90777 12.348823,2.53716 22.081191,12.26955 24.638191,24.63819 6.838435,-0.59465 13.062395,-4.26163 16.907775,-10.0297 l 29.2566,-43.904722 c 1.32804,-2.001984 2.04162,-4.340923 2.04162,-6.75915 0,-6.719506 -5.45092,-12.170428 -12.17043,-12.170428 -2.3984,0 -4.75718,0.713573 -6.75915,2.041623 z M 88.052677,131.81933 c 0,-12.26953 -9.930593,-22.20012 -22.200138,-22.20012 -12.269532,0 -22.200126,9.93059 -22.200126,22.20012 0,0.77305 0.03966,1.54609 0.118929,2.2993 0.356787,3.46877 -2.021792,7.21505 -5.510393,7.21505 h -0.951431 c -3.508409,0 -6.342895,2.83447 -6.342895,6.3429 0,3.5084 2.834486,6.34289 6.342895,6.34289 h 28.543021 c 12.269545,0 22.200138,-9.93059 22.200138,-22.20014 z"
id="path1-2"
style="fill:#989898;fill-opacity:1;stroke-width:0.198215" /></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,11 +1,13 @@
@page deck_search_syntax_help Deck Search Syntax Help
## Deck Search Syntax Help
-----
The search bar recognizes a set of special commands.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
searches are case insensitive.
<dl>
<dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt>
<dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd>
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd>
@ -27,6 +29,11 @@ searches are case insensitive.
<dt><u>F</u>ormat:</dt>
<dd>[f:standard](#f:standard) <small>(Any deck with format set to standard)</small></dd>
<dt><u>C</u>omments:</dt>
<dd>[c:good](#c:good) <small>(Any deck with comments containing the word good)</small></dd>
<dd>[c:good c:deck](#c:good c:deck) <small>(Any deck with comments containing the words good and deck)</small></dd>
<dd>[c:"good deck"](#c:%22good deck%22) <small>(Any deck with comments containing the exact phrase "good deck")</small></dd>
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
@ -40,6 +47,6 @@ searches are case insensitive.
<dd>[t:aggro OR o:control](#t:aggro OR o:control) <small>(Any deck filename that contains either aggro or control)</small></dd>
<dt>Grouping:</dt>
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
<dd><a href="#red -([[]]:100 OR aggro)">red -([[]]:100 OR aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
</dl>
</dl>

View file

@ -1,10 +1,12 @@
@page search_syntax_help Search Syntax Help
## Search Syntax Help
-----
The search bar recognizes a set of special commands similar to some other card databases.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive.
<dl>
<dt>Name:</dt>
<dd>[birds of paradise](#birds of paradise) <small>(Any card name containing the words birds, of, and paradise)</small></dd>
<dd>["birds of paradise"](#%22birds of paradise%22) <small>(Any card name containing the exact phrase "birds of paradise")</small></dd>
@ -49,20 +51,20 @@ In this list of examples below, each entry has an explanation and can be clicked
<dt><u>E</u>dition:</dt>
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
<dd>[e:lea or e:leb](#e:lea or e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
<dt>Negate:</dt>
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
<dt>Branching:</dt>
<dd>[t:sliver or o:changeling](#t:sliver or o:changeling) <small>(Any card that is either a sliver or has changeling)</small></dd>
<dd>[t:sliver OR o:changeling](#t:sliver OR o:changeling) <small>(Any card that is either a sliver or has changeling)</small></dd>
<dt>Grouping:</dt>
<dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
<dd><a href="#t:angel -(angel OR c:w)">t:angel -(angel OR c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
<dt>Regular Expression:</dt>
<dd>[/^fell/](#/^fell/) <small>(Any card name that begins with "fell")</small></dd>
<dd>[o:/counter target .* spell/](#o:/counter target .* spell/) <small>(Any card text with "counter target *something* spell")</small></dd>
<dd>[o:/for each .* and\/or .*/](#o:/for each .* and\/or .*/) <small>(/'s can be escaped with a \)</small></dd>
</dl>
</dl>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M512 320C512 214 426 128 320 128L320 512C426 512 512 426 512 320zM64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320z"/></svg>

After

Width:  |  Height:  |  Size: 410 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 188.5L300.1 175.5C293.6 173.9 288.8 168.4 288.1 161.7C287.4 155 290.9 148.6 296.8 145.6L337.6 125.2L294.3 92.7C288.8 88.6 286.5 81.4 288.7 74.8C290.9 68.2 297.1 64 304 64L464 64C494.2 64 522.7 78.2 540.8 102.4L598.4 179.2C604.6 187.5 608 197.6 608 208C608 234.5 586.5 256 560 256L538.5 256C521.5 256 505.2 249.3 493.2 237.3L479.9 224L447.9 224L447.9 245.5C447.9 270.3 460.7 293.4 481.7 306.6L588.3 373.2C620.4 393.3 639.9 428.4 639.9 466.3C639.9 526.9 590.8 576.1 530.1 576.1L32.3 576C29 576 25.7 575.6 22.7 574.6C13.5 571.8 6 565 2.3 556C1 552.7 .1 549.1 0 545.3C-.2 541.6 .3 538 1.3 534.6C4.1 525.4 10.9 517.9 19.9 514.2C22.9 513 26.1 512.2 29.4 512L433.3 476C441.6 475.3 448 468.3 448 459.9C448 455.6 446.3 451.5 443.3 448.5L398.9 404.1C368.9 374.1 352 333.4 352 291L352 188.5zM512 136.3C512 136.2 512 136.1 512 136C512 135.9 512 135.8 512 135.7L512 136.3zM510.7 143.7L464.3 132.1C464.1 133.4 464 134.7 464 136C464 149.3 474.7 160 488 160C498.6 160 507.5 153.2 510.7 143.7zM130.9 180.5C147.2 166 171.3 164.3 189.4 176.4L320 263.4L320 290.9C320 323.7 328.4 355.7 344 383.9L112 383.9C105.3 383.9 99.3 379.7 97 373.5C94.7 367.3 96.5 360.2 101.6 355.8L171 296.3L18.4 319.8C11.4 320.9 4.5 317.2 1.5 310.8C-1.5 304.4 .1 296.8 5.4 292L130.9 180.5z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px" viewBox="0 0 971.986 971.986"
xml:space="preserve">
<g>
<path d="M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3
c16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873
c-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M160 96C124.7 96 96 124.7 96 160L96 480C96 515.3 124.7 544 160 544L480 544C515.3 544 544 515.3 544 480L544 237.3C544 220.3 537.3 204 525.3 192L448 114.7C436 102.7 419.7 96 402.7 96L160 96zM192 192C192 174.3 206.3 160 224 160L384 160C401.7 160 416 174.3 416 192L416 256C416 273.7 401.7 288 384 288L224 288C206.3 288 192 273.7 192 256L192 192zM320 352C355.3 352 384 380.7 384 416C384 451.3 355.3 480 320 480C284.7 480 256 451.3 256 416C256 380.7 284.7 352 320 352z"/></svg>

After

Width:  |  Height:  |  Size: 693 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M535.6 85.7C513.7 63.8 478.3 63.8 456.4 85.7L432 110.1L529.9 208L554.3 183.6C576.2 161.7 576.2 126.3 554.3 104.4L535.6 85.7zM236.4 305.7C230.3 311.8 225.6 319.3 222.9 327.6L193.3 416.4C190.4 425 192.7 434.5 199.1 441C205.5 447.5 215 449.7 223.7 446.8L312.5 417.2C320.7 414.5 328.2 409.8 334.4 403.7L496 241.9L398.1 144L236.4 305.7zM160 128C107 128 64 171 64 224L64 480C64 533 107 576 160 576L416 576C469 576 512 533 512 480L512 384C512 366.3 497.7 352 480 352C462.3 352 448 366.3 448 384L448 480C448 497.7 433.7 512 416 512L160 512C142.3 512 128 497.7 128 480L128 224C128 206.3 142.3 192 160 192L256 192C273.7 192 288 177.7 288 160C288 142.3 273.7 128 256 128L160 128z"/></svg>

After

Width:  |  Height:  |  Size: 899 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M384 96L512 96C529.7 96 544 110.3 544 128C544 145.7 529.7 160 512 160L398.4 160C393.2 185.8 375.5 207.1 352 217.3L352 512L512 512C529.7 512 544 526.3 544 544C544 561.7 529.7 576 512 576L128 576C110.3 576 96 561.7 96 544C96 526.3 110.3 512 128 512L288 512L288 217.3C264.5 207 246.8 185.7 241.6 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L256 96C270.6 76.6 293.8 64 320 64C346.2 64 369.4 76.6 384 96zM439.6 384L584.4 384L512 259.8L439.6 384zM512 480C449.1 480 396.8 446 386 401.1C383.4 390.1 387 378.8 392.7 369L487.9 205.8C492.9 197.2 502.1 192 512 192C521.9 192 531.1 197.3 536.1 205.8L631.3 369C637 378.8 640.6 390.1 638 401.1C627.2 445.9 574.9 480 512 480zM126.8 259.8L54.4 384L199.3 384L126.8 259.8zM.9 401.1C-1.7 390.1 1.9 378.8 7.6 369L102.8 205.8C107.8 197.2 117 192 126.9 192C136.8 192 146 197.3 151 205.8L246.2 369C251.9 378.8 255.5 390.1 252.9 401.1C242.1 445.9 189.8 480 126.9 480C64 480 11.7 446 .9 401.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M32 176C32 134.5 63.6 100.4 104 96.4L104 96L384 96C437 96 480 139 480 192L480 368L304 368C264.2 368 232 400.2 232 440L232 500C232 524.3 212.3 544 188 544C163.7 544 144 524.3 144 500L144 272L80 272C53.5 272 32 250.5 32 224L32 176zM268.8 544C275.9 530.9 280 515.9 280 500L280 440C280 426.7 290.7 416 304 416L552 416C565.3 416 576 426.7 576 440L576 464C576 508.2 540.2 544 496 544L268.8 544zM112 144C94.3 144 80 158.3 80 176L80 224L144 224L144 176C144 158.3 129.7 144 112 144z"/></svg>

After

Width:  |  Height:  |  Size: 704 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M278.6 438.6L182.6 534.6C170.1 547.1 149.8 547.1 137.3 534.6L41.3 438.6C28.8 426.1 28.8 405.8 41.3 393.3C53.8 380.8 74.1 380.8 86.6 393.3L128 434.7L128 128C128 110.3 142.3 96 160 96C177.7 96 192 110.3 192 128L192 434.7L233.4 393.3C245.9 380.8 266.2 380.8 278.7 393.3C291.2 405.8 291.2 426.1 278.7 438.6zM352 544C334.3 544 320 529.7 320 512C320 494.3 334.3 480 352 480L384 480C401.7 480 416 494.3 416 512C416 529.7 401.7 544 384 544L352 544zM352 416C334.3 416 320 401.7 320 384C320 366.3 334.3 352 352 352L448 352C465.7 352 480 366.3 480 384C480 401.7 465.7 416 448 416L352 416zM352 288C334.3 288 320 273.7 320 256C320 238.3 334.3 224 352 224L512 224C529.7 224 544 238.3 544 256C544 273.7 529.7 288 512 288L352 288zM352 160C334.3 160 320 145.7 320 128C320 110.3 334.3 96 352 96L576 96C593.7 96 608 110.3 608 128C608 145.7 593.7 160 576 160L352 160z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -89,6 +89,8 @@ void TappedOutInterface::analyzeDeck(const DeckList &deck)
QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
// we interpret the redirect and open it in the browser instead, do not follow redirects
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
manager->post(request, data);
}

View file

@ -8,10 +8,11 @@
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../interface/deck_loader/card_node_function.h"
#include "../../../interface/deck_loader/deck_loader.h"
#include <QJsonArray>
#include <QJsonObject>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/deck_list/deck_list.h>
class IJsonDeckParser
{
@ -49,7 +50,7 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
deckList.loadFromStream_Plain(outStream, false);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
return deckList;
@ -96,7 +97,7 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
deckList.loadFromStream_Plain(outStream, false);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
QJsonObject commandersObj = obj.value("commanders").toObject();

View file

@ -239,6 +239,7 @@ SettingsCache::SettingsCache()
homeTabBackgroundSource = settings->value("home/background", "themed").toString();
homeTabBackgroundShuffleFrequency = settings->value("home/background/shuffleTimer", 0).toInt();
homeTabDisplayCardName = settings->value("home/background/displayCardName", true).toBool();
tabVisualDeckStorageOpen = settings->value("tabs/visualDeckStorage", true).toBool();
tabServerOpen = settings->value("tabs/server", true).toBool();
@ -267,9 +268,6 @@ SettingsCache::SettingsCache()
picDownload = settings->value("personal/picturedownload", true).toBool();
showStatusBar = settings->value("personal/showStatusBar", false).toBool();
mainWindowGeometry = settings->value("interface/main_window_geometry").toByteArray();
tokenDialogGeometry = settings->value("interface/token_dialog_geometry").toByteArray();
setsDialogGeometry = settings->value("interface/sets_dialog_geometry").toByteArray();
notificationsEnabled = settings->value("interface/notificationsenabled", true).toBool();
spectatorNotificationsEnabled = settings->value("interface/specnotificationsenabled", false).toBool();
buddyConnectNotificationsEnabled = settings->value("interface/buddyconnectnotificationsenabled", true).toBool();
@ -279,7 +277,6 @@ SettingsCache::SettingsCache()
doNotDeleteArrowsInSubPhases = settings->value("interface/doNotDeleteArrowsInSubPhases", true).toBool();
startingHandSize = settings->value("interface/startinghandsize", 7).toInt();
annotateTokens = settings->value("interface/annotatetokens", false).toBool();
tabGameSplitterSizes = settings->value("interface/tabgame_splittersizes").toByteArray();
knownMissingFeatures = settings->value("interface/knownmissingfeatures", "").toString();
useTearOffMenus = settings->value("interface/usetearoffmenus", true).toBool();
cardViewInitialRowsMax = settings->value("interface/cardViewInitialRowsMax", 14).toInt();
@ -287,6 +284,9 @@ SettingsCache::SettingsCache()
closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool();
focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool();
showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool();
showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
displayCardNames = settings->value("cards/displaycardnames", true).toBool();
@ -309,6 +309,7 @@ SettingsCache::SettingsCache()
visualDeckStorageDefaultTagsList =
settings->value("interface/visualdeckstoragedefaulttagslist", defaultTags).toStringList();
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
visualDeckStorageShowColorIdentity = settings->value("interface/visualdeckstorageshowcoloridentity", true).toBool();
visualDeckStorageShowBannerCardComboBox =
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
visualDeckStorageShowTagsOnDeckPreviews =
@ -387,6 +388,13 @@ SettingsCache::SettingsCache()
defaultStartingLifeTotal = settings->value("game/defaultstartinglifetotal", 20).toInt();
shareDecklistsOnLoad = settings->value("game/sharedecklistsonload", false).toBool();
rememberGameSettings = settings->value("game/remembergamesettings", true).toBool();
// Local game settings use "localgameoptions/" prefix to keep them separate
// from server game settings which use "game/" prefix
localGameRememberSettings = settings->value("localgameoptions/remembersettings", false).toBool();
localGameMaxPlayers = settings->value("localgameoptions/maxplayers", 1).toInt();
localGameStartingLifeTotal = settings->value("localgameoptions/startinglifetotal", 20).toInt();
clientID = settings->value("personal/clientid", CLIENT_INFO_NOT_SET).toString();
clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString();
}
@ -594,6 +602,13 @@ void SettingsCache::setHomeTabBackgroundShuffleFrequency(int _frequency)
emit homeTabBackgroundShuffleFrequencyChanged();
}
void SettingsCache::setHomeTabDisplayCardName(QT_STATE_CHANGED_T _displayCardName)
{
homeTabDisplayCardName = static_cast<bool>(_displayCardName);
settings->setValue("home/background/displayCardName", homeTabDisplayCardName);
emit homeTabDisplayCardNameChanged();
}
void SettingsCache::setTabVisualDeckStorageOpen(bool value)
{
tabVisualDeckStorageOpen = value;
@ -704,12 +719,6 @@ void SettingsCache::setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens)
settings->setValue("interface/annotatetokens", annotateTokens);
}
void SettingsCache::setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes)
{
tabGameSplitterSizes = _tabGameSplitterSizes;
settings->setValue("interface/tabgame_splittersizes", tabGameSplitterSizes);
}
void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
{
showShortcuts = static_cast<bool>(_showShortcuts);
@ -821,6 +830,13 @@ void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T val
settings->setValue("interface/visualdeckstoragesearchfoldernames", visualDeckStorageSearchFolderNames);
}
void SettingsCache::setVisualDeckStorageShowColorIdentity(QT_STATE_CHANGED_T value)
{
visualDeckStorageShowColorIdentity = value;
settings->setValue("interface/visualdeckstorageshowcoloridentity", visualDeckStorageShowColorIdentity);
emit visualDeckStorageShowColorIdentityChanged(visualDeckStorageShowColorIdentity);
}
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
{
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
@ -1074,24 +1090,6 @@ void SettingsCache::setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignore
settings->setValue("chat/ignore_unregistered_messages", ignoreUnregisteredUserMessages);
}
void SettingsCache::setMainWindowGeometry(const QByteArray &_mainWindowGeometry)
{
mainWindowGeometry = _mainWindowGeometry;
settings->setValue("interface/main_window_geometry", mainWindowGeometry);
}
void SettingsCache::setTokenDialogGeometry(const QByteArray &_tokenDialogGeometry)
{
tokenDialogGeometry = _tokenDialogGeometry;
settings->setValue("interface/token_dialog_geometry", tokenDialogGeometry);
}
void SettingsCache::setSetsDialogGeometry(const QByteArray &_setsDialogGeometry)
{
setsDialogGeometry = _setsDialogGeometry;
settings->setValue("interface/sets_dialog_geometry", setsDialogGeometry);
}
void SettingsCache::setPixmapCacheSize(const int _pixmapCacheSize)
{
pixmapCacheSize = _pixmapCacheSize;
@ -1127,257 +1125,21 @@ void SettingsCache::setClientVersion(const QString &_clientVersion)
QStringList SettingsCache::getCountries() const
{
static QStringList countries = QStringList() << "ad"
<< "ae"
<< "af"
<< "ag"
<< "ai"
<< "al"
<< "am"
<< "ao"
<< "aq"
<< "ar"
<< "as"
<< "at"
<< "au"
<< "aw"
<< "ax"
<< "az"
<< "ba"
<< "bb"
<< "bd"
<< "be"
<< "bf"
<< "bg"
<< "bh"
<< "bi"
<< "bj"
<< "bl"
<< "bm"
<< "bn"
<< "bo"
<< "bq"
<< "br"
<< "bs"
<< "bt"
<< "bv"
<< "bw"
<< "by"
<< "bz"
<< "ca"
<< "cc"
<< "cd"
<< "cf"
<< "cg"
<< "ch"
<< "ci"
<< "ck"
<< "cl"
<< "cm"
<< "cn"
<< "co"
<< "cr"
<< "cu"
<< "cv"
<< "cw"
<< "cx"
<< "cy"
<< "cz"
<< "de"
<< "dj"
<< "dk"
<< "dm"
<< "do"
<< "dz"
<< "ec"
<< "ee"
<< "eg"
<< "eh"
<< "er"
<< "es"
<< "et"
<< "eu"
<< "fi"
<< "fj"
<< "fk"
<< "fm"
<< "fo"
<< "fr"
<< "ga"
<< "gb"
<< "gd"
<< "ge"
<< "gf"
<< "gg"
<< "gh"
<< "gi"
<< "gl"
<< "gm"
<< "gn"
<< "gp"
<< "gq"
<< "gr"
<< "gs"
<< "gt"
<< "gu"
<< "gw"
<< "gy"
<< "hk"
<< "hm"
<< "hn"
<< "hr"
<< "ht"
<< "hu"
<< "id"
<< "ie"
<< "il"
<< "im"
<< "in"
<< "io"
<< "iq"
<< "ir"
<< "is"
<< "it"
<< "je"
<< "jm"
<< "jo"
<< "jp"
<< "ke"
<< "kg"
<< "kh"
<< "ki"
<< "km"
<< "kn"
<< "kp"
<< "kr"
<< "kw"
<< "ky"
<< "kz"
<< "la"
<< "lb"
<< "lc"
<< "li"
<< "lk"
<< "lr"
<< "ls"
<< "lt"
<< "lu"
<< "lv"
<< "ly"
<< "ma"
<< "mc"
<< "md"
<< "me"
<< "mf"
<< "mg"
<< "mh"
<< "mk"
<< "ml"
<< "mm"
<< "mn"
<< "mo"
<< "mp"
<< "mq"
<< "mr"
<< "ms"
<< "mt"
<< "mu"
<< "mv"
<< "mw"
<< "mx"
<< "my"
<< "mz"
<< "na"
<< "nc"
<< "ne"
<< "nf"
<< "ng"
<< "ni"
<< "nl"
<< "no"
<< "np"
<< "nr"
<< "nu"
<< "nz"
<< "om"
<< "pa"
<< "pe"
<< "pf"
<< "pg"
<< "ph"
<< "pk"
<< "pl"
<< "pm"
<< "pn"
<< "pr"
<< "ps"
<< "pt"
<< "pw"
<< "py"
<< "qa"
<< "re"
<< "ro"
<< "rs"
<< "ru"
<< "rw"
<< "sa"
<< "sb"
<< "sc"
<< "sd"
<< "se"
<< "sg"
<< "sh"
<< "si"
<< "sj"
<< "sk"
<< "sl"
<< "sm"
<< "sn"
<< "so"
<< "sr"
<< "ss"
<< "st"
<< "sv"
<< "sx"
<< "sy"
<< "sz"
<< "tc"
<< "td"
<< "tf"
<< "tg"
<< "th"
<< "tj"
<< "tk"
<< "tl"
<< "tm"
<< "tn"
<< "to"
<< "tr"
<< "tt"
<< "tv"
<< "tw"
<< "tz"
<< "ua"
<< "ug"
<< "um"
<< "us"
<< "uy"
<< "uz"
<< "va"
<< "vc"
<< "ve"
<< "vg"
<< "vi"
<< "vn"
<< "vu"
<< "wf"
<< "ws"
<< "xk"
<< "ye"
<< "yt"
<< "za"
<< "zm"
<< "zw";
static const QStringList countries = {
"ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb",
"bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by",
"bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx",
"cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "eu", "fi", "fj",
"fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr",
"gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq",
"ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz",
"la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh",
"mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc",
"ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl",
"pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se",
"sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", "sz", "tc", "td",
"tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us",
"uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "xk", "ye", "yt", "za", "zm", "zw"};
return countries;
}
@ -1490,6 +1252,24 @@ void SettingsCache::setRememberGameSettings(const bool _rememberGameSettings)
settings->setValue("game/remembergamesettings", rememberGameSettings);
}
void SettingsCache::setLocalGameRememberSettings(bool value)
{
localGameRememberSettings = value;
settings->setValue("localgameoptions/remembersettings", value);
}
void SettingsCache::setLocalGameMaxPlayers(int value)
{
localGameMaxPlayers = value;
settings->setValue("localgameoptions/maxplayers", value);
}
void SettingsCache::setLocalGameStartingLifeTotal(int value)
{
localGameStartingLifeTotal = value;
settings->setValue("localgameoptions/startinglifetotal", value);
}
void SettingsCache::setNotifyAboutUpdate(QT_STATE_CHANGED_T _notifyaboutupdate)
{
notifyAboutUpdates = static_cast<bool>(_notifyaboutupdate);
@ -1531,6 +1311,18 @@ void SettingsCache::setRoundCardCorners(bool _roundCardCorners)
emit roundCardCornersChanged(roundCardCorners);
}
void SettingsCache::setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount)
{
showDragSelectionCount = static_cast<bool>(_showDragSelectionCount);
settings->setValue("interface/showlassoselectioncount", showDragSelectionCount);
}
void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount)
{
showTotalSelectionCount = static_cast<bool>(_showTotalSelectionCount);
settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount);
}
void SettingsCache::loadPaths()
{
QString dataPath = getDataPath();

View file

@ -143,6 +143,7 @@ signals:
void themeChanged();
void homeTabBackgroundSourceChanged();
void homeTabBackgroundShuffleFrequencyChanged();
void homeTabDisplayCardNameChanged();
void picDownloadChanged();
void showStatusBarChanged(bool state);
void showGameSelectorFilterToolbarChanged(bool state);
@ -157,6 +158,7 @@ signals:
void deckEditorTagsWidgetVisibleChanged(bool _visible);
void visualDeckStorageShowTagFilterChanged(bool _visible);
void visualDeckStorageDefaultTagsListChanged();
void visualDeckStorageShowColorIdentityChanged(bool _visible);
void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible);
void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible);
void visualDeckStorageCardSizeChanged();
@ -203,9 +205,6 @@ private:
DebugSettings *debugSettings;
CardCounterSettings *cardCounterSettings;
QByteArray mainWindowGeometry;
QByteArray tokenDialogGeometry;
QByteArray setsDialogGeometry;
QString lang;
QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath,
customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource;
@ -222,6 +221,7 @@ private:
bool showTipsOnStartup;
QList<int> seenTips;
int homeTabBackgroundShuffleFrequency;
bool homeTabDisplayCardName;
bool mbDownloadSpoilers;
int updateReleaseChannel;
int maxFontSize;
@ -235,7 +235,6 @@ private:
bool doNotDeleteArrowsInSubPhases;
int startingHandSize;
bool annotateTokens;
QByteArray tabGameSplitterSizes;
bool showShortcuts;
bool showGameSelectorFilterToolbar;
bool displayCardNames;
@ -249,6 +248,7 @@ private:
bool deckEditorTagsWidgetVisible;
int visualDeckStorageSortingOrder;
bool visualDeckStorageShowFolders;
bool visualDeckStorageShowColorIdentity;
bool visualDeckStorageShowBannerCardComboBox;
bool visualDeckStorageShowTagsOnDeckPreviews;
bool visualDeckStorageShowTagFilter;
@ -330,10 +330,18 @@ private:
[[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const;
void loadPaths();
bool rememberGameSettings;
// Local game settings (separate from server game settings in game/*)
bool localGameRememberSettings;
int localGameMaxPlayers;
int localGameStartingLifeTotal;
QList<ReleaseChannel *> releaseChannels;
bool isPortableBuild;
bool roundCardCorners;
bool showStatusBar;
bool showDragSelectionCount;
bool showTotalSelectionCount;
public:
SettingsCache();
@ -341,18 +349,6 @@ public:
QString getSettingsPath();
[[nodiscard]] QString getCachePath() const;
[[nodiscard]] QString getNetworkCachePath() const;
[[nodiscard]] const QByteArray &getMainWindowGeometry() const
{
return mainWindowGeometry;
}
[[nodiscard]] const QByteArray &getTokenDialogGeometry() const
{
return tokenDialogGeometry;
}
[[nodiscard]] const QByteArray &getSetsDialogGeometry() const
{
return setsDialogGeometry;
}
[[nodiscard]] QString getLang() const
{
return lang;
@ -413,6 +409,10 @@ public:
{
return homeTabBackgroundShuffleFrequency;
}
[[nodiscard]] bool getHomeTabDisplayCardName() const
{
return homeTabDisplayCardName;
}
[[nodiscard]] bool getTabVisualDeckStorageOpen() const
{
return tabVisualDeckStorageOpen;
@ -457,6 +457,14 @@ public:
{
return showStatusBar;
}
[[nodiscard]] bool getShowDragSelectionCount() const
{
return showDragSelectionCount;
}
[[nodiscard]] bool getShowTotalSelectionCount() const
{
return showTotalSelectionCount;
}
[[nodiscard]] bool getNotificationsEnabled() const
{
return notificationsEnabled;
@ -547,10 +555,6 @@ public:
{
return annotateTokens;
}
[[nodiscard]] QByteArray getTabGameSplitterSizes() const
{
return tabGameSplitterSizes;
}
[[nodiscard]] bool getShowShortcuts() const
{
return showShortcuts;
@ -615,6 +619,10 @@ public:
{
return visualDeckStorageSearchFolderNames;
}
[[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const
{
return visualDeckStorageShowColorIdentity;
}
[[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const
{
return visualDeckStorageShowBannerCardComboBox;
@ -870,6 +878,18 @@ public:
{
return rememberGameSettings;
}
[[nodiscard]] bool getLocalGameRememberSettings() const
{
return localGameRememberSettings;
}
[[nodiscard]] int getLocalGameMaxPlayers() const
{
return localGameMaxPlayers;
}
[[nodiscard]] int getLocalGameStartingLifeTotal() const
{
return localGameStartingLifeTotal;
}
[[nodiscard]] int getKeepAlive() const override
{
return keepalive;
@ -983,9 +1003,6 @@ public:
public slots:
void setDownloadSpoilerStatus(bool _spoilerStatus);
void setMainWindowGeometry(const QByteArray &_mainWindowGeometry);
void setTokenDialogGeometry(const QByteArray &_tokenDialog);
void setSetsDialogGeometry(const QByteArray &_setsDialog);
void setLang(const QString &_lang);
void setShowTipsOnStartup(bool _showTipsOnStartup);
void setSeenTips(const QList<int> &_seenTips);
@ -1001,6 +1018,7 @@ public slots:
void setThemeName(const QString &_themeName);
void setHomeTabBackgroundSource(const QString &_backgroundSource);
void setHomeTabBackgroundShuffleFrequency(int _frequency);
void setHomeTabDisplayCardName(QT_STATE_CHANGED_T _displayCardName);
void setTabVisualDeckStorageOpen(bool value);
void setTabServerOpen(bool value);
void setTabAccountOpen(bool value);
@ -1021,7 +1039,6 @@ public slots:
void setDoNotDeleteArrowsInSubPhases(QT_STATE_CHANGED_T _doNotDeleteArrowsInSubPhases);
void setStartingHandSize(int _startingHandSize);
void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens);
void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes);
void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts);
void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar);
void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames);
@ -1038,6 +1055,7 @@ public slots:
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageDefaultTagsList(QStringList _defaultTagsList);
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowColorIdentity(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
@ -1099,6 +1117,9 @@ public slots:
void setDefaultStartingLifeTotal(const int _defaultStartingLifeTotal);
void setShareDecklistsOnLoad(const bool _shareDecklistsOnLoad);
void setRememberGameSettings(const bool _rememberGameSettings);
void setLocalGameRememberSettings(bool value);
void setLocalGameMaxPlayers(int value);
void setLocalGameStartingLifeTotal(int value);
void setCheckUpdatesOnStartup(QT_STATE_CHANGED_T value);
void setStartupCardUpdateCheckPromptForUpdate(bool value);
void setStartupCardUpdateCheckAlwaysUpdate(bool value);
@ -1109,6 +1130,7 @@ public slots:
void setUpdateReleaseChannelIndex(int value);
void setMaxFontSize(int _max);
void setRoundCardCorners(bool _roundCardCorners);
void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount);
void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount);
};
#endif

View file

@ -11,6 +11,8 @@ CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *p
void CardCounterSettings::setColor(int counterId, const QColor &color)
{
QSettings settings = getSettings();
QString key = QString("cards/counters/%1/color").arg(counterId);
if (settings.value(key).value<QColor>() == color)
@ -36,7 +38,7 @@ QColor CardCounterSettings::color(int counterId) const
defaultColor = QColor::fromHsv(h, s, v);
}
return settings.value(QString("cards/counters/%1/color").arg(counterId), defaultColor).value<QColor>();
return getSettings().value(QString("cards/counters/%1/color").arg(counterId), defaultColor).value<QColor>();
}
QString CardCounterSettings::displayName(int counterId) const

View file

@ -501,7 +501,7 @@ private:
{"Player/aUntapAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Untap All"),
parseSequenceString("Ctrl+U"),
ShortcutGroup::Playing_Area)},
{"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Untap"),
{"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Skip Untapping"),
parseSequenceString("Alt+U"),
ShortcutGroup::Playing_Area)},
{"Player/aFlip", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Turn Card Over"),
@ -513,6 +513,9 @@ private:
{"Player/aPlay", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Play Card"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},
{"Player/aPlayFacedown", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Play Card, Face Down"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},
{"Player/aAttach", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Attach Card..."),
parseSequenceString("Ctrl+Alt+A"),
ShortcutGroup::Playing_Area)},
@ -560,12 +563,9 @@ private:
{"Player/aMoveToTopLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aPlayFacedown", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield, Face Down"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aPlay", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aMoveToTable", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"),
parseSequenceString(""),
ShortcutGroup::Move_selected)},
{"Player/aViewHand",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Hand"), parseSequenceString(""), ShortcutGroup::View)},
{"Player/aViewGraveyard",
@ -598,11 +598,19 @@ private:
{"Player/aMoveTopCardsToGraveyard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple)"),
parseSequenceString("Alt+M"),
ShortcutGroup::Move_top)},
{"Player/aMoveTopCardsToGraveyardFaceDown",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple), Face Down"),
parseSequenceString(""),
ShortcutGroup::Move_top)},
{"Player/aMoveTopCardToExile",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"), parseSequenceString(""), ShortcutGroup::Move_top)},
{"Player/aMoveTopCardsToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple)"),
parseSequenceString(""),
ShortcutGroup::Move_top)},
{"Player/aMoveTopCardsToExileFaceDown",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple), Face Down"),
parseSequenceString(""),
ShortcutGroup::Move_top)},
{"Player/aMoveTopCardsUntil", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Stack Until Found"),
parseSequenceString("Ctrl+Shift+Y"),
ShortcutGroup::Move_top)},
@ -620,11 +628,19 @@ private:
{"Player/aMoveBottomCardsToGrave", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple)"),
parseSequenceString(""),
ShortcutGroup::Move_bottom)},
{"Player/aMoveBottomCardsToGraveFaceDown",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Graveyard (Multiple), Face Down"),
parseSequenceString(""),
ShortcutGroup::Move_bottom)},
{"Player/aMoveBottomCardToExile",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile"), parseSequenceString(""), ShortcutGroup::Move_bottom)},
{"Player/aMoveBottomCardsToExile", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple)"),
parseSequenceString(""),
ShortcutGroup::Move_bottom)},
{"Player/aMoveBottomCardsToExileFaceDown",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Exile (Multiple), Face Down"),
parseSequenceString(""),
ShortcutGroup::Move_bottom)},
{"Player/aMoveBottomCardToTop", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"),
parseSequenceString(""),
ShortcutGroup::Move_bottom)},
@ -660,6 +676,12 @@ private:
{"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"),
parseSequenceString("Ctrl+M"),
ShortcutGroup::Drawing)},
{"Player/aMulliganSame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Same hand size)"),
parseSequenceString("Ctrl+Shift+M"),
ShortcutGroup::Drawing)},
{"Player/aMulliganMinusOne", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Hand size - 1)"),
parseSequenceString("Ctrl+Shift+Alt+M"),
ShortcutGroup::Drawing)},
{"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"),
parseSequenceString("Ctrl+D"),
ShortcutGroup::Drawing)},

View file

@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / GenericQuery
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / CommentQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
@ -25,12 +25,13 @@ DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String
PathQuery <- [Pp] 'ath'? [:] String
FormatQuery <- [Ff] 'ormat'? [:] String
CommentQuery <- [Cc] ('omment' 's'?)? [:] String
GenericQuery <- String
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
UnescapedStringListPart <- !['":<>=! ].
UnescapedStringListPart <- !['":<>()=! ].
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
@ -166,6 +167,14 @@ static void setupParserRules()
};
};
search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto value = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
auto comments = deck->deckLoader->getDeck().deckList.getComments();
return comments.contains(value, Qt::CaseInsensitive);
};
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {

View file

@ -1,8 +1,9 @@
#include "abstract_game.h"
#include "../interface/widgets/tabs/tab_game.h"
#include "player/player.h"
AbstractGame::AbstractGame(TabGame *_tab) : tab(_tab)
AbstractGame::AbstractGame(TabGame *_tab) : QObject(_tab), tab(_tab)
{
gameMetaInfo = new GameMetaInfo(this);
gameEventHandler = new GameEventHandler(this);

View file

@ -1,14 +1,13 @@
#include "abstract_card_drag_item.h"
#include "../../client/settings/cache_settings.h"
#include "../z_values.h"
#include <QCursor>
#include <QDebug>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
static const float CARD_WIDTH_HALF = CARD_WIDTH / 2;
static const float CARD_HEIGHT_HALF = CARD_HEIGHT / 2;
const QColor GHOST_MASK = QColor(255, 255, 255, 50);
AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
@ -18,19 +17,19 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
{
if (parentDrag) {
parentDrag->addChildDrag(this);
setZValue(2000000007 + hotSpot.x() * 1000000 + hotSpot.y() * 1000 + 1000);
setZValue(ZValues::childDragZValue(hotSpot.x(), hotSpot.y()));
connect(parentDrag, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
} else {
hotSpot = QPointF{qBound(0.0, hotSpot.x(), static_cast<qreal>(CARD_WIDTH - 1)),
qBound(0.0, hotSpot.y(), static_cast<qreal>(CARD_HEIGHT - 1))};
hotSpot = QPointF{qBound(0.0, hotSpot.x(), CardDimensions::WIDTH_F - 1),
qBound(0.0, hotSpot.y(), CardDimensions::HEIGHT_F - 1)};
setCursor(Qt::ClosedHandCursor);
setZValue(2000000007);
setZValue(ZValues::DRAG_ITEM);
}
if (item->getTapped())
setTransform(QTransform()
.translate(CARD_WIDTH_HALF, CARD_HEIGHT_HALF)
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(90)
.translate(-CARD_WIDTH_HALF, -CARD_HEIGHT_HALF));
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
setCacheMode(DeviceCoordinateCache);
@ -47,7 +46,7 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
QPainterPath AbstractCardDragItem::shape() const
{
QPainterPath shape;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CARD_WIDTH : 0.0;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape;
}

View file

@ -34,7 +34,7 @@ public:
AbstractCardDragItem(AbstractCardItem *_item, const QPointF &_hotSpot, AbstractCardDragItem *parentDrag = 0);
[[nodiscard]] QRectF boundingRect() const override
{
return QRectF(0, 0, CARD_WIDTH, CARD_HEIGHT);
return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F);
}
[[nodiscard]] QPainterPath shape() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

View file

@ -3,6 +3,7 @@
#include "../../client/settings/cache_settings.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../game_scene.h"
#include "../z_values.h"
#include <QCursor>
#include <QGraphicsScene>
@ -38,13 +39,13 @@ AbstractCardItem::~AbstractCardItem()
QRectF AbstractCardItem::boundingRect() const
{
return QRectF(0, 0, CARD_WIDTH, CARD_HEIGHT);
return QRectF(0, 0, CardDimensions::WIDTH_F, CardDimensions::HEIGHT_F);
}
QPainterPath AbstractCardItem::shape() const
{
QPainterPath shape;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CARD_WIDTH : 0.0;
qreal cardCornerRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * CardDimensions::WIDTH_F : 0.0;
shape.addRoundedRect(boundingRect(), cardCornerRadius, cardCornerRadius);
return shape;
}
@ -215,9 +216,9 @@ void AbstractCardItem::setHovered(bool _hovered)
if (_hovered)
processHoverEvent();
isHovered = _hovered;
setZValue(_hovered ? 2000000004 : realZValue);
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1);
setTransformOriginPoint(_hovered ? CARD_WIDTH / 2 : 0, _hovered ? CARD_HEIGHT / 2 : 0);
setTransformOriginPoint(_hovered ? CardDimensions::WIDTH_HALF_F : 0, _hovered ? CardDimensions::HEIGHT_HALF_F : 0);
update();
}
@ -273,9 +274,9 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
else {
tapAngle = tapped ? 90 : 0;
setTransform(QTransform()
.translate((float)CARD_WIDTH / 2, (float)CARD_HEIGHT / 2)
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(tapAngle)
.translate((float)-CARD_WIDTH / 2, (float)-CARD_HEIGHT / 2));
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
update();
}
}

View file

@ -8,6 +8,7 @@
#define ABSTRACTCARDITEM_H
#include "../../game_graphics/board/graphics_item_type.h"
#include "../card_dimensions.h"
#include "arrow_target.h"
#include <libcockatrice/card/printing/exact_card.h>
@ -15,9 +16,6 @@
class Player;
const int CARD_WIDTH = 72;
const int CARD_HEIGHT = 102;
class AbstractCardItem : public ArrowTarget
{
Q_OBJECT

View file

@ -8,6 +8,7 @@
#define COUNTER_H
#include "../../interface/widgets/menus/tearoff_menu.h"
#include "../player/menu/abstract_player_component.h"
#include <QGraphicsItem>
#include <QInputDialog>
@ -18,7 +19,7 @@ class QKeyEvent;
class QMenu;
class QString;
class AbstractCounter : public QObject, public QGraphicsItem
class AbstractCounter : public QObject, public QGraphicsItem, public AbstractPlayerComponent
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
@ -56,10 +57,10 @@ public:
QGraphicsItem *parent = nullptr);
~AbstractCounter() override;
void retranslateUi();
void retranslateUi() override;
void setValue(int _value);
void setShortcutsActive();
void setShortcutsInactive();
void setShortcutsActive() override;
void setShortcutsInactive() override;
void delCounter();
QMenu *getMenu() const

View file

@ -5,6 +5,7 @@
#include "../player/player.h"
#include "../player/player_actions.h"
#include "../player/player_target.h"
#include "../z_values.h"
#include "../zones/card_zone.h"
#include "card_item.h"
@ -18,12 +19,13 @@
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h>
ArrowItem::ArrowItem(Player *_player, int _id, ArrowTarget *_startItem, ArrowTarget *_targetItem, const QColor &_color)
: QGraphicsItem(), player(_player), id(_id), startItem(_startItem), targetItem(_targetItem), targetLocked(false),
color(_color), fullColor(true)
{
setZValue(2000000005);
setZValue(ZValues::ARROWS);
if (startItem)
startItem->addArrowFrom(this);
@ -238,16 +240,16 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
}
// if the card is in hand then we will move the card to stack or table as part of drawing the arrow
if (startZone->getName() == "hand") {
if (startZone->getName() == ZoneNames::HAND) {
startCard->playCard(false);
CardInfoPtr ci = startCard->getCard().getCardPtr();
bool playToStack = SettingsCache::instance().getPlayToStack();
if (ci &&
((!playToStack && ci->getUiAttributes().tableRow == 3) ||
(playToStack && ci->getUiAttributes().tableRow != 0 && startCard->getZone()->getName() != "stack")))
cmd.set_start_zone("stack");
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
startCard->getZone()->getName() != ZoneNames::STACK)))
cmd.set_start_zone(ZoneNames::STACK);
else
cmd.set_start_zone(playToStack ? "stack" : "table");
cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE);
}
if (deleteInPhase != 0) {
@ -317,7 +319,7 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCard)
{
// do nothing if target is already attached to another card or is not in play
if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != "table") {
if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) {
return;
}
@ -325,12 +327,12 @@ void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCar
CardZoneLogic *targetZone = targetCard->getZone();
// move card onto table first if attaching from some other zone
if (startZone->getName() != "table") {
if (startZone->getName() != ZoneNames::TABLE) {
player->getPlayerActions()->playCardToTable(startCard, false);
}
Command_AttachCard cmd;
cmd.set_start_zone("table");
cmd.set_start_zone(ZoneNames::TABLE);
cmd.set_card_id(startCard->getId());
cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(targetZone->getName().toStdString());

View file

@ -13,9 +13,10 @@
CardDragItem::CardDragItem(CardItem *_item,
int _id,
const QPointF &_hotSpot,
bool _faceDown,
bool _forceFaceDown,
AbstractCardDragItem *parentDrag)
: AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), faceDown(_faceDown), occupied(false), currentZone(0)
: AbstractCardDragItem(_item, _hotSpot, parentDrag), id(_id), forceFaceDown(_forceFaceDown), occupied(false),
currentZone(0)
{
}

View file

@ -16,7 +16,7 @@ class CardDragItem : public AbstractCardDragItem
Q_OBJECT
private:
int id;
bool faceDown;
bool forceFaceDown;
bool occupied;
CardZone *currentZone;
@ -24,15 +24,15 @@ public:
CardDragItem(CardItem *_item,
int _id,
const QPointF &_hotSpot,
bool _faceDown,
bool _forceFaceDown,
AbstractCardDragItem *parentDrag = 0);
int getId() const
{
return id;
}
bool getFaceDown() const
bool isForceFaceDown() const
{
return faceDown;
return forceFaceDown;
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
void updatePosition(const QPointF &cursorScenePos) override;

View file

@ -212,10 +212,12 @@ void CardItem::setAttachedTo(CardItem *_attachedTo)
}
}
/**
* @brief Resets the fields that should be reset after a zone transition
*/
void CardItem::resetState(bool keepAnnotations)
{
attacking = false;
facedown = false;
counters.clear();
pt.clear();
if (!keepAnnotations) {
@ -251,10 +253,10 @@ void CardItem::processCardInfo(const ServerInfo_Card &_info)
setDoesntUntap(_info.doesnt_untap());
}
CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool faceDown)
CardDragItem *CardItem::createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown)
{
deleteDragItem();
dragItem = new CardDragItem(this, _id, _pos, faceDown);
dragItem = new CardDragItem(this, _id, _pos, forceFaceDown);
dragItem->setVisible(false);
scene()->addItem(dragItem);
dragItem->updatePosition(_scenePos);
@ -352,7 +354,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
// Use the buttonDownPos to align the hot spot with the position when
// the user originally clicked
createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), facedown || forceFaceDown);
createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
dragItem->grabMouse();
int childIndex = 0;
@ -365,7 +367,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
if (zone->getHasCardAttr())
childPos = card->pos() - pos();
else
childPos = QPointF(childIndex * CARD_WIDTH / 2, 0);
childPos = QPointF(childIndex * CardDimensions::WIDTH_HALF_F, 0);
CardDragItem *drag =
new CardDragItem(card, card->getId(), childPos, card->getFaceDown() || forceFaceDown, dragItem);
drag->setPos(dragItem->pos() + childPos);
@ -474,9 +476,9 @@ bool CardItem::animationEvent()
}
setTransform(QTransform()
.translate(CARD_WIDTH_HALF, CARD_HEIGHT_HALF)
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(tapAngle)
.translate(-CARD_WIDTH_HALF, -CARD_HEIGHT_HALF));
.translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F));
setHovered(false);
update();

View file

@ -21,8 +21,6 @@ class QAction;
class QColor;
const int MAX_COUNTERS_ON_CARD = 999;
const float CARD_WIDTH_HALF = CARD_WIDTH / 2;
const float CARD_HEIGHT_HALF = CARD_HEIGHT / 2;
const int ROTATION_DEGREES_PER_FRAME = 10;
class CardItem : public AbstractCardItem
@ -142,7 +140,7 @@ public:
void processCardInfo(const ServerInfo_Card &_info);
bool animationEvent();
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool faceDown);
CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown);
void deleteDragItem();
void drawArrow(const QColor &arrowColor);
void drawAttachArrow();

View file

@ -0,0 +1,29 @@
#ifndef CARD_DIMENSIONS_H
#define CARD_DIMENSIONS_H
#include <QtGlobal>
/**
* @file card_dimensions.h
* @brief Canonical card dimension constants for layout and Z-value calculations.
*
* These values represent the logical pixel dimensions of a standard card graphic.
* They are used throughout the game scene for layout, rendering, and Z-value computation.
*/
namespace CardDimensions
{
/// Card width in pixels
constexpr int WIDTH = 72;
/// Card height in pixels
constexpr int HEIGHT = 102;
/// Pre-converted for floating-point contexts (Z-value calculations)
constexpr qreal WIDTH_F = static_cast<qreal>(WIDTH);
constexpr qreal HEIGHT_F = static_cast<qreal>(HEIGHT);
/// Half-dimensions for centering and rotation transforms
constexpr qreal WIDTH_HALF_F = WIDTH_F / 2;
constexpr qreal HEIGHT_HALF_F = HEIGHT_F / 2;
} // namespace CardDimensions
#endif // CARD_DIMENSIONS_H

View file

@ -95,8 +95,9 @@ void DeckViewCard::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
pen.setJoinStyle(Qt::MiterJoin);
pen.setColor(originZone == DECK_ZONE_MAIN ? Qt::green : Qt::red);
painter->setPen(pen);
qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CARD_WIDTH - 3) : 0.0;
painter->drawRoundedRect(QRectF(1.5, 1.5, CARD_WIDTH - 3., CARD_HEIGHT - 3.), cardRadius, cardRadius);
qreal cardRadius = SettingsCache::instance().getRoundCardCorners() ? 0.05 * (CardDimensions::WIDTH_F - 3) : 0.0;
painter->drawRoundedRect(QRectF(1.5, 1.5, CardDimensions::WIDTH_F - 3, CardDimensions::HEIGHT_F - 3), cardRadius,
cardRadius);
painter->restore();
}
@ -122,7 +123,7 @@ void DeckViewCard::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
if (c == this)
continue;
++j;
auto childPos = QPointF(j * CARD_WIDTH / 2, 0);
auto childPos = QPointF(j * CardDimensions::WIDTH_HALF_F, 0);
auto *drag = new DeckViewCardDragItem(c, childPos, dragItem);
drag->setPos(dragItem->pos() + childPos);
scene()->addItem(drag);
@ -204,7 +205,7 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI
painter->setPen(QColor(255, 255, 255, 100));
painter->drawLine(QPointF(0, yUntilNow - paddingY / 2), QPointF(width, yUntilNow - paddingY / 2));
}
qreal thisRowHeight = CARD_HEIGHT * currentRowsAndCols[i].first;
qreal thisRowHeight = CardDimensions::HEIGHT_F * currentRowsAndCols[i].first;
QRectF textRect(0, yUntilNow, totalTextWidth, thisRowHeight);
yUntilNow += thisRowHeight + paddingY;
@ -260,9 +261,9 @@ QSizeF DeckViewCardContainer::calculateBoundingRect(const QList<QPair<int, int>>
// Calculate space needed for cards
for (int i = 0; i < rowsAndCols.size(); ++i) {
totalHeight += CARD_HEIGHT * rowsAndCols[i].first + paddingY;
if (CARD_WIDTH * rowsAndCols[i].second > totalWidth)
totalWidth = CARD_WIDTH * rowsAndCols[i].second;
totalHeight += CardDimensions::HEIGHT_F * rowsAndCols[i].first + paddingY;
if (CardDimensions::WIDTH_F * rowsAndCols[i].second > totalWidth)
totalWidth = CardDimensions::WIDTH_F * rowsAndCols[i].second;
}
return QSizeF(getCardTypeTextWidth() + totalWidth, totalHeight + separatorY + paddingY);
@ -289,9 +290,10 @@ void DeckViewCardContainer::rearrangeItems(const QList<QPair<int, int>> &rowsAnd
std::sort(row.begin(), row.end(), DeckViewCardContainer::sortCardsByName);
for (int j = 0; j < row.size(); ++j) {
DeckViewCard *card = row[j];
card->setPos(x + (j % tempCols) * CARD_WIDTH, yUntilNow + (j / tempCols) * CARD_HEIGHT);
card->setPos(x + (j % tempCols) * CardDimensions::WIDTH_F,
yUntilNow + (j / tempCols) * CardDimensions::HEIGHT_F);
}
yUntilNow += tempRows * CARD_HEIGHT + paddingY;
yUntilNow += tempRows * CardDimensions::HEIGHT_F + paddingY;
}
prepareGeometryChange();
@ -392,7 +394,7 @@ void DeckViewScene::applySideboardPlan(const QList<MoveCard_ToZone> &plan)
void DeckViewScene::rearrangeItems()
{
const int spacing = CARD_HEIGHT / 3;
const int spacing = CardDimensions::HEIGHT / 3;
QList<DeckViewCardContainer *> contList = cardContainers.values();
// Initialize space requirements

View file

@ -260,16 +260,15 @@ void DeckViewContainer::loadLocalDeck()
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
{
DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath);
DeckLoader deck(this);
bool success = deck.loadFromFile(filePath, fmt, true);
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(filePath, fmt, true);
if (!success) {
if (!deckOpt) {
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
return;
}
loadDeckFromDeckList(deck.getDeck().deckList);
loadDeckFromDeckList(deckOpt.value().deckList);
}
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)

View file

@ -146,13 +146,13 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
setWindowTitle(tr("Create token"));
resize(600, 500);
restoreGeometry(SettingsCache::instance().getTokenDialogGeometry());
restoreGeometry(SettingsCache::instance().layouts().getTokenDialogGeometry());
}
void DlgCreateToken::closeEvent(QCloseEvent *event)
{
event->accept();
SettingsCache::instance().setTokenDialogGeometry(saveGeometry());
SettingsCache::instance().layouts().setTokenDialogGeometry(saveGeometry());
}
void DlgCreateToken::faceDownCheckBoxToggled(bool checked)
@ -225,13 +225,13 @@ void DlgCreateToken::actChooseTokenFromDeck(bool checked)
void DlgCreateToken::actOk()
{
SettingsCache::instance().setTokenDialogGeometry(saveGeometry());
SettingsCache::instance().layouts().setTokenDialogGeometry(saveGeometry());
accept();
}
void DlgCreateToken::actReject()
{
SettingsCache::instance().setTokenDialogGeometry(saveGeometry());
SettingsCache::instance().layouts().setTokenDialogGeometry(saveGeometry());
reject();
}

View file

@ -12,8 +12,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/filters/filter_string.h>
DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, uint _numberOfHits, bool autoPlay)
: QDialog(parent)
DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, const MoveTopCardsUntilOptions &options) : QDialog(parent)
{
exprLabel = new QLabel(tr("Card name (or search expressions):"));
@ -21,13 +20,13 @@ DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, u
exprComboBox->setFocus();
exprComboBox->setEditable(true);
exprComboBox->setInsertPolicy(QComboBox::InsertAtTop);
exprComboBox->insertItems(0, exprs);
exprComboBox->insertItems(0, options.exprs);
exprLabel->setBuddy(exprComboBox);
numberOfHitsLabel = new QLabel(tr("Number of hits:"));
numberOfHitsEdit = new QSpinBox(this);
numberOfHitsEdit->setRange(1, 99);
numberOfHitsEdit->setValue(_numberOfHits);
numberOfHitsEdit->setValue(options.numberOfHits);
numberOfHitsLabel->setBuddy(numberOfHitsEdit);
auto *grid = new QGridLayout;
@ -35,7 +34,7 @@ DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, QStringList exprs, u
grid->addWidget(numberOfHitsEdit, 0, 1);
autoPlayCheckBox = new QCheckBox(tr("Auto play hits"));
autoPlayCheckBox->setChecked(autoPlay);
autoPlayCheckBox->setChecked(options.autoPlay);
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgMoveTopCardsUntil::validateAndAccept);
@ -118,6 +117,13 @@ QString DlgMoveTopCardsUntil::getExpr() const
return exprComboBox->currentText();
}
MoveTopCardsUntilOptions DlgMoveTopCardsUntil::getOptions() const
{
return {.exprs = getExprs(),
.numberOfHits = numberOfHitsEdit->text().toInt(),
.autoPlay = autoPlayCheckBox->isChecked()};
}
QStringList DlgMoveTopCardsUntil::getExprs() const
{
QStringList exprs;
@ -125,14 +131,4 @@ QStringList DlgMoveTopCardsUntil::getExprs() const
exprs.append(exprComboBox->itemText(i));
}
return exprs;
}
uint DlgMoveTopCardsUntil::getNumberOfHits() const
{
return numberOfHitsEdit->text().toUInt();
}
bool DlgMoveTopCardsUntil::isAutoPlay() const
{
return autoPlayCheckBox->isChecked();
}
}

View file

@ -16,6 +16,13 @@
class FilterString;
struct MoveTopCardsUntilOptions
{
QStringList exprs = {};
int numberOfHits = 1;
bool autoPlay = false;
};
class DlgMoveTopCardsUntil : public QDialog
{
Q_OBJECT
@ -29,15 +36,12 @@ class DlgMoveTopCardsUntil : public QDialog
void validateAndAccept();
bool validateMatchExists(const FilterString &filterString);
public:
explicit DlgMoveTopCardsUntil(QWidget *parent = nullptr,
QStringList exprs = QStringList(),
uint numberOfHits = 1,
bool autoPlay = false);
[[nodiscard]] QString getExpr() const;
[[nodiscard]] QStringList getExprs() const;
[[nodiscard]] uint getNumberOfHits() const;
[[nodiscard]] bool isAutoPlay() const;
public:
explicit DlgMoveTopCardsUntil(QWidget *parent = nullptr, const MoveTopCardsUntilOptions &options = {});
[[nodiscard]] QString getExpr() const;
[[nodiscard]] MoveTopCardsUntilOptions getOptions() const;
};
#endif // DLG_MOVE_TOP_CARDS_UNTIL_H

View file

@ -429,13 +429,13 @@ void GameEventHandler::eventLeave(const Event_Leave &event, int eventPlayerId, c
if (!player)
return;
player->clear();
emit playerLeft(eventPlayerId);
emit logLeave(player, getLeaveReason(event.reason()));
game->getPlayerManager()->removePlayer(eventPlayerId);
player->clear();
player->deleteLater();
// Rearrange all remaining zones so that attachment relationship updates take place

View file

@ -14,6 +14,7 @@
#include <QGraphicsView>
#include <QSet>
#include <QtMath>
#include <libcockatrice/utility/zone_names.h>
#include <numeric>
/**
@ -410,9 +411,9 @@ void GameScene::toggleZoneView(Player *player, const QString &zoneName, int numb
connect(item, &ZoneViewWidget::closePressed, this, &GameScene::removeZoneView);
addItem(item);
if (zoneName == "grave")
if (zoneName == ZoneNames::GRAVE)
item->setPos(360, 100);
else if (zoneName == "rfg")
else if (zoneName == ZoneNames::EXILE)
item->setPos(380, 120);
else
item->setPos(340, 80);
@ -520,9 +521,9 @@ void GameScene::startRubberBand(const QPointF &selectionOrigin)
emit sigStartRubberBand(selectionOrigin);
}
void GameScene::resizeRubberBand(const QPointF &cursorPoint)
void GameScene::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
{
emit sigResizeRubberBand(cursorPoint);
emit sigResizeRubberBand(cursorPoint, selectedCount);
}
void GameScene::stopRubberBand()

View file

@ -163,7 +163,7 @@ public:
/** Unregisters a card from animation updates. */
void unregisterAnimationItem(AbstractCardItem *card);
void startRubberBand(const QPointF &selectionOrigin);
void resizeRubberBand(const QPointF &cursorPoint);
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
void stopRubberBand();
public slots:
@ -196,7 +196,7 @@ protected:
signals:
void sigStartRubberBand(const QPointF &selectionOrigin);
void sigResizeRubberBand(const QPointF &cursorPoint);
void sigResizeRubberBand(const QPointF &cursorPoint, int selectedCount);
void sigStopRubberBand();
};

View file

@ -4,9 +4,32 @@
#include "game_scene.h"
#include <QAction>
#include <QLabel>
#include <QResizeEvent>
#include <QRubberBand>
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it.
class SelectionRubberBand : public QRubberBand
{
public:
using QRubberBand::QRubberBand;
protected:
void showEvent(QShowEvent *event) override
{
QWidget::showEvent(event); // Skip QRubberBand's raise()
}
void changeEvent(QEvent *event) override
{
if (event->type() == QEvent::ZOrderChange) {
return; // Skip QRubberBand's raise() on z-order changes
}
QRubberBand::changeEvent(event);
}
};
GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0)
{
setBackgroundBrush(QBrush(QColor(0, 0, 0)));
@ -19,6 +42,7 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand);
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
aCloseMostRecentZoneView = new QAction(this);
@ -27,7 +51,23 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&GameView::refreshShortcuts);
refreshShortcuts();
rubberBand = new QRubberBand(QRubberBand::Rectangle, this);
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
const QString countLabelStyle = "color: white; "
"font-size: 14px; "
"font-weight: bold; "
"background-color: rgba(0, 0, 0, 160); "
"border-radius: 3px; "
"padding: 1px 2px;";
dragCountLabel = new QLabel(this);
dragCountLabel->setStyleSheet(countLabelStyle);
dragCountLabel->hide();
dragCountLabel->raise();
totalCountLabel = new QLabel(this);
totalCountLabel->setStyleSheet(countLabelStyle);
totalCountLabel->hide();
}
void GameView::resizeEvent(QResizeEvent *event)
@ -39,6 +79,7 @@ void GameView::resizeEvent(QResizeEvent *event)
s->processViewSizeChange(event->size());
updateSceneRect(scene()->sceneRect());
updateTotalSelectionCount(event->size());
}
void GameView::updateSceneRect(const QRectF &rect)
@ -48,20 +89,67 @@ void GameView::updateSceneRect(const QRectF &rect)
void GameView::startRubberBand(const QPointF &_selectionOrigin)
{
if (!rubberBand)
return;
selectionOrigin = _selectionOrigin;
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
rubberBand->show();
}
void GameView::resizeRubberBand(const QPointF &cursorPoint)
void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
{
if (rubberBand)
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), cursorPoint.toPoint()).normalized());
if (!rubberBand)
return;
constexpr int kLabelPaddingInPixels = 4;
QPoint cursor = cursorPoint.toPoint();
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
rubberBand->setGeometry(rect);
if (!SettingsCache::instance().getShowDragSelectionCount()) {
dragCountLabel->hide();
return;
}
if (selectedCount > 0) {
dragCountLabel->setText(QString::number(selectedCount));
dragCountLabel->adjustSize();
QSize labelSize = dragCountLabel->size();
if (rect.width() < labelSize.width() + 2 * kLabelPaddingInPixels ||
rect.height() < labelSize.height() + 2 * kLabelPaddingInPixels) {
dragCountLabel->hide();
return;
}
const int minX = rect.left() + kLabelPaddingInPixels;
const int minY = rect.top() + kLabelPaddingInPixels;
int x = qMax(minX, cursor.x() - labelSize.width() - kLabelPaddingInPixels);
int y = qMax(minY, cursor.y() - labelSize.height() - kLabelPaddingInPixels);
bool isAtTopLeftCorner = (x == minX) && (y == minY);
if (isAtTopLeftCorner) {
constexpr int kCursorClearanceInPixels = 16;
x = qMin(cursor.x() + kCursorClearanceInPixels, rect.right() - labelSize.width() - kLabelPaddingInPixels);
}
dragCountLabel->move(x, y);
dragCountLabel->show();
} else {
dragCountLabel->hide();
}
}
void GameView::stopRubberBand()
{
if (!rubberBand)
return;
rubberBand->hide();
dragCountLabel->hide();
}
void GameView::refreshShortcuts()
@ -69,3 +157,28 @@ void GameView::refreshShortcuts()
aCloseMostRecentZoneView->setShortcuts(
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
}
void GameView::updateTotalSelectionCount(const QSize &viewSize)
{
if (!SettingsCache::instance().getShowTotalSelectionCount()) {
totalCountLabel->hide();
return;
}
int count = scene()->selectedItems().count();
if (count > 1) {
totalCountLabel->setText(QString::number(count));
totalCountLabel->adjustSize();
constexpr int kMarginInPixels = 10;
int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width();
int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height();
int x = availableWidth - totalCountLabel->width() - kMarginInPixels;
int y = availableHeight - totalCountLabel->height() - kMarginInPixels;
totalCountLabel->move(x, y);
totalCountLabel->show();
} else {
totalCountLabel->hide();
}
}

View file

@ -10,6 +10,7 @@
#include <QGraphicsView>
class GameScene;
class QLabel;
class QRubberBand;
class GameView : public QGraphicsView
@ -18,15 +19,18 @@ class GameView : public QGraphicsView
private:
QAction *aCloseMostRecentZoneView;
QRubberBand *rubberBand;
QLabel *dragCountLabel;
QLabel *totalCountLabel;
QPointF selectionOrigin;
protected:
void resizeEvent(QResizeEvent *event) override;
private slots:
void startRubberBand(const QPointF &selectionOrigin);
void resizeRubberBand(const QPointF &cursorPoint);
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
void stopRubberBand();
void refreshShortcuts();
void updateTotalSelectionCount(const QSize &viewSize = QSize());
public slots:
void updateSceneRect(const QRectF &rect);

View file

@ -10,16 +10,9 @@
#include <../../client/settings/card_counter_settings.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
#include <libcockatrice/utility/zone_names.h>
#include <utility>
static const QString TABLE_ZONE_NAME = "table";
static const QString GRAVE_ZONE_NAME = "grave";
static const QString EXILE_ZONE_NAME = "rfg";
static const QString HAND_ZONE_NAME = "hand";
static const QString DECK_ZONE_NAME = "deck";
static const QString SIDEBOARD_ZONE_NAME = "sb";
static const QString STACK_ZONE_NAME = "stack";
static QString sanitizeHtml(QString dirty)
{
return dirty.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
@ -37,15 +30,15 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
QString fromStr;
QString zoneName = zone->getName();
if (zoneName == TABLE_ZONE_NAME) {
if (zoneName == ZoneNames::TABLE) {
fromStr = tr(" from play");
} else if (zoneName == GRAVE_ZONE_NAME) {
} else if (zoneName == ZoneNames::GRAVE) {
fromStr = tr(" from their graveyard");
} else if (zoneName == EXILE_ZONE_NAME) {
} else if (zoneName == ZoneNames::EXILE) {
fromStr = tr(" from exile");
} else if (zoneName == HAND_ZONE_NAME) {
} else if (zoneName == ZoneNames::HAND) {
fromStr = tr(" from their hand");
} else if (zoneName == DECK_ZONE_NAME) {
} else if (zoneName == ZoneNames::DECK) {
if (position == 0) {
if (cardName.isEmpty()) {
if (ownerChange) {
@ -61,7 +54,7 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
fromStr = tr(" from the top of their library");
}
}
} else if (position >= zone->getCards().size() - 1) {
} else if (position == zone->getCards().size()) {
if (cardName.isEmpty()) {
if (ownerChange) {
cardName = tr("the bottom card of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName());
@ -83,9 +76,9 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
fromStr = tr(" from their library");
}
}
} else if (zoneName == SIDEBOARD_ZONE_NAME) {
} else if (zoneName == ZoneNames::SIDEBOARD) {
fromStr = tr(" from sideboard");
} else if (zoneName == STACK_ZONE_NAME) {
} else if (zoneName == ZoneNames::STACK) {
fromStr = tr(" from the stack");
} else {
fromStr = tr(" from custom zone '%1'").arg(zoneName);
@ -275,9 +268,9 @@ void MessageLogWidget::logMoveCard(Player *player,
bool ownerChanged = startZone->getPlayer() != targetZone->getPlayer();
// do not log if moved within the same zone
if ((startZoneName == TABLE_ZONE_NAME && targetZoneName == TABLE_ZONE_NAME && !ownerChanged) ||
(startZoneName == HAND_ZONE_NAME && targetZoneName == HAND_ZONE_NAME) ||
(startZoneName == EXILE_ZONE_NAME && targetZoneName == EXILE_ZONE_NAME)) {
if ((startZoneName == ZoneNames::TABLE && targetZoneName == ZoneNames::TABLE && !ownerChanged) ||
(startZoneName == ZoneNames::HAND && targetZoneName == ZoneNames::HAND) ||
(startZoneName == ZoneNames::EXILE && targetZoneName == ZoneNames::EXILE)) {
return;
}
@ -306,20 +299,28 @@ void MessageLogWidget::logMoveCard(Player *player,
QString finalStr;
std::optional<QString> fourthArg;
if (targetZoneName == TABLE_ZONE_NAME) {
if (targetZoneName == ZoneNames::TABLE) {
soundEngine->playSound("play_card");
if (card->getFaceDown()) {
finalStr = tr("%1 puts %2 into play%3 face down.");
} else {
finalStr = tr("%1 puts %2 into play%3.");
}
} else if (targetZoneName == GRAVE_ZONE_NAME) {
finalStr = tr("%1 puts %2%3 into their graveyard.");
} else if (targetZoneName == EXILE_ZONE_NAME) {
finalStr = tr("%1 exiles %2%3.");
} else if (targetZoneName == HAND_ZONE_NAME) {
} else if (targetZoneName == ZoneNames::GRAVE) {
if (card->getFaceDown()) {
finalStr = tr("%1 puts %2%3 into their graveyard face down.");
} else {
finalStr = tr("%1 puts %2%3 into their graveyard.");
}
} else if (targetZoneName == ZoneNames::EXILE) {
if (card->getFaceDown()) {
finalStr = tr("%1 exiles %2%3 face down.");
} else {
finalStr = tr("%1 exiles %2%3.");
}
} else if (targetZoneName == ZoneNames::HAND) {
finalStr = tr("%1 moves %2%3 to their hand.");
} else if (targetZoneName == DECK_ZONE_NAME) {
} else if (targetZoneName == ZoneNames::DECK) {
if (newX == -1) {
finalStr = tr("%1 puts %2%3 into their library.");
} else if (newX >= targetZone->getCards().size()) {
@ -331,14 +332,22 @@ void MessageLogWidget::logMoveCard(Player *player,
fourthArg = QString::number(newX);
finalStr = tr("%1 puts %2%3 into their library %4 cards from the top.");
}
} else if (targetZoneName == SIDEBOARD_ZONE_NAME) {
} else if (targetZoneName == ZoneNames::SIDEBOARD) {
finalStr = tr("%1 moves %2%3 to sideboard.");
} else if (targetZoneName == STACK_ZONE_NAME) {
} else if (targetZoneName == ZoneNames::STACK) {
soundEngine->playSound("play_card");
finalStr = tr("%1 plays %2%3.");
if (card->getFaceDown()) {
finalStr = tr("%1 plays %2%3 face down.");
} else {
finalStr = tr("%1 plays %2%3.");
}
} else {
fourthArg = targetZoneName;
finalStr = tr("%1 moves %2%3 to custom zone '%4'.");
if (card->getFaceDown()) {
finalStr = tr("%1 moves %2%3 to custom zone '%4' face down.");
} else {
finalStr = tr("%1 moves %2%3 to custom zone '%4'.");
}
}
QString message = finalStr.arg(sanitizeHtml(player->getPlayerInfo()->getName()), cardStr, nameFrom.second);

View file

@ -11,6 +11,7 @@
#include <libcockatrice/protocol/pb/command_next_turn.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/utility/zone_names.h>
PhaseButton::PhaseButton(const QString &_name, QGraphicsItem *parent, QAction *_doubleClickAction, bool _highlightable)
: QObject(), QGraphicsItem(parent), name(_name), active(false), highlightable(_highlightable),
@ -259,7 +260,7 @@ void PhasesToolbar::actNextTurn()
void PhasesToolbar::actUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone("table");
cmd.set_zone(ZoneNames::TABLE);
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");

View file

@ -9,17 +9,20 @@
enum CardMenuActionType
{
// Per-card attribute actions (must be <= cmClone for cardMenuAction() dispatch)
cmTap,
cmUntap,
cmDoesntUntap,
cmFlip,
cmPeek,
cmClone,
// Move actions (must be > cmClone for cardMenuAction() dispatch)
cmMoveToTopLibrary,
cmMoveToBottomLibrary,
cmMoveToHand,
cmMoveToGraveyard,
cmMoveToExile
cmMoveToExile,
cmMoveToTable
};
#endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H

View file

@ -0,0 +1,32 @@
/**
* @file abstract_player_component.h
* @ingroup GameMenusPlayers
* @brief Polymorphic interface for player-bound UI components managed by PlayerMenu.
*/
#ifndef COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H
#define COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H
/**
* @brief Interface for player-bound UI components that need shortcut and translation lifecycle management.
*
* Not a QObject avoids diamond inheritance with Qt's MOC. Each concrete component
* inherits QObject through its Qt base class (QMenu, TearOffMenu, QGraphicsItem, etc.)
* and this interface through regular multiple inheritance.
*/
class AbstractPlayerComponent
{
public:
virtual ~AbstractPlayerComponent() = default;
/// Bind keyboard shortcuts. Called when this player gains focus.
virtual void setShortcutsActive() = 0;
/// Unbind keyboard shortcuts. Called when this player loses focus.
virtual void setShortcutsInactive() = 0;
/// Retranslate all user-visible strings. Called on language change.
virtual void retranslateUi() = 0;
};
#endif // COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H

View file

@ -12,6 +12,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/zone_names.h>
CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive)
: player(_player), card(_card), shortcutsActive(_shortcutsActive)
@ -34,6 +35,8 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
connect(aTap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction);
aDoesntUntap = new QAction(this);
aDoesntUntap->setData(cmDoesntUntap);
aDoesntUntap->setCheckable(true);
aDoesntUntap->setChecked(card != nullptr && card->getDoesntUntap());
connect(aDoesntUntap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction);
aAttach = new QAction(this);
connect(aAttach, &QAction::triggered, playerActions, &PlayerActions::actAttach);
@ -107,36 +110,26 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
if (revealedCard) {
addAction(aHide);
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectColumn);
addRelatedCardView();
} else if (writeableCard) {
} else {
if (card->getZone()) {
if (card->getZone()->getName() == "table") {
createTableMenu();
} else if (card->getZone()->getName() == "stack") {
createStackMenu();
} else if (card->getZone()->getName() == "rfg" || card->getZone()->getName() == "grave") {
createGraveyardOrExileMenu();
if (card->getZone()->getName() == ZoneNames::TABLE) {
createTableMenu(writeableCard);
} else if (card->getZone()->getName() == ZoneNames::STACK) {
createStackMenu(writeableCard);
} else if (card->getZone()->getName() == ZoneNames::EXILE ||
card->getZone()->getName() == ZoneNames::GRAVE) {
createGraveyardOrExileMenu(writeableCard);
} else {
createHandOrCustomZoneMenu();
createHandOrCustomZoneMenu(writeableCard);
}
} else {
addMenu(new MoveMenu(player));
}
} else {
if (card->getZone() && card->getZone()->getName() != "hand") {
addAction(aDrawArrow);
addSeparator();
addRelatedCardView();
addRelatedCardActions();
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
createZonelessMenu(writeableCard);
}
}
}
@ -152,22 +145,18 @@ void CardMenu::removePlayer(Player *playerToRemove)
}
}
void CardMenu::createTableMenu()
void CardMenu::createTableMenu(bool canModifyCard)
{
// Card is on the battlefield
bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player;
if (!canModifyCard) {
addRelatedCardView();
addRelatedCardActions();
addSeparator();
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectRow);
addRelatedCardView();
addRelatedCardActions();
return;
}
@ -177,10 +166,9 @@ void CardMenu::createTableMenu()
if (card->getFaceDown()) {
addAction(aPeek);
}
addRelatedCardView();
addRelatedCardActions();
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aAttach);
if (card->getAttachedTo()) {
@ -191,9 +179,6 @@ void CardMenu::createTableMenu()
addMenu(new PtMenu(player));
addAction(aSetAnnotation);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
addAction(aSelectRow);
@ -209,67 +194,81 @@ void CardMenu::createTableMenu()
}
addSeparator();
addMenu(mCardCounters);
addRelatedCardView();
addRelatedCardActions();
}
void CardMenu::createStackMenu()
void CardMenu::createStackMenu(bool canModifyCard)
{
bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player;
// Card is on the stack
if (canModifyCard) {
addAction(aAttach);
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
} else {
if (!canModifyCard) {
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addRelatedCardView();
addRelatedCardActions();
return;
}
addAction(aPlay);
addAction(aPlayFacedown);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
addSeparator();
addAction(aSelectAll);
addRelatedCardView();
addRelatedCardActions();
}
void CardMenu::createGraveyardOrExileMenu()
void CardMenu::createGraveyardOrExileMenu(bool canModifyCard)
{
bool canModifyCard = player->getPlayerInfo()->judge || card->getOwner() == player;
// Card is in the graveyard or exile
if (canModifyCard) {
addAction(aPlay);
addAction(aPlayFacedown);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
addAction(aSelectColumn);
addSeparator();
addAction(aAttach);
if (!canModifyCard) {
addAction(aDrawArrow);
} else {
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectColumn);
addSeparator();
addAction(aDrawArrow);
addRelatedCardView();
addRelatedCardActions();
return;
}
addAction(aPlay);
addAction(aPlayFacedown);
addSeparator();
addAction(aClone);
addMenu(new MoveMenu(player));
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
addSeparator();
addAction(aSelectAll);
addAction(aSelectColumn);
addRelatedCardView();
addRelatedCardActions();
}
void CardMenu::createHandOrCustomZoneMenu()
void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
{
if (!canModifyCard) {
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addRelatedCardView();
addRelatedCardActions();
return;
}
// Card is in hand or a custom zone specified by server
addAction(aPlay);
addAction(aPlayFacedown);
@ -285,7 +284,7 @@ void CardMenu::createHandOrCustomZoneMenu()
addMenu(new MoveMenu(player));
// actions that are really wonky when done from deck or sideboard
if (card->getZone()->getName() == "hand") {
if (card->getZone()->getName() == ZoneNames::HAND) {
addSeparator();
addAction(aAttach);
addAction(aDrawArrow);
@ -298,11 +297,18 @@ void CardMenu::createHandOrCustomZoneMenu()
}
addRelatedCardView();
if (card->getZone()->getName() == "hand") {
if (card->getZone()->getName() == ZoneNames::HAND) {
addRelatedCardActions();
}
}
void CardMenu::createZonelessMenu(bool canModifyCard)
{
if (canModifyCard) {
addMenu(new MoveMenu(player));
}
}
/**
* @brief Populates the menu with an action for each active player.
*
@ -446,7 +452,7 @@ void CardMenu::retranslateUi()
aRevealToAll->setText(tr("&All players"));
//: Turn sideways or back again
aTap->setText(tr("&Tap / Untap"));
aDoesntUntap->setText(tr("Toggle &normal untapping"));
aDoesntUntap->setText(tr("Skip &untapping"));
//: Turn face up/face down
aFlip->setText(tr("T&urn Over")); // Only the user facing names in client got renamed to "turn over"
// All code and proto bits are still unchanged (flip) for compatibility reasons

View file

@ -18,10 +18,11 @@ class CardMenu : public QMenu
public:
explicit CardMenu(Player *player, const CardItem *card, bool shortcutsActive);
void removePlayer(Player *playerToRemove);
void createTableMenu();
void createStackMenu();
void createGraveyardOrExileMenu();
void createHandOrCustomZoneMenu();
void createTableMenu(bool canModifyCard);
void createStackMenu(bool canModifyCard);
void createGraveyardOrExileMenu(bool canModifyCard);
void createHandOrCustomZoneMenu(bool canModifyCard);
void createZonelessMenu(bool canModifyCard);
QMenu *mCardCounters;

View file

@ -7,15 +7,23 @@
#ifndef COCKATRICE_CUSTOM_ZONE_MENU_H
#define COCKATRICE_CUSTOM_ZONE_MENU_H
#include "abstract_player_component.h"
#include <QMenu>
class Player;
class CustomZoneMenu : public QMenu
class CustomZoneMenu : public QMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
explicit CustomZoneMenu(Player *player);
void retranslateUi();
void retranslateUi() override;
void setShortcutsActive() override
{
}
void setShortcutsInactive() override
{
}
private:
Player *player;

View file

@ -6,6 +6,7 @@
#include <QAction>
#include <QMenu>
#include <libcockatrice/utility/zone_names.h>
GraveyardMenu::GraveyardMenu(Player *_player, QWidget *parent) : TearOffMenu(parent), player(_player)
{
@ -39,16 +40,16 @@ void GraveyardMenu::createMoveActions()
if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) {
aMoveGraveToTopLibrary = new QAction(this);
aMoveGraveToTopLibrary->setData(QList<QVariant>() << "deck" << 0);
aMoveGraveToTopLibrary->setData(QList<QVariant>() << ZoneNames::DECK << 0);
aMoveGraveToBottomLibrary = new QAction(this);
aMoveGraveToBottomLibrary->setData(QList<QVariant>() << "deck" << -1);
aMoveGraveToBottomLibrary->setData(QList<QVariant>() << ZoneNames::DECK << -1);
aMoveGraveToHand = new QAction(this);
aMoveGraveToHand->setData(QList<QVariant>() << "hand" << 0);
aMoveGraveToHand->setData(QList<QVariant>() << ZoneNames::HAND << 0);
aMoveGraveToRfg = new QAction(this);
aMoveGraveToRfg->setData(QList<QVariant>() << "rfg" << 0);
aMoveGraveToRfg->setData(QList<QVariant>() << ZoneNames::EXILE << 0);
connect(aMoveGraveToTopLibrary, &QAction::triggered, grave, &PileZoneLogic::moveAllToZone);
connect(aMoveGraveToBottomLibrary, &QAction::triggered, grave, &PileZoneLogic::moveAllToZone);

View file

@ -8,12 +8,13 @@
#define COCKATRICE_GRAVE_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "abstract_player_component.h"
#include <QAction>
#include <QMenu>
class Player;
class GraveyardMenu : public TearOffMenu
class GraveyardMenu : public TearOffMenu, public AbstractPlayerComponent
{
Q_OBJECT
signals:
@ -25,9 +26,9 @@ public:
void createViewActions();
void populateRevealRandomMenuWithActivePlayers();
void onRevealRandomTriggered();
void retranslateUi();
void setShortcutsActive();
void setShortcutsInactive();
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
QMenu *mRevealRandomGraveyardCard = nullptr;
QMenu *moveGraveMenu = nullptr;

View file

@ -9,6 +9,7 @@
#include <QAction>
#include <QMenu>
#include <libcockatrice/utility/zone_names.h>
HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : TearOffMenu(parent), player(_player)
{
@ -60,19 +61,29 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T
connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan);
addAction(aMulligan);
// Mulligan same size
aMulliganSame = new QAction(this);
connect(aMulliganSame, &QAction::triggered, actions, &PlayerActions::actMulliganSameSize);
addAction(aMulliganSame);
// Mulligan -1
aMulliganMinusOne = new QAction(this);
connect(aMulliganMinusOne, &QAction::triggered, actions, &PlayerActions::actMulliganMinusOne);
addAction(aMulliganMinusOne);
addSeparator();
mMoveHandMenu = addTearOffMenu(QString());
if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) {
aMoveHandToTopLibrary = new QAction(this);
aMoveHandToTopLibrary->setData(QList<QVariant>() << "deck" << 0);
aMoveHandToTopLibrary->setData(QList<QVariant>() << ZoneNames::DECK << 0);
aMoveHandToBottomLibrary = new QAction(this);
aMoveHandToBottomLibrary->setData(QList<QVariant>() << "deck" << -1);
aMoveHandToBottomLibrary->setData(QList<QVariant>() << ZoneNames::DECK << -1);
aMoveHandToGrave = new QAction(this);
aMoveHandToGrave->setData(QList<QVariant>() << "grave" << 0);
aMoveHandToGrave->setData(QList<QVariant>() << ZoneNames::GRAVE << 0);
aMoveHandToRfg = new QAction(this);
aMoveHandToRfg->setData(QList<QVariant>() << "rfg" << 0);
aMoveHandToRfg->setData(QList<QVariant>() << ZoneNames::EXILE << 0);
auto hand = player->getHandZone();
@ -104,7 +115,9 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type"));
aSortHandByManaValue->setText(tr("Mana Value"));
aMulligan->setText(tr("Take &mulligan"));
aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
mMoveHandMenu->setTitle(tr("&Move hand to..."));
aMoveHandToTopLibrary->setText(tr("&Top of library"));
@ -128,6 +141,8 @@ void HandMenu::setShortcutsActive()
aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType"));
aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue"));
aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan"));
aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame"));
aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne"));
aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll"));
aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll"));
}

View file

@ -8,6 +8,7 @@
#define COCKATRICE_HAND_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "abstract_player_component.h"
#include <QAction>
#include <QMenu>
@ -15,7 +16,7 @@
class Player;
class PlayerActions;
class HandMenu : public TearOffMenu
class HandMenu : public TearOffMenu, public AbstractPlayerComponent
{
Q_OBJECT
@ -31,9 +32,9 @@ public:
return mRevealRandomHandCard;
}
void retranslateUi();
void setShortcutsActive();
void setShortcutsInactive();
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
private slots:
void populateRevealHandMenuWithActivePlayers();
@ -46,6 +47,8 @@ private:
QAction *aViewHand = nullptr;
QAction *aMulligan = nullptr;
QAction *aMulliganSame = nullptr;
QAction *aMulliganMinusOne = nullptr;
QMenu *mSortHand = nullptr;
QAction *aSortHandByName = nullptr;

View file

@ -51,8 +51,10 @@ LibraryMenu::LibraryMenu(Player *_player, QWidget *parent) : TearOffMenu(parent)
topLibraryMenu->addSeparator();
topLibraryMenu->addAction(aMoveTopCardToGraveyard);
topLibraryMenu->addAction(aMoveTopCardsToGraveyard);
topLibraryMenu->addAction(aMoveTopCardsToGraveyardFaceDown);
topLibraryMenu->addAction(aMoveTopCardToExile);
topLibraryMenu->addAction(aMoveTopCardsToExile);
topLibraryMenu->addAction(aMoveTopCardsToExileFaceDown);
topLibraryMenu->addAction(aMoveTopCardsUntil);
topLibraryMenu->addSeparator();
topLibraryMenu->addAction(aShuffleTopCards);
@ -66,8 +68,10 @@ LibraryMenu::LibraryMenu(Player *_player, QWidget *parent) : TearOffMenu(parent)
bottomLibraryMenu->addSeparator();
bottomLibraryMenu->addAction(aMoveBottomCardToGraveyard);
bottomLibraryMenu->addAction(aMoveBottomCardsToGraveyard);
bottomLibraryMenu->addAction(aMoveBottomCardsToGraveyardFaceDown);
bottomLibraryMenu->addAction(aMoveBottomCardToExile);
bottomLibraryMenu->addAction(aMoveBottomCardsToExile);
bottomLibraryMenu->addAction(aMoveBottomCardsToExileFaceDown);
bottomLibraryMenu->addSeparator();
bottomLibraryMenu->addAction(aShuffleBottomCards);
@ -136,8 +140,14 @@ void LibraryMenu::createMoveActions()
connect(aMoveTopCardToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToExile);
aMoveTopCardsToGraveyard = new QAction(this);
connect(aMoveTopCardsToGraveyard, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToGrave);
aMoveTopCardsToGraveyardFaceDown = new QAction(this);
connect(aMoveTopCardsToGraveyardFaceDown, &QAction::triggered, playerActions,
&PlayerActions::actMoveTopCardsToGraveFaceDown);
aMoveTopCardsToExile = new QAction(this);
connect(aMoveTopCardsToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToExile);
aMoveTopCardsToExileFaceDown = new QAction(this);
connect(aMoveTopCardsToExileFaceDown, &QAction::triggered, playerActions,
&PlayerActions::actMoveTopCardsToExileFaceDown);
aMoveTopCardsUntil = new QAction(this);
connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsUntil);
aMoveTopCardToBottom = new QAction(this);
@ -156,8 +166,14 @@ void LibraryMenu::createMoveActions()
aMoveBottomCardsToGraveyard = new QAction(this);
connect(aMoveBottomCardsToGraveyard, &QAction::triggered, playerActions,
&PlayerActions::actMoveBottomCardsToGrave);
aMoveBottomCardsToGraveyardFaceDown = new QAction(this);
connect(aMoveBottomCardsToGraveyardFaceDown, &QAction::triggered, playerActions,
&PlayerActions::actMoveBottomCardsToGraveFaceDown);
aMoveBottomCardsToExile = new QAction(this);
connect(aMoveBottomCardsToExile, &QAction::triggered, playerActions, &PlayerActions::actMoveBottomCardsToExile);
aMoveBottomCardsToExileFaceDown = new QAction(this);
connect(aMoveBottomCardsToExileFaceDown, &QAction::triggered, playerActions,
&PlayerActions::actMoveBottomCardsToExileFaceDown);
aMoveBottomCardToTop = new QAction(this);
connect(aMoveBottomCardToTop, &QAction::triggered, playerActions, &PlayerActions::actMoveBottomCardToTop);
}
@ -216,7 +232,9 @@ void LibraryMenu::retranslateUi()
aMoveTopCardToGraveyard->setText(tr("Move top card to grave&yard"));
aMoveTopCardToExile->setText(tr("Move top card to e&xile"));
aMoveTopCardsToGraveyard->setText(tr("Move top cards to &graveyard..."));
aMoveTopCardsToGraveyardFaceDown->setText(tr("Move top cards to graveyard face down..."));
aMoveTopCardsToExile->setText(tr("Move top cards to &exile..."));
aMoveTopCardsToExileFaceDown->setText(tr("Move top cards to exile face down..."));
aMoveTopCardsUntil->setText(tr("Put top cards on stack &until..."));
aShuffleTopCards->setText(tr("Shuffle top cards..."));
@ -227,7 +245,9 @@ void LibraryMenu::retranslateUi()
aMoveBottomCardToGraveyard->setText(tr("Move bottom card to grave&yard"));
aMoveBottomCardToExile->setText(tr("Move bottom card to e&xile"));
aMoveBottomCardsToGraveyard->setText(tr("Move bottom cards to &graveyard..."));
aMoveBottomCardsToGraveyardFaceDown->setText(tr("Move bottom cards to graveyard face down..."));
aMoveBottomCardsToExile->setText(tr("Move bottom cards to &exile..."));
aMoveBottomCardsToExileFaceDown->setText(tr("Move bottom cards to exile face down..."));
aMoveBottomCardToTop->setText(tr("Put bottom card on &top"));
aShuffleBottomCards->setText(tr("Shuffle bottom cards..."));
}
@ -335,8 +355,10 @@ void LibraryMenu::setShortcutsActive()
aMoveTopToPlayFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopToPlayFaceDown"));
aMoveTopCardToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToGraveyard"));
aMoveTopCardsToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToGraveyard"));
aMoveTopCardsToGraveyardFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToGraveyardFaceDown"));
aMoveTopCardToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToExile"));
aMoveTopCardsToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToExile"));
aMoveTopCardsToExileFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsToExileFaceDown"));
aMoveTopCardsUntil->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardsUntil"));
aMoveTopCardToBottom->setShortcuts(shortcuts.getShortcut("Player/aMoveTopCardToBottom"));
aDrawBottomCard->setShortcuts(shortcuts.getShortcut("Player/aDrawBottomCard"));
@ -345,8 +367,10 @@ void LibraryMenu::setShortcutsActive()
aMoveBottomToPlayFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomToPlayFaceDown"));
aMoveBottomCardToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToGrave"));
aMoveBottomCardsToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToGrave"));
aMoveBottomCardsToGraveyardFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToGraveFaceDown"));
aMoveBottomCardToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToExile"));
aMoveBottomCardsToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToExile"));
aMoveBottomCardsToExileFaceDown->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardsToExileFaceDown"));
aMoveBottomCardToTop->setShortcuts(shortcuts.getShortcut("Player/aMoveBottomCardToTop"));
}
@ -367,8 +391,10 @@ void LibraryMenu::setShortcutsInactive()
aMoveTopToPlayFaceDown->setShortcut(QKeySequence());
aMoveTopCardToGraveyard->setShortcut(QKeySequence());
aMoveTopCardsToGraveyard->setShortcut(QKeySequence());
aMoveTopCardsToGraveyardFaceDown->setShortcut(QKeySequence());
aMoveTopCardToExile->setShortcut(QKeySequence());
aMoveTopCardsToExile->setShortcut(QKeySequence());
aMoveTopCardsToExileFaceDown->setShortcut(QKeySequence());
aMoveTopCardsUntil->setShortcut(QKeySequence());
aDrawBottomCard->setShortcut(QKeySequence());
aDrawBottomCards->setShortcut(QKeySequence());
@ -376,6 +402,8 @@ void LibraryMenu::setShortcutsInactive()
aMoveBottomToPlayFaceDown->setShortcut(QKeySequence());
aMoveBottomCardToGraveyard->setShortcut(QKeySequence());
aMoveBottomCardsToGraveyard->setShortcut(QKeySequence());
aMoveBottomCardsToGraveyardFaceDown->setShortcut(QKeySequence());
aMoveBottomCardToExile->setShortcut(QKeySequence());
aMoveBottomCardsToExile->setShortcut(QKeySequence());
aMoveBottomCardsToExileFaceDown->setShortcut(QKeySequence());
}

View file

@ -8,6 +8,7 @@
#define COCKATRICE_LIBRARY_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "abstract_player_component.h"
#include <QAction>
#include <QMenu>
@ -15,7 +16,7 @@
class Player;
class PlayerActions;
class LibraryMenu : public TearOffMenu
class LibraryMenu : public TearOffMenu, public AbstractPlayerComponent
{
Q_OBJECT
public slots:
@ -28,15 +29,15 @@ public:
void createShuffleActions();
void createMoveActions();
void createViewActions();
void retranslateUi();
void retranslateUi() override;
void populateRevealLibraryMenuWithActivePlayers();
void populateLendLibraryMenuWithActivePlayers();
void populateRevealTopCardMenuWithActivePlayers();
void onRevealLibraryTriggered();
void onLendLibraryTriggered();
void onRevealTopCardTriggered();
void setShortcutsActive();
void setShortcutsInactive();
void setShortcutsActive() override;
void setShortcutsInactive() override;
[[nodiscard]] bool isAlwaysRevealTopCardChecked() const
{
@ -88,7 +89,9 @@ public:
QAction *aMoveTopCardToGraveyard = nullptr;
QAction *aMoveTopCardToExile = nullptr;
QAction *aMoveTopCardsToGraveyard = nullptr;
QAction *aMoveTopCardsToGraveyardFaceDown = nullptr;
QAction *aMoveTopCardsToExile = nullptr;
QAction *aMoveTopCardsToExileFaceDown = nullptr;
QAction *aMoveTopCardsUntil = nullptr;
QAction *aShuffleTopCards = nullptr;
@ -100,7 +103,9 @@ public:
QAction *aMoveBottomCardToGraveyard = nullptr;
QAction *aMoveBottomCardToExile = nullptr;
QAction *aMoveBottomCardsToGraveyard = nullptr;
QAction *aMoveBottomCardsToGraveyardFaceDown = nullptr;
QAction *aMoveBottomCardsToExile = nullptr;
QAction *aMoveBottomCardsToExileFaceDown = nullptr;
QAction *aShuffleBottomCards = nullptr;
int defaultNumberTopCards = 1;

View file

@ -11,6 +11,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
aMoveToBottomLibrary = new QAction(this);
aMoveToBottomLibrary->setData(cmMoveToBottomLibrary);
aMoveToXfromTopOfLibrary = new QAction(this);
aMoveToTable = new QAction(this);
aMoveToTable->setData(cmMoveToTable);
aMoveToGraveyard = new QAction(this);
aMoveToHand = new QAction(this);
aMoveToHand->setData(cmMoveToHand);
@ -22,6 +24,7 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(),
&PlayerActions::actMoveCardXCardsFromTop);
connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction);
@ -30,6 +33,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to"))
addAction(aMoveToXfromTopOfLibrary);
addAction(aMoveToBottomLibrary);
addSeparator();
addAction(aMoveToTable);
addSeparator();
addAction(aMoveToHand);
addSeparator();
addAction(aMoveToGraveyard);
@ -47,6 +52,7 @@ void MoveMenu::setShortcutsActive()
aMoveToTopLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToTopLibrary"));
aMoveToBottomLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToBottomLibrary"));
aMoveToTable->setShortcuts(shortcuts.getShortcut("Player/aMoveToTable"));
aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand"));
aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard"));
aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile"));
@ -57,7 +63,8 @@ void MoveMenu::retranslateUi()
aMoveToTopLibrary->setText(tr("&Top of library in random order"));
aMoveToXfromTopOfLibrary->setText(tr("X cards from the top of library..."));
aMoveToBottomLibrary->setText(tr("&Bottom of library in random order"));
aMoveToTable->setText(tr("T&able"));
aMoveToHand->setText(tr("&Hand"));
aMoveToGraveyard->setText(tr("&Graveyard"));
aMoveToExile->setText(tr("&Exile"));
}
}

View file

@ -23,6 +23,7 @@ public:
QAction *aMoveToBottomLibrary = nullptr;
QAction *aMoveToHand = nullptr;
QAction *aMoveToTable = nullptr;
QAction *aMoveToGraveyard = nullptr;
QAction *aMoveToExile = nullptr;
};

View file

@ -10,38 +10,29 @@
#include <libcockatrice/protocol/pb/command_reveal_cards.pb.h>
PlayerMenu::PlayerMenu(Player *_player) : player(_player)
PlayerMenu::PlayerMenu(Player *_player) : QObject(_player), player(_player)
{
playerMenu = new TearOffMenu();
if (player->getPlayerInfo()->getLocalOrJudge()) {
handMenu = new HandMenu(player, player->getPlayerActions(), playerMenu);
playerMenu->addMenu(handMenu);
libraryMenu = new LibraryMenu(player, playerMenu);
playerMenu->addMenu(libraryMenu);
handMenu = addManagedMenu<HandMenu>(player, player->getPlayerActions(), playerMenu);
libraryMenu = addManagedMenu<LibraryMenu>(player, playerMenu);
} else {
handMenu = nullptr;
libraryMenu = nullptr;
}
graveMenu = new GraveyardMenu(player, playerMenu);
playerMenu->addMenu(graveMenu);
rfgMenu = new RfgMenu(player, playerMenu);
playerMenu->addMenu(rfgMenu);
graveMenu = addManagedMenu<GraveyardMenu>(player, playerMenu);
rfgMenu = addManagedMenu<RfgMenu>(player, playerMenu);
if (player->getPlayerInfo()->getLocalOrJudge()) {
sideboardMenu = new SideboardMenu(player, playerMenu);
playerMenu->addMenu(sideboardMenu);
customZonesMenu = new CustomZoneMenu(player);
playerMenu->addMenu(customZonesMenu);
sideboardMenu = addManagedMenu<SideboardMenu>(player, playerMenu);
customZonesMenu = addManagedMenu<CustomZoneMenu>(player);
playerMenu->addSeparator();
countersMenu = playerMenu->addMenu(QString());
utilityMenu = new UtilityMenu(player, playerMenu);
utilityMenu = createManagedComponent<UtilityMenu>(player, playerMenu);
} else {
sideboardMenu = nullptr;
customZonesMenu = nullptr;
@ -50,8 +41,7 @@ PlayerMenu::PlayerMenu(Player *_player) : player(_player)
}
if (player->getPlayerInfo()->getLocal()) {
sayMenu = new SayMenu(player);
playerMenu->addMenu(sayMenu);
sayMenu = addManagedMenu<SayMenu>(player);
} else {
sayMenu = nullptr;
}
@ -99,40 +89,18 @@ void PlayerMenu::retranslateUi()
{
playerMenu->setTitle(tr("Player \"%1\"").arg(player->getPlayerInfo()->getName()));
if (handMenu) {
handMenu->retranslateUi();
}
if (libraryMenu) {
libraryMenu->retranslateUi();
}
graveMenu->retranslateUi();
rfgMenu->retranslateUi();
if (sideboardMenu) {
sideboardMenu->retranslateUi();
for (auto *component : managedComponents) {
component->retranslateUi();
}
if (countersMenu) {
countersMenu->setTitle(tr("&Counters"));
}
if (customZonesMenu) {
customZonesMenu->retranslateUi();
}
QMapIterator<int, AbstractCounter *> counterIterator(player->getCounters());
while (counterIterator.hasNext()) {
counterIterator.next().value()->retranslateUi();
}
if (utilityMenu) {
utilityMenu->retranslateUi();
}
if (sayMenu) {
sayMenu->setTitle(tr("S&ay"));
}
}
void PlayerMenu::refreshShortcuts()
@ -153,52 +121,29 @@ void PlayerMenu::setShortcutsActive()
{
shortcutsActive = true;
if (handMenu) {
handMenu->setShortcutsActive();
}
if (libraryMenu) {
libraryMenu->setShortcutsActive();
}
graveMenu->setShortcutsActive();
// No shortcuts for RfgMenu yet
if (sideboardMenu) {
sideboardMenu->setShortcutsActive();
for (auto *component : managedComponents) {
component->setShortcutsActive();
}
// Counters implement AbstractPlayerComponent but are iterated via Player::counters
// (the authoritative source) rather than managedComponents to avoid a redundant
// list that must stay in sync with the map.
QMapIterator<int, AbstractCounter *> counterIterator(player->getCounters());
while (counterIterator.hasNext()) {
counterIterator.next().value()->setShortcutsActive();
}
if (utilityMenu) {
utilityMenu->setShortcutsActive();
}
}
void PlayerMenu::setShortcutsInactive()
{
shortcutsActive = false;
if (handMenu) {
handMenu->setShortcutsInactive();
}
if (libraryMenu) {
libraryMenu->setShortcutsInactive();
}
graveMenu->setShortcutsInactive();
// No shortcuts for RfgMenu yet
if (sideboardMenu) {
sideboardMenu->setShortcutsInactive();
for (auto *component : managedComponents) {
component->setShortcutsInactive();
}
QMapIterator<int, AbstractCounter *> counterIterator(player->getCounters());
while (counterIterator.hasNext()) {
counterIterator.next().value()->setShortcutsInactive();
}
if (utilityMenu) {
utilityMenu->setShortcutsInactive();
}
}

View file

@ -1,7 +1,7 @@
/**
* @file player_menu.h
* @ingroup GameMenusPlayers
* @brief TODO: Document this.
* @brief Orchestrates lifecycle management for all player-bound UI components.
*/
#ifndef COCKATRICE_PLAYER_MENU_H
@ -18,6 +18,7 @@
#include "sideboard_menu.h"
#include "utility_menu.h"
#include <QList>
#include <QMenu>
#include <QObject>
@ -36,7 +37,8 @@ private slots:
void refreshShortcuts();
public:
PlayerMenu(Player *player);
explicit PlayerMenu(Player *player);
/// Lifecycle methods: delegate to all managedComponents, plus counters separately via player->getCounters().
void retranslateUi();
QMenu *updateCardMenu(const CardItem *card);
@ -66,7 +68,9 @@ public:
return shortcutsActive;
}
/// Delegates to all managedComponents, plus counters separately.
void setShortcutsActive();
/// Delegates to all managedComponents, plus counters separately.
void setShortcutsInactive();
private:
@ -82,9 +86,26 @@ private:
SayMenu *sayMenu;
CustomZoneMenu *customZonesMenu;
bool shortcutsActive;
/// Drives AbstractPlayerComponent lifecycle delegation. Counters are iterated separately via player->getCounters().
QList<AbstractPlayerComponent *> managedComponents;
bool shortcutsActive = false;
void initSayMenu();
/// Creates component, adds it as a submenu of playerMenu, and registers in managedComponents.
template <typename MenuT, typename... Args> MenuT *addManagedMenu(Args &&...args)
{
auto *menu = new MenuT(std::forward<Args>(args)...);
playerMenu->addMenu(menu);
managedComponents.append(menu);
return menu;
}
/// Creates component and registers in managedComponents, but does NOT add it as a submenu.
template <typename ComponentT, typename... Args> ComponentT *createManagedComponent(Args &&...args)
{
auto *component = new ComponentT(std::forward<Args>(args)...);
managedComponents.append(component);
return component;
}
};
#endif // COCKATRICE_PLAYER_MENU_H

View file

@ -3,6 +3,8 @@
#include "../player.h"
#include "../player_actions.h"
#include <libcockatrice/utility/zone_names.h>
RfgMenu::RfgMenu(Player *_player, QWidget *parent) : TearOffMenu(parent), player(_player)
{
createMoveActions();
@ -30,13 +32,13 @@ void RfgMenu::createMoveActions()
auto rfg = player->getRfgZone();
aMoveRfgToTopLibrary = new QAction(this);
aMoveRfgToTopLibrary->setData(QList<QVariant>() << "deck" << 0);
aMoveRfgToTopLibrary->setData(QList<QVariant>() << ZoneNames::DECK << 0);
aMoveRfgToBottomLibrary = new QAction(this);
aMoveRfgToBottomLibrary->setData(QList<QVariant>() << "deck" << -1);
aMoveRfgToBottomLibrary->setData(QList<QVariant>() << ZoneNames::DECK << -1);
aMoveRfgToHand = new QAction(this);
aMoveRfgToHand->setData(QList<QVariant>() << "hand" << 0);
aMoveRfgToHand->setData(QList<QVariant>() << ZoneNames::HAND << 0);
aMoveRfgToGrave = new QAction(this);
aMoveRfgToGrave->setData(QList<QVariant>() << "grave" << 0);
aMoveRfgToGrave->setData(QList<QVariant>() << ZoneNames::GRAVE << 0);
connect(aMoveRfgToTopLibrary, &QAction::triggered, rfg, &PileZoneLogic::moveAllToZone);
connect(aMoveRfgToBottomLibrary, &QAction::triggered, rfg, &PileZoneLogic::moveAllToZone);

View file

@ -8,19 +8,26 @@
#define COCKATRICE_RFG_MENU_H
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "abstract_player_component.h"
#include <QAction>
#include <QMenu>
class Player;
class RfgMenu : public TearOffMenu
class RfgMenu : public TearOffMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
explicit RfgMenu(Player *player, QWidget *parent = nullptr);
void createMoveActions();
void createViewActions();
void retranslateUi();
void retranslateUi() override;
void setShortcutsActive() override
{
}
void setShortcutsInactive() override
{
}
QMenu *moveRfgMenu = nullptr;

View file

@ -8,6 +8,31 @@ SayMenu::SayMenu(Player *_player) : player(_player)
{
connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu);
initSayMenu();
retranslateUi();
}
void SayMenu::retranslateUi()
{
setTitle(tr("S&ay"));
}
void SayMenu::setShortcutsActive()
{
shortcutsActive = true;
const auto menuActions = actions();
for (int i = 0; i < menuActions.size() && i < 10; ++i) {
menuActions[i]->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10)));
}
}
void SayMenu::setShortcutsInactive()
{
shortcutsActive = false;
for (auto *action : actions()) {
action->setShortcut(QKeySequence());
}
}
void SayMenu::initSayMenu()
@ -19,10 +44,11 @@ void SayMenu::initSayMenu()
for (int i = 0; i < count; ++i) {
auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this);
if (i < 10) {
newAction->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10)));
}
connect(newAction, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actSayMessage);
addAction(newAction);
}
}
if (shortcutsActive) {
setShortcutsActive();
}
}

View file

@ -7,18 +7,27 @@
#ifndef COCKATRICE_SAY_MENU_H
#define COCKATRICE_SAY_MENU_H
#include "abstract_player_component.h"
#include <QMenu>
class Player;
class SayMenu : public QMenu
class SayMenu : public QMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
explicit SayMenu(Player *player);
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
private slots:
void initSayMenu();
private:
Player *player;
bool shortcutsActive = false;
};
#endif // COCKATRICE_SAY_MENU_H

View file

@ -7,18 +7,20 @@
#ifndef COCKATRICE_SIDEBOARD_MENU_H
#define COCKATRICE_SIDEBOARD_MENU_H
#include "abstract_player_component.h"
#include <QMenu>
class Player;
class SideboardMenu : public QMenu
class SideboardMenu : public QMenu, public AbstractPlayerComponent
{
Q_OBJECT
public:
explicit SideboardMenu(Player *player, QMenu *playerMenu);
void retranslateUi();
void setShortcutsActive();
void setShortcutsInactive();
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
private:
Player *player;

View file

@ -7,17 +7,19 @@
#ifndef COCKATRICE_UTILITY_MENU_H
#define COCKATRICE_UTILITY_MENU_H
#include "abstract_player_component.h"
#include <QMenu>
class Player;
class UtilityMenu : public QMenu
class UtilityMenu : public QMenu, public AbstractPlayerComponent
{
Q_OBJECT
public slots:
void populatePredefinedTokensMenu();
void retranslateUi();
void setShortcutsActive();
void setShortcutsInactive();
void retranslateUi() override;
void setShortcutsActive() override;
void setShortcutsInactive() override;
public:
explicit UtilityMenu(Player *player, QMenu *playerMenu);

View file

@ -61,15 +61,15 @@ void Player::forwardActionSignalsToEventHandler()
void Player::initializeZones()
{
addZone(new PileZoneLogic(this, "deck", false, true, false, this));
addZone(new PileZoneLogic(this, "grave", false, false, true, this));
addZone(new PileZoneLogic(this, "rfg", false, false, true, this));
addZone(new PileZoneLogic(this, "sb", false, false, false, this));
addZone(new TableZoneLogic(this, "table", true, false, true, this));
addZone(new StackZoneLogic(this, "stack", true, false, true, this));
addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this));
addZone(new PileZoneLogic(this, ZoneNames::GRAVE, false, false, true, this));
addZone(new PileZoneLogic(this, ZoneNames::EXILE, false, false, true, this));
addZone(new PileZoneLogic(this, ZoneNames::SIDEBOARD, false, false, false, this));
addZone(new TableZoneLogic(this, ZoneNames::TABLE, true, false, true, this));
addZone(new StackZoneLogic(this, ZoneNames::STACK, true, false, true, this));
bool visibleHand = playerInfo->getLocalOrJudge() ||
(game->getPlayerManager()->isSpectator() && game->getGameMetaInfo()->spectatorsOmniscient());
addZone(new HandZoneLogic(this, "hand", false, false, visibleHand, this));
addZone(new HandZoneLogic(this, ZoneNames::HAND, false, false, visibleHand, this));
}
Player::~Player()
@ -119,13 +119,13 @@ void Player::setZoneId(int _zoneId)
void Player::processPlayerInfo(const ServerInfo_Player &info)
{
static QSet<QString> builtinZones{/* PileZones */
"deck", "grave", "rfg", "sb",
ZoneNames::DECK, ZoneNames::GRAVE, ZoneNames::EXILE, ZoneNames::SIDEBOARD,
/* TableZone */
"table",
ZoneNames::TABLE,
/* StackZone */
"stack",
ZoneNames::STACK,
/* HandZone */
"hand"};
ZoneNames::HAND};
clearCounters();
clearArrows();

View file

@ -27,6 +27,7 @@
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/protocol/pb/card_attributes.pb.h>
#include <libcockatrice/protocol/pb/game_event.pb.h>
#include <libcockatrice/utility/zone_names.h>
inline Q_LOGGING_CATEGORY(PlayerLog, "player");
@ -155,37 +156,37 @@ public:
PileZoneLogic *getDeckZone()
{
return qobject_cast<PileZoneLogic *>(zones.value("deck"));
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::DECK));
}
PileZoneLogic *getGraveZone()
{
return qobject_cast<PileZoneLogic *>(zones.value("grave"));
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::GRAVE));
}
PileZoneLogic *getRfgZone()
{
return qobject_cast<PileZoneLogic *>(zones.value("rfg"));
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::EXILE));
}
PileZoneLogic *getSideboardZone()
{
return qobject_cast<PileZoneLogic *>(zones.value("sb"));
return qobject_cast<PileZoneLogic *>(zones.value(ZoneNames::SIDEBOARD));
}
TableZoneLogic *getTableZone()
{
return qobject_cast<TableZoneLogic *>(zones.value("table"));
return qobject_cast<TableZoneLogic *>(zones.value(ZoneNames::TABLE));
}
StackZoneLogic *getStackZone()
{
return qobject_cast<StackZoneLogic *>(zones.value("stack"));
return qobject_cast<StackZoneLogic *>(zones.value(ZoneNames::STACK));
}
HandZoneLogic *getHandZone()
{
return qobject_cast<HandZoneLogic *>(zones.value("hand"));
return qobject_cast<HandZoneLogic *>(zones.value(ZoneNames::HAND));
}
AbstractCounter *addCounter(const ServerInfo_Counter &counter);

View file

@ -3,6 +3,7 @@
#include "../../interface/widgets/tabs/tab_game.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "../board/card_item.h"
#include "../client/settings/card_counter_settings.h"
#include "../dialogs/dlg_move_top_cards_until.h"
#include "../dialogs/dlg_roll_dice.h"
#include "../zones/hand_zone.h"
@ -27,12 +28,15 @@
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/utility/expression.h>
#include <libcockatrice/utility/trice_limits.h>
#include <libcockatrice/utility/zone_names.h>
// milliseconds in between triggers of the move top cards until action
static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100;
PlayerActions::PlayerActions(Player *_player) : player(_player), lastTokenTableRow(0), movingCardsUntil(false)
PlayerActions::PlayerActions(Player *_player)
: QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false)
{
moveTopCardTimer = new QTimer(this);
moveTopCardTimer->setInterval(MOVE_TOP_CARD_UNTIL_INTERVAL);
@ -63,25 +67,25 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
int tableRow = info.getUiAttributes().tableRow;
bool playToStack = SettingsCache::instance().getPlayToStack();
QString currentZone = card->getZone()->getName();
if (currentZone == "stack" && tableRow == 3) {
cmd.set_target_zone("grave");
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
cmd.set_target_zone(ZoneNames::GRAVE);
cmd.set_x(0);
cmd.set_y(0);
} else if (!faceDown &&
((!playToStack && tableRow == 3) || ((playToStack && tableRow != 0) && currentZone != "stack"))) {
cmd.set_target_zone("stack");
} else if (!faceDown && ((!playToStack && tableRow == 3) ||
((playToStack && tableRow != 0) && currentZone != ZoneNames::STACK))) {
cmd.set_target_zone(ZoneNames::STACK);
cmd.set_x(-1);
cmd.set_y(0);
} else {
tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
}
cardToMove->set_tapped(!faceDown && info.getUiAttributes().cipt);
if (tableRow != 3)
cmd.set_target_zone("table");
cmd.set_target_zone(ZoneNames::TABLE);
cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y());
}
@ -113,18 +117,13 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown)
const CardInfo &info = exactCard.getInfo();
int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow;
// default instant/sorcery cards to the noncreatures row
if (tableRow > 2) {
tableRow = 1;
}
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow));
cardToMove->set_face_down(faceDown);
if (!faceDown) {
cardToMove->set_pt(info.getPowTough().toStdString());
}
cardToMove->set_tapped(!faceDown && info.getUiAttributes().cipt);
cmd.set_target_zone("table");
cmd.set_target_zone(ZoneNames::TABLE);
cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y());
sendGameCommand(cmd);
@ -132,12 +131,12 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown)
void PlayerActions::actViewLibrary()
{
player->getGameScene()->toggleZoneView(player, "deck", -1);
player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, -1);
}
void PlayerActions::actViewHand()
{
player->getGameScene()->toggleZoneView(player, "hand", -1);
player->getGameScene()->toggleZoneView(player, ZoneNames::HAND, -1);
}
/**
@ -181,7 +180,7 @@ void PlayerActions::actViewTopCards()
deckSize, 1, &ok);
if (ok) {
defaultNumberTopCards = number;
player->getGameScene()->toggleZoneView(player, "deck", number);
player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number);
}
}
@ -194,14 +193,14 @@ void PlayerActions::actViewBottomCards()
deckSize, 1, &ok);
if (ok) {
defaultNumberBottomCards = number;
player->getGameScene()->toggleZoneView(player, "deck", number, true);
player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number, true);
}
}
void PlayerActions::actAlwaysRevealTopCard()
{
Command_ChangeZoneProperties cmd;
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_always_reveal_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysRevealTopCardChecked());
sendGameCommand(cmd);
@ -210,7 +209,7 @@ void PlayerActions::actAlwaysRevealTopCard()
void PlayerActions::actAlwaysLookAtTopCard()
{
Command_ChangeZoneProperties cmd;
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_always_look_at_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysLookAtTopCardChecked());
sendGameCommand(cmd);
@ -223,17 +222,17 @@ void PlayerActions::actOpenDeckInDeckEditor()
void PlayerActions::actViewGraveyard()
{
player->getGameScene()->toggleZoneView(player, "grave", -1);
player->getGameScene()->toggleZoneView(player, ZoneNames::GRAVE, -1);
}
void PlayerActions::actViewRfg()
{
player->getGameScene()->toggleZoneView(player, "rfg", -1);
player->getGameScene()->toggleZoneView(player, ZoneNames::EXILE, -1);
}
void PlayerActions::actViewSideboard()
{
player->getGameScene()->toggleZoneView(player, "sb", -1);
player->getGameScene()->toggleZoneView(player, ZoneNames::SIDEBOARD, -1);
}
void PlayerActions::actShuffle()
@ -263,7 +262,7 @@ void PlayerActions::actShuffleTop()
defaultNumberTopCards = number;
Command_Shuffle cmd;
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_start(0);
cmd.set_end(number - 1); // inclusive, the indexed card at end will be shuffled
@ -292,7 +291,7 @@ void PlayerActions::actShuffleBottom()
defaultNumberBottomCards = number;
Command_Shuffle cmd;
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_start(-number);
cmd.set_end(-1);
@ -310,28 +309,48 @@ void PlayerActions::actMulligan()
{
int startSize = SettingsCache::instance().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck
int deckSize = player->getDeckZone()->getCards().size() + handSize;
bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"),
tr("Number of cards: (max. %1)").arg(deckSize) + '\n' +
tr("0 and lower are in comparison to current hand size"),
startSize, -handSize, deckSize, 1, &ok);
if (!ok) {
return;
}
Command_Mulligan cmd;
if (number < 1) {
if (handSize == 0) {
return;
}
cmd.set_number(handSize + number);
} else {
cmd.set_number(number);
number = handSize + number;
}
doMulligan(number);
SettingsCache::instance().setStartingHandSize(number);
}
void PlayerActions::actMulliganSameSize()
{
int handSize = player->getHandZone()->getCards().size();
doMulligan(handSize);
}
void PlayerActions::actMulliganMinusOne()
{
int handSize = player->getHandZone()->getCards().size();
int targetSize = qMax(1, handSize - 1);
doMulligan(targetSize);
}
void PlayerActions::doMulligan(int number)
{
if (number < 1) {
return;
}
Command_Mulligan cmd;
cmd.set_number(number);
sendGameCommand(cmd);
if (startSize != number) {
SettingsCache::instance().setStartingHandSize(number);
}
}
void PlayerActions::actDrawCards()
@ -356,7 +375,7 @@ void PlayerActions::actUndoDraw()
void PlayerActions::cmdSetTopCard(Command_MoveCard &cmd)
{
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(0);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
@ -366,7 +385,7 @@ void PlayerActions::cmdSetBottomCard(Command_MoveCard &cmd)
{
CardZoneLogic *zone = player->getDeckZone();
int lastCard = zone->getCards().size() - 1;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(lastCard);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
@ -380,7 +399,7 @@ void PlayerActions::actMoveTopCardToGrave()
Command_MoveCard cmd;
cmdSetTopCard(cmd);
cmd.set_target_zone("grave");
cmd.set_target_zone(ZoneNames::GRAVE);
cmd.set_x(0);
cmd.set_y(0);
@ -395,7 +414,7 @@ void PlayerActions::actMoveTopCardToExile()
Command_MoveCard cmd;
cmdSetTopCard(cmd);
cmd.set_target_zone("rfg");
cmd.set_target_zone(ZoneNames::EXILE);
cmd.set_x(0);
cmd.set_y(0);
@ -404,37 +423,25 @@ void PlayerActions::actMoveTopCardToExile()
void PlayerActions::actMoveTopCardsToGrave()
{
const int maxCards = player->getDeckZone()->getCards().size();
if (maxCards == 0) {
return;
}
moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), false);
}
bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to grave"),
tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1,
maxCards, 1, &ok);
if (!ok) {
return;
} else if (number > maxCards) {
number = maxCards;
}
defaultNumberTopCards = number;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("grave");
cmd.set_x(0);
cmd.set_y(0);
for (int i = number - 1; i >= 0; --i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(i);
}
sendGameCommand(cmd);
void PlayerActions::actMoveTopCardsToGraveFaceDown()
{
moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), true);
}
void PlayerActions::actMoveTopCardsToExile()
{
moveTopCardsTo(ZoneNames::EXILE, tr("exile"), false);
}
void PlayerActions::actMoveTopCardsToExileFaceDown()
{
moveTopCardsTo(ZoneNames::EXILE, tr("exile"), true);
}
void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown)
{
const int maxCards = player->getDeckZone()->getCards().size();
if (maxCards == 0) {
@ -442,25 +449,31 @@ void PlayerActions::actMoveTopCardsToExile()
}
bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to exile"),
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to %1").arg(zoneDisplayName),
tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1,
maxCards, 1, &ok);
if (!ok) {
return;
} else if (number > maxCards) {
}
if (number > maxCards) {
number = maxCards;
}
defaultNumberTopCards = number;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("rfg");
cmd.set_target_zone(targetZone.toStdString());
cmd.set_x(0);
cmd.set_y(0);
for (int i = number - 1; i >= 0; --i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(i);
auto card = cmd.mutable_cards_to_move()->add_card();
card->set_card_id(i);
if (faceDown) {
card->set_face_down(true);
}
}
sendGameCommand(cmd);
@ -470,22 +483,19 @@ void PlayerActions::actMoveTopCardsUntil()
{
stopMoveTopCardsUntil();
DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilExprs, movingCardsUntilNumberOfHits,
movingCardsUntilAutoPlay);
DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilOptions);
if (!dlg.exec()) {
return;
}
auto expr = dlg.getExpr();
movingCardsUntilExprs = dlg.getExprs();
movingCardsUntilNumberOfHits = dlg.getNumberOfHits();
movingCardsUntilAutoPlay = dlg.isAutoPlay();
movingCardsUntilOptions = dlg.getOptions();
if (player->getDeckZone()->getCards().empty()) {
stopMoveTopCardsUntil();
} else {
movingCardsUntilFilter = FilterString(expr);
movingCardsUntilCounter = movingCardsUntilNumberOfHits;
movingCardsUntilCounter = movingCardsUntilOptions.numberOfHits;
movingCardsUntil = true;
actMoveTopCardToPlay();
}
@ -497,7 +507,7 @@ void PlayerActions::moveOneCardUntil(CardItem *card)
const bool isMatch = card && movingCardsUntilFilter.check(card->getCard().getCardPtr());
if (isMatch && movingCardsUntilAutoPlay) {
if (isMatch && movingCardsUntilOptions.autoPlay) {
// Directly calling playCard will deadlock, since we are already in the middle of processing an event.
// Use QTimer::singleShot to queue up the playCard on the event loop.
QTimer::singleShot(0, this, [card, this] { playCard(card, false); });
@ -535,7 +545,7 @@ void PlayerActions::actMoveTopCardToBottom()
Command_MoveCard cmd;
cmdSetTopCard(cmd);
cmd.set_target_zone("deck");
cmd.set_target_zone(ZoneNames::DECK);
cmd.set_x(-1); // bottom of deck
cmd.set_y(0);
@ -550,7 +560,7 @@ void PlayerActions::actMoveTopCardToPlay()
Command_MoveCard cmd;
cmdSetTopCard(cmd);
cmd.set_target_zone("stack");
cmd.set_target_zone(ZoneNames::STACK);
cmd.set_x(-1);
cmd.set_y(0);
@ -564,12 +574,12 @@ void PlayerActions::actMoveTopCardToPlayFaceDown()
}
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
CardToMove *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(0);
cardToMove->set_face_down(true);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("table");
cmd.set_target_zone(ZoneNames::TABLE);
cmd.set_x(-1);
cmd.set_y(0);
@ -584,7 +594,7 @@ void PlayerActions::actMoveBottomCardToGrave()
Command_MoveCard cmd;
cmdSetBottomCard(cmd);
cmd.set_target_zone("grave");
cmd.set_target_zone(ZoneNames::GRAVE);
cmd.set_x(0);
cmd.set_y(0);
@ -599,7 +609,7 @@ void PlayerActions::actMoveBottomCardToExile()
Command_MoveCard cmd;
cmdSetBottomCard(cmd);
cmd.set_target_zone("rfg");
cmd.set_target_zone(ZoneNames::EXILE);
cmd.set_x(0);
cmd.set_y(0);
@ -608,37 +618,25 @@ void PlayerActions::actMoveBottomCardToExile()
void PlayerActions::actMoveBottomCardsToGrave()
{
const int maxCards = player->getDeckZone()->getCards().size();
if (maxCards == 0) {
return;
}
moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), false);
}
bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to grave"),
tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1,
maxCards, 1, &ok);
if (!ok) {
return;
} else if (number > maxCards) {
number = maxCards;
}
defaultNumberBottomCards = number;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("grave");
cmd.set_x(0);
cmd.set_y(0);
for (int i = maxCards - number; i < maxCards; ++i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(i);
}
sendGameCommand(cmd);
void PlayerActions::actMoveBottomCardsToGraveFaceDown()
{
moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), true);
}
void PlayerActions::actMoveBottomCardsToExile()
{
moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), false);
}
void PlayerActions::actMoveBottomCardsToExileFaceDown()
{
moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), true);
}
void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown)
{
const int maxCards = player->getDeckZone()->getCards().size();
if (maxCards == 0) {
@ -646,25 +644,31 @@ void PlayerActions::actMoveBottomCardsToExile()
}
bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to exile"),
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to %1").arg(zoneDisplayName),
tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1,
maxCards, 1, &ok);
if (!ok) {
return;
} else if (number > maxCards) {
}
if (number > maxCards) {
number = maxCards;
}
defaultNumberBottomCards = number;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("rfg");
cmd.set_target_zone(targetZone.toStdString());
cmd.set_x(0);
cmd.set_y(0);
for (int i = maxCards - number; i < maxCards; ++i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(i);
auto card = cmd.mutable_cards_to_move()->add_card();
card->set_card_id(i);
if (faceDown) {
card->set_face_down(true);
}
}
sendGameCommand(cmd);
@ -678,7 +682,7 @@ void PlayerActions::actMoveBottomCardToTop()
Command_MoveCard cmd;
cmdSetBottomCard(cmd);
cmd.set_target_zone("deck");
cmd.set_target_zone(ZoneNames::DECK);
cmd.set_x(0); // top of deck
cmd.set_y(0);
@ -748,7 +752,7 @@ void PlayerActions::actDrawBottomCard()
Command_MoveCard cmd;
cmdSetBottomCard(cmd);
cmd.set_target_zone("hand");
cmd.set_target_zone(ZoneNames::HAND);
cmd.set_x(0);
cmd.set_y(0);
@ -774,9 +778,9 @@ void PlayerActions::actDrawBottomCards()
defaultNumberBottomCards = number;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("hand");
cmd.set_target_zone(ZoneNames::HAND);
cmd.set_x(0);
cmd.set_y(0);
@ -795,7 +799,7 @@ void PlayerActions::actMoveBottomCardToPlay()
Command_MoveCard cmd;
cmdSetBottomCard(cmd);
cmd.set_target_zone("stack");
cmd.set_target_zone(ZoneNames::STACK);
cmd.set_x(-1);
cmd.set_y(0);
@ -812,13 +816,13 @@ void PlayerActions::actMoveBottomCardToPlayFaceDown()
int lastCard = zone->getCards().size() - 1;
Command_MoveCard cmd;
cmd.set_start_zone("deck");
cmd.set_start_zone(ZoneNames::DECK);
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(lastCard);
cardToMove->set_face_down(true);
cmd.set_target_player_id(player->getPlayerInfo()->getId());
cmd.set_target_zone("table");
cmd.set_target_zone(ZoneNames::TABLE);
cmd.set_x(-1);
cmd.set_y(0);
@ -828,7 +832,7 @@ void PlayerActions::actMoveBottomCardToPlayFaceDown()
void PlayerActions::actUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone("table");
cmd.set_zone(ZoneNames::TABLE);
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");
@ -860,7 +864,7 @@ void PlayerActions::actCreateToken()
ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId});
if (correctedCard) {
lastTokenInfo.name = correctedCard.getName();
lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard.getInfo().getUiAttributes().tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow);
if (lastTokenInfo.pt.isEmpty()) {
lastTokenInfo.pt = correctedCard.getInfo().getPowTough();
}
@ -878,7 +882,7 @@ void PlayerActions::actCreateAnotherToken()
}
Command_CreateToken cmd;
cmd.set_zone("table");
cmd.set_zone(ZoneNames::TABLE);
cmd.set_card_name(lastTokenInfo.name.toStdString());
cmd.set_card_provider_id(lastTokenInfo.providerId.toStdString());
cmd.set_color(lastTokenInfo.color.toStdString());
@ -911,7 +915,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo)
.providerId =
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
lastTokenTableRow = TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow);
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name));
}
@ -1059,7 +1063,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const
// move card onto table first if attaching from some other zone
// we only do this for AttachTo because cross-zone TransformInto is already handled server-side
if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != "table") {
if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) {
playCardToTable(sourceCard, false);
}
@ -1079,13 +1083,11 @@ void PlayerActions::createCard(const CardItem *sourceCard,
return;
}
// get the target token's location
// TODO: Define this QPoint into its own function along with the one below
QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow));
QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow));
// create the token for the related card
Command_CreateToken cmd;
cmd.set_zone("table");
cmd.set_zone(ZoneNames::TABLE);
cmd.set_card_name(cardInfo->getName().toStdString());
switch (cardInfo->getColors().size()) {
case 0:
@ -1114,12 +1116,12 @@ void PlayerActions::createCard(const CardItem *sourceCard,
switch (attachType) {
case CardRelationType::DoesNotAttach:
cmd.set_target_zone("table");
cmd.set_target_zone(ZoneNames::TABLE);
cmd.set_card_provider_id(relatedCard.getPrinting().getUuid().toStdString());
break;
case CardRelationType::AttachTo:
cmd.set_target_zone("table"); // We currently only support creating tokens on the table
cmd.set_target_zone(ZoneNames::TABLE); // We currently only support creating tokens on the table
cmd.set_card_provider_id(relatedCard.getPrinting().getUuid().toStdString());
cmd.set_target_card_id(sourceCard->getId());
cmd.set_target_mode(Command_CreateToken::ATTACH_TO);
@ -1127,7 +1129,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
case CardRelationType::TransformInto:
// allow cards to directly transform on stack
cmd.set_zone(sourceCard->getZone()->getName() == "stack" ? "stack" : "table");
cmd.set_zone(sourceCard->getZone()->getName() == ZoneNames::STACK ? ZoneNames::STACK : ZoneNames::TABLE);
// Transform card zone changes are handled server-side
cmd.set_target_zone(sourceCard->getZone()->getName().toStdString());
cmd.set_target_card_id(sourceCard->getId());
@ -1242,7 +1244,7 @@ void PlayerActions::actMoveCardXCardsFromTop()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("deck");
cmd->set_target_zone(ZoneNames::DECK);
cmd->set_x(number);
cmd->set_y(0);
commandList.append(cmd);
@ -1573,23 +1575,34 @@ void PlayerActions::actCardCounterTrigger()
break;
}
case 11: { // set counter with dialog
bool ok;
player->setDialogSemaphore(true);
int oldValue = 0;
if (player->getGameScene()->selectedItems().size() == 1) {
auto *card = static_cast<CardItem *>(player->getGameScene()->selectedItems().first());
oldValue = card->getCounters().value(counterId, 0);
// If a single card is selected, we show the old value in the dialog. Otherwise, we show "x"
QList<QGraphicsItem *> sel = player->getGameScene()->selectedItems();
QString oldValueForDlg = "x";
if (sel.size() == 1) {
auto *card = dynamic_cast<CardItem *>(sel.first());
oldValueForDlg = QString::number(card->getCounters().value(counterId, 0));
}
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Set counters"), tr("Number:"), oldValue,
0, MAX_COUNTERS_ON_CARD, 1, &ok);
auto &cardCounterSettings = SettingsCache::instance().cardCounters();
QString counterName = cardCounterSettings.displayName(counterId);
AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab());
int ok = dialog.exec();
player->setDialogSemaphore(false);
if (player->clearCardsToDelete() || !ok) {
return;
}
for (const auto &item : player->getGameScene()->selectedItems()) {
auto *card = static_cast<CardItem *>(item);
for (const auto &item : sel) {
auto *card = dynamic_cast<CardItem *>(item);
int oldValue = card->getCounters().value(counterId, 0);
Expression exp(oldValue);
int number = static_cast<int>(exp.parse(dialog.textValue()));
auto *cmd = new Command_SetCardCounter;
cmd->set_zone(card->getZone()->getName().toStdString());
cmd->set_card_id(card->getId());
@ -1631,7 +1644,7 @@ void PlayerActions::playSelectedCards(const bool faceDown)
[](const auto &card1, const auto &card2) { return card1->getId() > card2->getId(); });
for (auto &card : selectedCards) {
if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != "table") {
if (card && !isUnwritableRevealZone(card->getZone()) && card->getZone()->getName() != ZoneNames::TABLE) {
playCard(card, faceDown);
}
}
@ -1684,7 +1697,7 @@ void PlayerActions::actRevealHand(int revealToPlayerId)
if (revealToPlayerId != -1) {
cmd.set_player_id(revealToPlayerId);
}
cmd.set_zone_name("hand");
cmd.set_zone_name(ZoneNames::HAND);
sendGameCommand(cmd);
}
@ -1695,7 +1708,7 @@ void PlayerActions::actRevealRandomHandCard(int revealToPlayerId)
if (revealToPlayerId != -1) {
cmd.set_player_id(revealToPlayerId);
}
cmd.set_zone_name("hand");
cmd.set_zone_name(ZoneNames::HAND);
cmd.add_card_id(RANDOM_CARD_FROM_ZONE);
sendGameCommand(cmd);
@ -1707,7 +1720,7 @@ void PlayerActions::actRevealLibrary(int revealToPlayerId)
if (revealToPlayerId != -1) {
cmd.set_player_id(revealToPlayerId);
}
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
sendGameCommand(cmd);
}
@ -1718,7 +1731,7 @@ void PlayerActions::actLendLibrary(int lendToPlayerId)
if (lendToPlayerId != -1) {
cmd.set_player_id(lendToPlayerId);
}
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_grant_write_access(true);
sendGameCommand(cmd);
@ -1731,7 +1744,7 @@ void PlayerActions::actRevealTopCards(int revealToPlayerId, int amount)
cmd.set_player_id(revealToPlayerId);
}
cmd.set_zone_name("deck");
cmd.set_zone_name(ZoneNames::DECK);
cmd.set_top_cards(amount);
// backward compatibility: servers before #1051 only permits to reveal the first card
cmd.add_card_id(0);
@ -1745,7 +1758,7 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId)
if (revealToPlayerId != -1) {
cmd.set_player_id(revealToPlayerId);
}
cmd.set_zone_name("grave");
cmd.set_zone_name(ZoneNames::GRAVE);
cmd.add_card_id(RANDOM_CARD_FROM_ZONE);
sendGameCommand(cmd);
}
@ -1808,7 +1821,7 @@ void PlayerActions::cardMenuAction()
}
case cmClone: {
auto *cmd = new Command_CreateToken;
cmd->set_zone("table");
cmd->set_zone(ZoneNames::TABLE);
cmd->set_card_name(card->getName().toStdString());
cmd->set_card_provider_id(card->getProviderId().toStdString());
cmd->set_color(card->getColor().toStdString());
@ -1850,13 +1863,13 @@ void PlayerActions::cardMenuAction()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("deck");
cmd->set_target_zone(ZoneNames::DECK);
cmd->set_x(0);
cmd->set_y(0);
if (idList.card_size() > 1) {
auto *scmd = new Command_Shuffle;
scmd->set_zone_name("deck");
scmd->set_zone_name(ZoneNames::DECK);
scmd->set_start(0);
scmd->set_end(idList.card_size() - 1); // inclusive, the indexed card at end will be shuffled
// Server process events backwards, so...
@ -1872,13 +1885,13 @@ void PlayerActions::cardMenuAction()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("deck");
cmd->set_target_zone(ZoneNames::DECK);
cmd->set_x(-1);
cmd->set_y(0);
if (idList.card_size() > 1) {
auto *scmd = new Command_Shuffle;
scmd->set_zone_name("deck");
scmd->set_zone_name(ZoneNames::DECK);
scmd->set_start(-idList.card_size());
scmd->set_end(-1);
// Server process events backwards, so...
@ -1894,7 +1907,7 @@ void PlayerActions::cardMenuAction()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("hand");
cmd->set_target_zone(ZoneNames::HAND);
cmd->set_x(0);
cmd->set_y(0);
commandList.append(cmd);
@ -1906,7 +1919,7 @@ void PlayerActions::cardMenuAction()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("grave");
cmd->set_target_zone(ZoneNames::GRAVE);
cmd->set_x(0);
cmd->set_y(0);
commandList.append(cmd);
@ -1918,12 +1931,40 @@ void PlayerActions::cardMenuAction()
cmd->set_start_zone(startZone.toStdString());
cmd->mutable_cards_to_move()->CopyFrom(idList);
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone("rfg");
cmd->set_target_zone(ZoneNames::EXILE);
cmd->set_x(0);
cmd->set_y(0);
commandList.append(cmd);
break;
}
case cmMoveToTable: {
// Each card needs its own command because table row, pt, and cipt vary per card
for (const auto &card : cardList) {
auto *cmd = new Command_MoveCard;
cmd->set_start_player_id(startPlayerId);
cmd->set_start_zone(startZone.toStdString());
cmd->set_target_player_id(player->getPlayerInfo()->getId());
cmd->set_target_zone(ZoneNames::TABLE);
cmd->set_x(-1);
CardToMove *ctm = cmd->mutable_cards_to_move()->add_card();
ctm->set_card_id(card->getId());
ctm->set_face_down(false);
int tableRow = 0;
ExactCard exactCard = card->getCard();
if (exactCard) {
const CardInfo &info = exactCard.getInfo();
tableRow = info.getUiAttributes().tableRow;
ctm->set_pt(info.getPowTough().toStdString());
ctm->set_tapped(info.getUiAttributes().cipt);
}
cmd->set_y(TableZone::tableRowToGridY(tableRow));
commandList.append(cmd);
}
break;
}
default:
break;
}

View file

@ -8,6 +8,7 @@
#ifndef COCKATRICE_PLAYER_ACTIONS_H
#define COCKATRICE_PLAYER_ACTIONS_H
#include "../dialogs/dlg_create_token.h"
#include "../dialogs/dlg_move_top_cards_until.h"
#include "event_processing_options.h"
#include "player.h"
@ -85,6 +86,9 @@ public slots:
void actDrawCards();
void actUndoDraw();
void actMulligan();
void actMulliganSameSize();
void actMulliganMinusOne();
void doMulligan(int number);
void actPlay();
void actPlayFacedown();
@ -95,7 +99,9 @@ public slots:
void actMoveTopCardToGrave();
void actMoveTopCardToExile();
void actMoveTopCardsToGrave();
void actMoveTopCardsToGraveFaceDown();
void actMoveTopCardsToExile();
void actMoveTopCardsToExileFaceDown();
void actMoveTopCardsUntil();
void actMoveTopCardToBottom();
void actDrawBottomCard();
@ -105,7 +111,9 @@ public slots:
void actMoveBottomCardToGrave();
void actMoveBottomCardToExile();
void actMoveBottomCardsToGrave();
void actMoveBottomCardsToGraveFaceDown();
void actMoveBottomCardsToExile();
void actMoveBottomCardsToExileFaceDown();
void actMoveBottomCardToTop();
void actSelectAll();
@ -171,11 +179,12 @@ private:
bool movingCardsUntil;
QTimer *moveTopCardTimer;
QStringList movingCardsUntilExprs = {};
int movingCardsUntilNumberOfHits = 1;
bool movingCardsUntilAutoPlay = false;
FilterString movingCardsUntilFilter;
int movingCardsUntilCounter = 0;
MoveTopCardsUntilOptions movingCardsUntilOptions;
void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown);
void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown);
void createCard(const CardItem *sourceCard,
const QString &dbCardName,

View file

@ -30,8 +30,9 @@
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
#include <libcockatrice/protocol/pb/event_set_counter.pb.h>
#include <libcockatrice/protocol/pb/event_shuffle.pb.h>
#include <libcockatrice/utility/zone_names.h>
PlayerEventHandler::PlayerEventHandler(Player *_player) : player(_player)
PlayerEventHandler::PlayerEventHandler(Player *_player) : QObject(_player), player(_player)
{
}
@ -59,7 +60,6 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event)
// we want to close empty views as well
if (length == 0 || length > absStart) { // note this assumes views always start at the top of the library
view->close();
break;
}
} else {
qWarning() << zone->getName() << "of" << player->getPlayerInfo()->getName() << "holds empty zoneview!";
@ -321,8 +321,8 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv
}
player->getPlayerMenu()->updateCardMenu(card);
if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == "deck" &&
targetZone->getName() == "stack") {
if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == ZoneNames::DECK &&
targetZone->getName() == ZoneNames::STACK) {
player->getPlayerActions()->moveOneCardUntil(card);
}
}
@ -594,4 +594,4 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
qWarning() << "unhandled game event" << type;
}
}
}
}

View file

@ -19,7 +19,8 @@ PlayerGraphicsItem::PlayerGraphicsItem(Player *_player) : player(_player)
playerArea = new PlayerArea(this);
playerTarget = new PlayerTarget(player, playerArea);
qreal avatarMargin = (counterAreaWidth + CARD_HEIGHT + 15 - playerTarget->boundingRect().width()) / 2.0;
qreal avatarMargin =
(counterAreaWidth + CardDimensions::HEIGHT_F + 15 - playerTarget->boundingRect().width()) / 2.0;
playerTarget->setPos(QPointF(avatarMargin, avatarMargin));
initializeZones();
@ -55,8 +56,9 @@ void PlayerGraphicsItem::onPlayerActiveChanged(bool _active)
void PlayerGraphicsItem::initializeZones()
{
deckZoneGraphicsItem = new PileZone(player->getDeckZone(), this);
auto base = QPointF(counterAreaWidth + (CARD_HEIGHT - CARD_WIDTH + 15) / 2.0,
10 + playerTarget->boundingRect().height() + 5 - (CARD_HEIGHT - CARD_WIDTH) / 2.0);
auto base = QPointF(counterAreaWidth + (CardDimensions::HEIGHT_F - CardDimensions::WIDTH_F + 15) / 2.0,
10 + playerTarget->boundingRect().height() + 5 -
(CardDimensions::HEIGHT_F - CardDimensions::WIDTH_F) / 2.0);
deckZoneGraphicsItem->setPos(base);
qreal h = deckZoneGraphicsItem->boundingRect().width() + 5;
@ -95,7 +97,7 @@ QRectF PlayerGraphicsItem::boundingRect() const
qreal PlayerGraphicsItem::getMinimumWidth() const
{
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CARD_HEIGHT + 15 + counterAreaWidth +
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().getHorizontalHand()) {
result += handZoneGraphicsItem->boundingRect().width();
@ -112,8 +114,8 @@ void PlayerGraphicsItem::paint(QPainter * /*painter*/,
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
{
// Extend table (and hand, if horizontal) to accommodate the new player width.
qreal tableWidth =
newPlayerWidth - CARD_HEIGHT - 15 - counterAreaWidth - stackZoneGraphicsItem->boundingRect().width();
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().getHorizontalHand()) {
tableWidth -= handZoneGraphicsItem->boundingRect().width();
}
@ -152,7 +154,7 @@ void PlayerGraphicsItem::rearrangeCounters()
void PlayerGraphicsItem::rearrangeZones()
{
auto base = QPointF(CARD_HEIGHT + counterAreaWidth + 15, 0);
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
if (SettingsCache::instance().getHorizontalHand()) {
if (mirrored) {
if (player->getHandZone()->contentsKnown()) {
@ -203,7 +205,7 @@ void PlayerGraphicsItem::rearrangeZones()
void PlayerGraphicsItem::updateBoundingRect()
{
prepareGeometryChange();
qreal width = CARD_HEIGHT + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width();
if (SettingsCache::instance().getHorizontalHand()) {
qreal handHeight =
player->getPlayerInfo()->getHandVisible() ? handZoneGraphicsItem->boundingRect().height() : 0;
@ -214,7 +216,7 @@ void PlayerGraphicsItem::updateBoundingRect()
0, 0, width + handZoneGraphicsItem->boundingRect().width() + tableZoneGraphicsItem->boundingRect().width(),
tableZoneGraphicsItem->boundingRect().height());
}
playerArea->setSize(CARD_HEIGHT + counterAreaWidth + 15, bRect.height());
playerArea->setSize(CardDimensions::HEIGHT_F + counterAreaWidth + 15, bRect.height());
emit sizeChanged();
}
}

View file

@ -0,0 +1,136 @@
/**
* @file z_value_layer_manager.h
* @ingroup GameGraphics
* @brief Semantic Z-value layer management for game scene rendering.
*
* This file provides a structured approach to Z-value allocation in the game scene.
* Z-values in Qt determine stacking order - higher values render on top of lower values.
*
* ## Layer Architecture
*
* The game scene is organized into three conceptual layers:
*
* 1. **Zone Layer (0-999)**: Zone backgrounds, containers, and static elements
* - Zone backgrounds (0.5-1.0)
* - Cards within zones (1.0 base + index)
*
* 2. **Card Layer (1-40,000,000)**: Dynamic card rendering on the table zone
* - Cards use formula: (actualY + CardDimensions::HEIGHT) * 100000 + (actualX + 1) * 100
* - Maximum card Z-value: ~40,000,000 (with 3 rows, actualY <= ~289)
*
* 3. **Overlay Layer (2,000,000,000+)**: UI elements that must appear above all cards
* - Hovered cards (+1)
* - Arrows (+3)
* - Zone views (+4)
* - Drag items (+5, +6)
* - Top UI elements (+7)
*
* ## Design Rationale
*
* The large gap between card Z-values (max ~40M) and overlay base (2B) provides
* safety margin for future table zone expansions while ensuring overlays always
* render above cards regardless of table position.
*
* ## Usage
*
* Prefer using the semantic constants from ZValues namespace:
* @code
* card->setZValue(ZValues::HOVERED_CARD);
* arrow->setZValue(ZValues::ARROWS);
* @endcode
*
* Use validation functions to verify card Z-values during development:
* @code
* Q_ASSERT(ZValueLayerManager::isValidCardZValue(cardZ));
* @endcode
*/
#ifndef Z_VALUE_LAYER_MANAGER_H
#define Z_VALUE_LAYER_MANAGER_H
#include <QtGlobal>
/**
* @namespace ZValueLayerManager
* @brief Utilities for Z-value validation and layer management.
*/
namespace ZValueLayerManager
{
/**
* @enum Layer
* @brief Semantic layer identifiers for Z-value allocation.
*
* These represent conceptual rendering layers, not actual Z-values.
* Use the corresponding ZValues constants for actual rendering.
*/
enum class Layer
{
/// Zone-level elements like backgrounds and containers
Zone,
/// Cards rendered in zones (uses sequential Z-values)
Card,
/// Temporary UI elements like hovered cards and drag items
Overlay
};
/**
* @brief Maximum Z-value a card can have on the table zone.
*
* Based on table zone formula: (actualY + CardDimensions::HEIGHT) * 100000 + (actualX + 1) * 100
* With maximum 3 rows and CardDimensions::HEIGHT = 102, actualY <= ~289.
* Maximum: (289 + 102) * 100000 + 100 * 100 = 39,110,000
*
* We use 40,000,000 as a safe upper bound with margin.
*/
constexpr qreal CARD_Z_VALUE_MAX = 40000000.0;
/**
* @brief Base Z-value for overlay elements.
*
* Must exceed CARD_Z_VALUE_MAX to ensure overlays render above all cards.
* The 50x margin (2B vs 40M) provides safety for future expansion.
*/
constexpr qreal OVERLAY_BASE = 2000000000.0;
/**
* @brief Validates that a Z-value is within the valid card range.
*
* Cards should have Z-values between CARD_BASE (1.0) and CARD_Z_VALUE_MAX.
* Values outside this range may interfere with overlay rendering.
*
* @param zValue The Z-value to validate
* @return true if the Z-value is valid for a card
*/
[[nodiscard]] constexpr bool isValidCardZValue(qreal zValue)
{
return zValue >= 1.0 && zValue <= CARD_Z_VALUE_MAX;
}
/**
* @brief Validates that a Z-value is in the overlay layer.
*
* Overlay elements should have Z-values at or above OVERLAY_BASE.
*
* @param zValue The Z-value to validate
* @return true if the Z-value is valid for an overlay element
*/
[[nodiscard]] constexpr bool isOverlayZValue(qreal zValue)
{
return zValue >= OVERLAY_BASE;
}
/**
* @brief Returns the Z-value for a specific overlay element.
*
* @param offset Offset from OVERLAY_BASE (0-7 for current elements)
* @return The absolute Z-value for the overlay element
*/
[[nodiscard]] constexpr qreal overlayZValue(qreal offset)
{
return OVERLAY_BASE + offset;
}
} // namespace ZValueLayerManager
#endif // Z_VALUE_LAYER_MANAGER_H

View file

@ -0,0 +1,83 @@
#ifndef Z_VALUES_H
#define Z_VALUES_H
#include "card_dimensions.h"
#include "z_value_layer_manager.h"
/**
* @file z_values.h
* @ingroup GameGraphics
* @brief Centralized Z-value constants for rendering layer order.
*
* Z-values in Qt determine stacking order. Higher values render on top.
* These constants define the visual layering hierarchy for the game scene.
*
* ## Layer Architecture
*
* See z_value_layer_manager.h for detailed documentation on the three-layer
* architecture (Zone, Card, Overlay) and the rationale for Z-value choices.
*
* ## Quick Reference
*
* | Layer | Z-Value Range | Purpose |
* |----------|------------------|-----------------------------------|
* | Zone | 0.5 - 1.0 | Zone backgrounds, containers |
* | Card | 1.0 - 40,000,000 | Cards on table (position-based) |
* | Overlay | 2,000,000,000+ | UI elements above all cards |
*/
namespace ZValues
{
// Expose base for callers that need it
constexpr qreal OVERLAY_BASE = ZValueLayerManager::OVERLAY_BASE;
// Overlay layer Z-values for items that should appear above normal cards
constexpr qreal HOVERED_CARD = ZValueLayerManager::overlayZValue(1.0);
constexpr qreal ARROWS = ZValueLayerManager::overlayZValue(3.0);
constexpr qreal ZONE_VIEW_WIDGET = ZValueLayerManager::overlayZValue(4.0);
constexpr qreal DRAG_ITEM = ZValueLayerManager::overlayZValue(5.0);
constexpr qreal DRAG_ITEM_CHILD = ZValueLayerManager::overlayZValue(6.0);
constexpr qreal TOP_UI = ZValueLayerManager::overlayZValue(7.0);
/**
* @brief Compute Z-value for child drag items based on hotspot position.
*
* When dragging multiple cards together, each child card needs a unique Z-value
* to prevent Z-fighting (flickering/flashing). The Z-values are derived from
* their position when grabbed to conserve original stacking. The formula encodes
* 2D coordinates into a single value where X has higher weight, ensuring
* deterministic visual stacking.
*
* @param hotSpotX The X coordinate of the grab position
* @param hotSpotY The Y coordinate of the grab position
* @return Unique Z-value for the child drag item
*/
[[nodiscard]] constexpr qreal childDragZValue(qreal hotSpotX, qreal hotSpotY)
{
return DRAG_ITEM_CHILD + hotSpotX * 1000000 + hotSpotY * 1000 + 1000;
}
/**
* @brief Compute Z-value for cards on the table zone based on position.
*
* Cards lower on the table (higher Y) render above cards higher up,
* and cards to the right (higher X) render above cards to the left.
* This creates natural visual stacking for overlapping cards.
*
* @param x The X coordinate of the card position
* @param y The Y coordinate of the card position
* @return Z-value for the card's table position
*/
[[nodiscard]] constexpr qreal tableCardZValue(qreal x, qreal y)
{
return (y + CardDimensions::HEIGHT_F) * 100000.0 + (x + 1) * 100.0;
}
// Card layering (general architecture, not command-zone specific)
constexpr qreal CARD_BASE = 1.0;
constexpr qreal CARD_MAX = ZValueLayerManager::CARD_Z_VALUE_MAX;
} // namespace ZValues
#endif // Z_VALUES_H

Some files were not shown because too many files have changed in this diff Show more