From 0a426619b66178dd2257f1ece60ff80f86f40e88 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Thu, 30 Mar 2023 03:01:43 +0000 Subject: CTAN sync 202303300301 --- macros/latex/contrib/songproj/Makefile | 8 +- macros/latex/contrib/songproj/song2tex.py | 26 +- macros/latex/contrib/songproj/songproj.dtx | 67 +- macros/latex/contrib/songproj/songproj.pdf | Bin 100783 -> 131017 bytes macros/latex/contrib/wargame/README.md | 49 +- macros/latex/contrib/wargame/doc/compat.pdf | Bin 432806 -> 432819 bytes macros/latex/contrib/wargame/doc/symbols.pdf | Bin 184493 -> 184525 bytes macros/latex/contrib/wargame/doc/tutorial/Makefile | 15 +- .../latex/contrib/wargame/doc/tutorial/export.tex | 199 +- macros/latex/contrib/wargame/doc/tutorial/game.pdf | Bin 583838 -> 588181 bytes macros/latex/contrib/wargame/doc/tutorial/game.sty | 42 +- macros/latex/contrib/wargame/doc/tutorial/game.tex | 27 +- macros/latex/contrib/wargame/doc/tutorial/patch.py | 50 +- macros/latex/contrib/wargame/doc/wargame.pdf | Bin 1310006 -> 1444437 bytes macros/latex/contrib/wargame/source/Makefile | 46 +- .../latex/contrib/wargame/source/chit/battle.dtx | 77 + macros/latex/contrib/wargame/source/chit/core.dtx | 8 +- macros/latex/contrib/wargame/source/chit/dice.dtx | 286 + .../latex/contrib/wargame/source/chit/elements.dtx | 33 +- macros/latex/contrib/wargame/source/chit/misc.dtx | 429 +- .../contrib/wargame/source/chit/modifiers.dtx | 42 + macros/latex/contrib/wargame/source/chit/oob.dtx | 172 + macros/latex/contrib/wargame/source/chit/shape.dtx | 220 +- macros/latex/contrib/wargame/source/chit/stack.dtx | 54 + macros/latex/contrib/wargame/source/chit/table.dtx | 176 + macros/latex/contrib/wargame/source/hex/board.dtx | 393 +- macros/latex/contrib/wargame/source/hex/coord.dtx | 39 +- macros/latex/contrib/wargame/source/hex/core.dtx | 1 + macros/latex/contrib/wargame/source/hex/extra.dtx | 2 +- macros/latex/contrib/wargame/source/hex/paths.dtx | 100 +- macros/latex/contrib/wargame/source/hex/ridges.dtx | 80 +- macros/latex/contrib/wargame/source/hex/shape.dtx | 105 + macros/latex/contrib/wargame/source/hex/split.dtx | 517 + .../latex/contrib/wargame/source/hex/terrain.dtx | 13 +- .../contrib/wargame/source/hex/terrain/beach.dtx | 10 +- .../contrib/wargame/source/hex/terrain/city.dtx | 3 + .../wargame/source/hex/terrain/light_woods.dtx | 4 + .../wargame/source/hex/terrain/mountains.dtx | 4 + .../contrib/wargame/source/hex/terrain/rough.dtx | 4 + .../contrib/wargame/source/hex/terrain/swamp.dtx | 4 + .../contrib/wargame/source/hex/terrain/town.dtx | 4 + .../contrib/wargame/source/hex/terrain/village.dtx | 4 + .../contrib/wargame/source/hex/terrain/woods.dtx | 4 + macros/latex/contrib/wargame/source/hex/towns.dtx | 11 +- .../wargame/source/natoapp6c/frames/friendly.dtx | 24 + .../contrib/wargame/source/natoapp6c/shape.dtx | 19 +- macros/latex/contrib/wargame/source/util/bb.dtx | 226 + .../latex/contrib/wargame/source/util/compound.dtx | 242 + macros/latex/contrib/wargame/source/util/core.dtx | 1340 +- .../latex/contrib/wargame/source/util/export.dtx | 817 ++ macros/latex/contrib/wargame/source/util/icons.dtx | 118 + macros/latex/contrib/wargame/source/util/misc.dtx | 145 + .../latex/contrib/wargame/source/util/randomid.dtx | 22 + macros/latex/contrib/wargame/source/util/tikz.dtx | 196 + .../latex/contrib/wargame/source/utils/wgexport.py | 14697 ++++++++++++++----- .../contrib/wargame/source/utils/wgsvg2tikz.py | 2 +- macros/latex/contrib/wargame/source/wargame.dtx | 265 +- macros/latex/contrib/wargame/source/wargame.ins | 28 +- macros/luatex/generic/luavlna/README.md | 2 +- macros/luatex/generic/luavlna/luavlna-doc.pdf | Bin 92320 -> 92324 bytes macros/luatex/generic/luavlna/luavlna.lua | 40 +- macros/luatex/generic/luavlna/luavlna.sty | 7 +- macros/luatex/generic/luavlna/luavlna.tex | 5 +- 63 files changed, 15890 insertions(+), 5633 deletions(-) create mode 100644 macros/latex/contrib/wargame/source/chit/battle.dtx create mode 100644 macros/latex/contrib/wargame/source/chit/dice.dtx create mode 100644 macros/latex/contrib/wargame/source/chit/modifiers.dtx create mode 100644 macros/latex/contrib/wargame/source/chit/oob.dtx create mode 100644 macros/latex/contrib/wargame/source/chit/stack.dtx create mode 100644 macros/latex/contrib/wargame/source/chit/table.dtx create mode 100644 macros/latex/contrib/wargame/source/hex/split.dtx create mode 100644 macros/latex/contrib/wargame/source/util/bb.dtx create mode 100644 macros/latex/contrib/wargame/source/util/compound.dtx create mode 100644 macros/latex/contrib/wargame/source/util/export.dtx create mode 100644 macros/latex/contrib/wargame/source/util/icons.dtx create mode 100644 macros/latex/contrib/wargame/source/util/misc.dtx create mode 100644 macros/latex/contrib/wargame/source/util/randomid.dtx create mode 100644 macros/latex/contrib/wargame/source/util/tikz.dtx (limited to 'macros') diff --git a/macros/latex/contrib/songproj/Makefile b/macros/latex/contrib/songproj/Makefile index 48edf01b51..de8b57bc12 100644 --- a/macros/latex/contrib/songproj/Makefile +++ b/macros/latex/contrib/songproj/Makefile @@ -22,10 +22,12 @@ pdf: songproj.pdf mostlyclean: rm -f *.aux *.glo *.gls *.hd *.idx *.ilg *.ind *.log *.out +ctanclean: mostlyclean + rm -f *.sty + clean: mostlyclean rm -f *.pdf -maintainer-clean: clean - rm -f *.sty +maintainer-clean: ctanclean clean -.PHONY: all sty doc mostlyclean clean maintainer-clean +.PHONY: all sty doc mostlyclean ctanclean clean maintainer-clean diff --git a/macros/latex/contrib/songproj/song2tex.py b/macros/latex/contrib/songproj/song2tex.py index dc32da2b3f..270a9ef721 100755 --- a/macros/latex/contrib/songproj/song2tex.py +++ b/macros/latex/contrib/songproj/song2tex.py @@ -8,6 +8,7 @@ import re # The following regexp match the typical markers of refrain and couplet: re_refrain = re.compile(r'(R(efrain)?|C(horus)?)( |[-./] ?)', re.IGNORECASE) re_couplet = re.compile(r'\d+( |[-./] ?)') +re_txt_ext = re.compile(r'\.te?xt$') def match_rest(regexp, s): @@ -41,35 +42,35 @@ def parse_song(lines, re_refrain, re_couplet): yield '\\begin{refrain}\n' state = State.REFRAIN line = rest - yield ' {} \\\\\n'.format(line) + yield ' {}'.format(line) elif (rest := match_rest(re_couplet, line)) is not None: # when a couplet start marker is found, enter a couplet. yield '\\begin{couplet}\n' state = State.COUPLET line = rest - yield ' {} \\\\\n'.format(line) + yield ' {}'.format(line) elif line != '': # when a non-empty line is found, also enter a couplet as all. yield '\\begin{couplet}\n' state = State.COUPLET - yield ' {} \\\\\n'.format(line) + yield ' {}'.format(line) elif state == State.REFRAIN: # We are in the refrain. Depending on what we find: if line != '': # when a non-empty line is found, stay in the refrain. - yield ' {} \\\\\n'.format(line) + yield ' \\\\\n {}'.format(line) else: # when an empty line is found, leave the refrain. - yield '\\end{refrain}\n\n' + yield '\n\\end{refrain}\n\n' state = State.LIMBO elif state == State.COUPLET: # We are in a couplet. Depending on what we find: if line != '': # when a non-empty line is found, stay in the couplet. - yield ' {} \\\\\n'.format(line) + yield ' \\\\\n {}'.format(line) else: # when an empty line is found, leave the couplet. - yield '\\end{couplet}\n\n' + yield '\n\\end{couplet}\n\n' state = State.LIMBO # No matter what we found, we have a song line, possibly empty, that we # have to check to see if it could not be the new longest line seen so far. @@ -78,9 +79,9 @@ def parse_song(lines, re_refrain, re_couplet): # The song text file is now finished. We may still be in the refrain or in a # couplet, that we have to close. if state == State.REFRAIN: - yield '\\end{refrain}\n\n' + yield '\n\\end{refrain}\n\n' elif state == State.COUPLET: - yield '\\end{couplet}\n\n' + yield '\n\\end{couplet}\n\n' # Now print the longest line of the entire song. yield '\\longest{{{}}}\n'.format(longest) @@ -92,8 +93,13 @@ def main(args=None): parser.add_argument("--couplet", '-c', type=re.compile, default=re_couplet, metavar="REGEXP", help="specify the couplet marker (regexp, default \"{}\")".format(re_couplet.pattern)) parser.add_argument("infile", help="input song text file", type=argparse.FileType('r')) - parser.add_argument("outfile", help="output LaTeX file", type=argparse.FileType('w')) + parser.add_argument("outfile", help="output LaTeX file", type=argparse.FileType('w'), nargs='?') args = parser.parse_args(args) + if args.outfile is None: + try: + args.outfile = open(re_txt_ext.sub('', args.infile.name) + '.tex', 'w') + except AttributeError: + parser.error("the following argument is required: outfile") for line in parse_song(args.infile, args.refrain, args.couplet): args.outfile.write(line) diff --git a/macros/latex/contrib/songproj/songproj.dtx b/macros/latex/contrib/songproj/songproj.dtx index 7c1783fe15..6ed09318c0 100644 --- a/macros/latex/contrib/songproj/songproj.dtx +++ b/macros/latex/contrib/songproj/songproj.dtx @@ -34,7 +34,7 @@ %\NeedsTeXFormat{LaTeX2e}[2020/10/01] %\ProvidesPackage{songproj} %<*package> - [2022/11/07 v1.0.1 Song projection] + [2023/03/29 v1.2.0 Song projection] % % %<*driver> @@ -52,7 +52,7 @@ % % \fi % -% \CheckSum{256} +% \CheckSum{269} % % \CharacterTable % {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z @@ -75,6 +75,8 @@ % \changes{v0.3.0}{2021/09/26}{Use \textsf{expl3} naming conventions} % \changes{v1.0.0}{2021/11/07}{First public release} % \changes{v1.0.1}{2021/11/07}{Add the generated extension file to Git} +% \changes{v1.1.0}{2023/03/27}{Allow couplet numbering with \cs{numbercouplets}} +% \changes{v1.2.0}{2023/03/29}{Allow couplet numbering with \cs{inputsong*}} % % \GetFileInfo{songproj.dtx} % @@ -147,6 +149,16 @@ % is only used to compute and record its length, and will not be typeset. % \end{function} % +% \changes{v1.1.0}{2023/03/27}{Document the \cs{numbercouplets} command} +% \begin{function}{\numbercouplets} +% \label{doc-cs-numbercouplets} +% Inside a \env{song} environment, the \cs{numbercouplets} command can be used +% to enable couplet numbering. This can be useful when specific couplets have +% been selected but the lead singer has a score a lyrics sheet that includes +% all of them: indicating the couplet numbers in the projection will allow them +% to check which couplet to sing. +% \end{function} +% % \changes{v0.4.0}{2021/09/29}{Document \env{intro} and \env{final} environments} % % \paragraph{} @@ -215,10 +227,12 @@ % % \subsection{The \cs{inputsong} command} % \changes{v1.0.0}{2021/11/07}{Document the \cs{inputsong} command} +% \changes{v1.2.0}{2023/03/29}{Document the \cs{inputsong*} command} % -% \begin{function}{\inputsong} +% \begin{function}{\inputsong, \inputsong*} % \begin{syntax} % \cs{inputsong}\marg{file}\marg{stanzas per slide}\oarg{couplet list} +% \cs{inputsong*}\marg{file}\marg{stanzas per slide}\oarg{couplet list} % \end{syntax} % % The \cs{inputsong} command environment is used as a shortcut for @@ -235,6 +249,9 @@ % \end{verbatim} % \end{function} % +% The starred version \cs{inputsong*} enables couplet numbering, as described +% in \ref{doc-cs-numbercouplets}. +% % \subsection{The \env{refrain}, \env{couplet}, \env{intro} and \env{final} environments} % % \changes{v0.4.0}{2021/09/29}{Document \env{intro} and \env{final} environments} @@ -293,6 +310,7 @@ % \end{verbatim} % % \subsubsection{Importing text lyrics} +% \changes{v1.1.0}{2023/03/27}{Derivate output file name from input file name if not specified} % % Song lyrics are often found in text format with basic markup: % @@ -391,6 +409,8 @@ % % \changes{v0.2.0}{2021/09/23}{Use \texttt{sp} prefix for functions and % variables} +% \changes{v1.1.0}{2023/03/27}{Add the \cs{g__sp_show_numbers_bool} variable to +% number or not couplets} % % We define a number of internal variables, that are used when reading and % formatting a song. All of these variables are meant to be set globally: since @@ -402,6 +422,7 @@ \bool_new:N \g__sp_song_start_bool % are we at the start of a song? \bool_new:N \g__sp_refrain_first_bool % does current song start with the % refrain? +\bool_new:N \g__sp_show_numbers_bool % should we show the couplet numbers? \int_new:N \g__sp_stanzas_per_slide_int % number of stanzas to show on each % slide (1 or 2) \dim_new:N \g__sp_linewidth_dim % length of the longest line in current @@ -500,8 +521,9 @@ % \end{macro} % % \begin{macro}{\__sp_song_couplet:n} +% \changes{v1.1.0}{2023/03/27}{Print the couplet number when requested} % -% This macro uses the \env{__sp_couplet} environment to a specified couplet of +% This macro uses the \env{__sp_couplet} environment to format a specified couplet of % the current song. It takes a single argument: % % \begin{arguments} @@ -515,6 +537,9 @@ \dim_compare:nNnTF \g__sp_linewidth_dim {=} {0pt} { \begin{__sp_couplet} } { \begin{__sp_couplet} [\g__sp_linewidth_dim] } + \bool_if:NTF \g__sp_show_numbers_bool + { \flagverse{#1.} } + {} \seq_item:Nn \g__sp_couplets_seq {#1} \end{__sp_couplet} } @@ -832,7 +857,20 @@ % \end{macrocode} % \end{macro} % +% \begin{macro}{\numbercouplets} +% \changes{v1.1.0}{2023/03/27}{New command to enable couplet numbering} +% +% This macro can be used within a song to indicate that its couplets should be +% numbered. +% +% \begin{macrocode} + \NewDocumentCommand {\numbercouplets}{} + { \bool_gset_true:N \g__sp_show_numbers_bool } +% \end{macrocode} +% \end{macro} +% % \begin{environment}{song} +% \changes{v1.1.0}{2023/03/27}{Do not number couplets by default} % % This environment is used as a container for entire songs. On opening, it does several things: % \begin{enumerate} @@ -847,6 +885,9 @@ % we are at the start of the song, which will allow the next % \env{refrain} or \env{couplet} to tell if the song starts with the % refrain or with a couplet; +% \item it sets the \cs{g__sp_show_numbers_bool} variable to false, to +% indicate that the couplets should not be numbered by default (the user +% will be able to override this with the \cs{numbercouplets} command). % \end{enumerate} % % This environment takes two arguments: @@ -877,6 +918,9 @@ % Indicate that we are in a song, and at its start \bool_gset_true:N \g__sp_song_bool \bool_gset_true:N \g__sp_song_start_bool + + % Couplets should not be numbered by default + \bool_gset_false:N \g__sp_show_numbers_bool } % \end{macrocode} % @@ -994,18 +1038,21 @@ % \end{environment} % % \begin{macro}{\inputsong} -% \changes{v1.0.0}{2021/11/07}{Add a \cs{inputsong} command} +% \changes{v1.0.0}{2021/11/07}{Add an \cs{inputsong} command} +% \changes{v1.2.0}{2023/03/29}{Add an \cs{inputsong*} command} % % This macro starts a \env{song} environment and \cs{input}s the song content % from an external file. % % \begin{macrocode} -\NewDocumentCommand {\inputsong} { m m o } +\NewDocumentCommand {\inputsong} { s m m o } { - \IfNoValueTF {#3} - { \begin{song} {#2} } - { \begin{song} {#2} [#3] } - \input{#1} + \IfNoValueTF {#4} + { \begin{song} {#3} } + { \begin{song} {#3} [#4] } + \IfBooleanT {#1} + { \numbercouplets } + \input{#2} \end{song} } % \end{macrocode} diff --git a/macros/latex/contrib/songproj/songproj.pdf b/macros/latex/contrib/songproj/songproj.pdf index 24216ce150..1329f0e986 100644 Binary files a/macros/latex/contrib/songproj/songproj.pdf and b/macros/latex/contrib/songproj/songproj.pdf differ diff --git a/macros/latex/contrib/wargame/README.md b/macros/latex/contrib/wargame/README.md index 000f3cd68b..3de32ab4d0 100644 --- a/macros/latex/contrib/wargame/README.md +++ b/macros/latex/contrib/wargame/README.md @@ -1,8 +1,6 @@ -A package to make Hex'n'Counter wargames in LaTeX -================================================= +# A package to make Hex'n'Counter wargames in LaTeX -Version 0.3.2 -------------- +## Version 0.5 This package can help make classic [Hex'n'Counter wargames](https://en.wikipedia.org/wiki/Wargame) using LaTeX. The @@ -25,14 +23,12 @@ one will need for a game (rules, charts, boards, counter sheets). - The package support exporting the game to a [VASSAL](#vassal-support%20'Section%20below') module -Sources -------- +## Sources The sources of the package are kept at [GitLab](https://gitlab.com/wargames_tex/wargame_tex) -Download from GitLab --------------------- +## Download from GitLab - [Zip file of package and support files](https://gitlab.com/wargames_tex/wargame_tex/-/jobs/artifacts/master/download?job=dist) @@ -67,14 +63,12 @@ to install in `~/texmf`. If you prefer another destination, say make install DESTDIR=/usr/local/share/texmf -Download from CTAN ------------------- +## Download from CTAN The package is available from [CTAN](https://ctan.org/tex-archive/macros/latex/contrib/wargame) in the -directory `/macros/latex/contrib/wargame`. As of this writing, the -package has not hit any of the major distributions (e.g., -[TeXLive](https://tug.org/texlive/index.html)). +directory `/macros/latex/contrib/wargame`. The package is part of the +CTAN distribution [TeXLive](https://tug.org/texlive/index.html). ### From TDS zip archive @@ -170,16 +164,14 @@ the module `PIL` for this. On Debian based systems, do sudo apt install poppler-utils python3-pil -Tutorial --------- +## Tutorial See the [tutorial](tutorial/README.md "Link works on GitLab only") page for more ([here](https://ctan.org/tex-archive/macros/latex/contrib/wargame/doc/tutorial/) if you browse from CTAN). -Examples --------- +## Examples Below are some print'n'play board wargames made with this package. These are not original games but rather revamps of existing games. All credits @@ -192,6 +184,9 @@ goes to the original authors of these games. - Western Front - [D-Day](https://gitlab.com/wargames_tex/dday_tex) (Avalon Hill classic) + - [Smithsonian + D-Day](https://gitlab.com/wargames_tex/sdday_tex) (Avalon + Hill) - [Paul Koenig's Market Garden](https://gitlab.com/wargames_tex/pkmg_tex) - [Paul Koenig's @@ -208,9 +203,10 @@ goes to the original authors of these games. - [Strike Force One](https://gitlab.com/wargames_tex/sfo_tex) - [Kriegspiel](https://gitlab.com/wargames_tex/kriegspiel_tex) (Avalon Hill classic) + - [Port Stanley](https://gitlab.com/wargames_tex/portstanley_tex) + (from the *Wargamer* magazine) -VASSAL support --------------- +## VASSAL support The packages has the script [`wgexport.py`](utils/wgexport.py) to generate a draft [VASSAL](https://vassalengine.org) module from the @@ -231,8 +227,14 @@ An example of this is given in the [Battle for Moscow](https://gitlab.com/wargames_tex/bfm_tex) project, and of course in the [tutorial](tutorial/ "Link works on GitLab only") -NATO App6 ---------- +The sources of this script is kept in a different project +[`pywargame`](https://gitlab.com/wargames_tex/pywargame) also on GitLab. +The script is generated in that package. The `pywargame` package allows +one to manipulate VASSAL modules from Python, or to read (and convert) +[CyberBoard](http://cyberboard.norsesoft.com/) +([GitHub](https://github.com/CyberBoardPBEM/cbwindows)) scenarios. + +## NATO App6 The package supports @@ -257,8 +259,7 @@ standard. - Other LaTeX package for making NATO symbology [XMilSymb](https://github.com/ralphieraccoon/MilSymb) -Copyright and license ---------------------- +## Copyright and license (c) 2022 Christian Holm Christensen @@ -277,7 +278,7 @@ you generate using the package is *not* required to be licensed according to the *ShareAlike* clause of CC-BY-SA-4.0. That is, you can license your materials *any way you want*. -However, that copyright rights on games is not as prohibitive as you may +However, copyright rights on games is not as prohibitive as you may think (see [this thread on BGG](https://boardgamegeek.com/thread/493249/)). What you *can* copyright is original, artistic expression. That is, the copyrighted diff --git a/macros/latex/contrib/wargame/doc/compat.pdf b/macros/latex/contrib/wargame/doc/compat.pdf index 70583eca2f..2ba344d4ab 100644 Binary files a/macros/latex/contrib/wargame/doc/compat.pdf and b/macros/latex/contrib/wargame/doc/compat.pdf differ diff --git a/macros/latex/contrib/wargame/doc/symbols.pdf b/macros/latex/contrib/wargame/doc/symbols.pdf index b7f8a0a6c0..cd8f0d3ddc 100644 Binary files a/macros/latex/contrib/wargame/doc/symbols.pdf and b/macros/latex/contrib/wargame/doc/symbols.pdf differ diff --git a/macros/latex/contrib/wargame/doc/tutorial/Makefile b/macros/latex/contrib/wargame/doc/tutorial/Makefile index 4947d84ee0..ed50fabad5 100644 --- a/macros/latex/contrib/wargame/doc/tutorial/Makefile +++ b/macros/latex/contrib/wargame/doc/tutorial/Makefile @@ -8,7 +8,7 @@ LATEX_FLAGS := -interaction=nonstopmode \ -shell-escape EXPORT := ../utils/wgexport.py EXPORT_FLAGS := - +PATCH := patch.py ifdef VERBOSE MUTE := REDIR := @@ -22,15 +22,15 @@ endif %.aux:%.tex @echo "LATEX $< -> $@" - $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR) + $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR) %.pdf:%.aux @echo "LATEX $*.tex -> $@ (via $*.aux)" - $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR) + $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR) %.pdf:%.tex @echo "LATEX $< -> $@" - $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR) + $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR) %.json:%.pdf @@ -39,11 +39,12 @@ all: game.pdf Game.vmod game.aux:game.tex game.sty export.tex export.pdf:export.tex game.sty -Game.vmod:export.pdf export.json patch.py game.pdf +Game.vmod:export.pdf export.json $(PATCH) game.pdf @echo "$(EXPORT) $< -> $@" - $(MUTE)$(EXPORT) export.pdf export.json -p patch.py -r game.pdf \ + $(MUTE)$(EXPORT) export.pdf export.json -p $(PATCH) -r game.pdf \ -d "Example module from LaTeX PnP game" \ - -t "LaTeX wargame tutorial" -v 0.1 -o $@ + -t "LaTeX wargame tutorial" -v 0.1 -o $@ \ + $(EXPORT_FLAGS) clean: rm -f *~ *.log *.out *.aux *.pdf *.vmod *.json diff --git a/macros/latex/contrib/wargame/doc/tutorial/export.tex b/macros/latex/contrib/wargame/doc/tutorial/export.tex index d51ecf0e5c..6d6722d9ac 100644 --- a/macros/latex/contrib/wargame/doc/tutorial/export.tex +++ b/macros/latex/contrib/wargame/doc/tutorial/export.tex @@ -72,14 +72,14 @@ % For that we will use the macro \verb+\doublechitimages+ (if we had % single sided counters, we would use \verb+\chitimages+). This macro % takes two arguments: The list of counter definitions and the faction -% of the chits. So lets go ahead and make those for factions A and B, +% of the chits. Let's go ahead and make those for factions A and B, % and the special faction ``Markers'' which will only be the game turn % counter. % % \begin{macrocode} \doublechitimages[A]{\alla} \doublechitimages[B]{\allb} -\doublechitimages[Markers]{{game turn}} +\doublechitimages[Markers]{{game turn chit}} % \end{macrocode} % % Again, the macros we made for the counters comes in handy. Note @@ -87,6 +87,51 @@ % VASSAL module. We will, in fact, use them one more time to create % the counter sheet. % +% To automatically make ``battle markers'' --- markers that identify +% individual battles by placing a numbered marker on top of the +% combatants, we can use the macro \verb+battlemarkers+. It takes one +% argument, which is the number of unique markers to make and add. +% The markers are round yellow circles with a number in +% them\footnote{This can of course be customised as everything else +% can.}. +% +% \begin{macrocode} +\battlemarkers{12} +% \end{macrocode} +% +% Furthermore, we can add ``odds'' and battle result markers. Odds +% markers can be placed via the battle markers context menu, and then +% later be replaced by result markers via the context menu of the odds +% markers. This is done via the macros \verb+\oddsmarkers+ and +% \verb+\resultmarkers+. Both macros accept a list of options (odds +% and results, respectively) with a possible background colour to use +% in the markers. +% +% \begin{macrocode} +\oddsmarkers{% + 1:3/red!25!white,% + 1:2/red!15!white,% + 1:1/orange!15!white,% + 2:1/white,% + 3:1/green!15!white,% + 4:1/green!25!white} +\resultmarkers{ + AE/red!50!white, + AR/red!25!white, + EX/white, + DR/green!10!white, + DE/green!25!white} +% \end{macrocode} +% +% By default, odds markers must be placed ``by-hand'', but via the +% VASSAL preferences it is possible to calculate the odds and show +% them when the battle is declared. By default, this calculation will +% only be concerned with the combat factors of the involved units, and +% features such as terrain is ignored. However, with a bit of Python +% skill, one can flesh out the implementation of the game rules in the +% optional patch (Python) script. We will not do that here, as a it +% is a little beyond the scope of this tutorial. +% % Right, so that made our counter images and the associated meta % data. Next thing is to add our board. For that, we use the % environment \texttt{boardimage}. Inside that environment we must @@ -116,7 +161,7 @@ % \end{macrocode} % % We have two charts that we would also like to be put in: The CRT and -% the TE. These \emph{must} be put into \Tikz{} pictures, and we +% the TEC. These \emph{must} be put into \Tikz{} pictures, and we % precede those with the macro \verb+\info+. This macro takes three % arguments: The name of the next image, the category, and % sub-category. Again, the name is more or less free form, and the @@ -153,7 +198,7 @@ \end{tikzpicture} % \end{macrocode} % -% We can a splash page image. We have not defined such an image in +% We can add splash page image. We have not defined such an image in % the \texttt{game} package, but we could have, so we will make it % here. Again, it should be a \Tikz{} picture. For this, we need to % use the category \texttt{front}. @@ -172,68 +217,67 @@ % the like. We will add a few such icons here, mainly to show how % the \texttt{patch.py} script works. % -% First, a button icon for the pool window. +% The \textsf{wgexport.cls} class provides a number of icons we may +% use for this. These are +% +% \begin{center} +% \begin{tikzpicture} +% \pic {flip icon}; +% \pic at (2.0,0) {restore icon}; +% \pic at (3.5,0) {eliminate icon}; +% \pic at (5.0,0) {pool icon}; +% \pic at (6.5,0) {oob icon={black}{black}}; +% \end{tikzpicture} +% \end{center} +% +% +% We will add these as pictures to our export PDF. First, the image +% for the pool of units. % % \begin{macrocode} \info{pool-icon}{icon}{} -\begin{tikzpicture}[scale=.15] - \draw[scale line widths,line width=4mm] - (0,1)--(0,-1) (-.7,.2)--(.7,.2); +\begin{tikzpicture}[transform shape,scale=.4] + \pic{pool icon}; \end{tikzpicture} % \end{macrocode} % -% The category \texttt{icon} has no special meaning. The icon will -% look like \tikz[scale=.3]{\draw[scale line widths,line width=4mm] -% (0,1)--(0,-1) (-.7,.2)--(.7,.2);} +% The category \texttt{icon} has no special meaning. % % We also want to add a custom icon for the OOB button. VASSAL has a % button icon for the piece inventory which is really quite -% appropriate, but alas it is already in use, so we will make our own -% (albeit similar). +% appropriate, but alas it is already in use, so we will make our +% own. Here, we use the \TikZ{} picture \texttt{oob icon} provided by +% \textsf{wgexport}. This picture needs two arguments: the left hand +% and right hand sides fill colours. We will use our background +% colours. % % \begin{macrocode} \info{oob-icon}{icon}{} -\begin{tikzpicture}[scale=.6] - \draw[fill=friendly] (.1,0) rectangle(.5,-.1); - \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25); - \draw[fill=friendly] (.1,-.3) rectangle(.2,-.4); - \draw[fill=hostile] (-.1,0) rectangle(-.3,-.25); +\begin{tikzpicture}[transform shape,scale=.4] + \pic{oob icon={a-bg}{b-bg}}; \end{tikzpicture} % \end{macrocode} -% -% This looks like -% \tikz{ -% \draw[fill=friendly] (.1,0) rectangle(.5,-.1); -% \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25); -% \draw[fill=friendly] (.1,-.3) rectangle(.2,-.4); -% \draw[fill=hostile] (-.1,0) rectangle(-.3,-.25);} % -% Normally the \texttt{wgexport.py} script used the \emph{undo} mage +% Normally the \texttt{wgexport.py} script uses the \emph{undo} image % for the flip button. However, that may be a bit confusing, so we -% will use a large ``F'' -% instead. \tikz{\node[color=blue!75!black,font=\sffamily\bfseries\LARGE]{F}} +% will use a custom image for that. We will use the pictures provided +% by \textsf{wgexport}. % -% And similarly for the ``Eliminate''\tikz{\node[color=red!75!black,font=\sffamily\bfseries\LARGE]{E}} -% -% and ``Restore'' buttons. \tikz{\node[color=green!75!black,font=\sffamily\bfseries\LARGE]{R}} % % \begin{macrocode} \info{flip-icon}{icon}{} -\begin{tikzpicture} - \node[color=blue!75!black,inner sep=0pt, - font=\sffamily\bfseries\large]{F}; +\begin{tikzpicture}[transform shape,scale=.4] + \pic{flip icon}; \end{tikzpicture} % \info{eliminate-icon}{icon}{} -\begin{tikzpicture} - \node[color=red!75!black,inner sep=0pt, - font=\sffamily\bfseries\large]{E}; +\begin{tikzpicture}[transform shape,scale=.4] + \pic{eliminate icon}; \end{tikzpicture} % \info{restore-icon}{icon}{} -\begin{tikzpicture} - \node[color=green!75!black,inner sep=0pt, - font=\sffamily\bfseries\large]{R}; +\begin{tikzpicture}[transform shape,scale=.4] + \pic{restore icon}; \end{tikzpicture} % \end{macrocode} % @@ -262,7 +306,7 @@ % course Un*x-like syntax. For other OSs, consult your \TeX{} % distribution's documentation for how to find files in the \TeX{} % installation. - +% % You can open the module in the VASSAL editor and see what was % generated. On the board, all areas marked with the \texttt{zone % scope} (or \texttt{zone path}) should be defined as zones. In the @@ -328,10 +372,9 @@ % done the heavy work for us. Not least be because we have taken care % to add in \texttt{zone} styles where needed. % -% We will -% put all the counters on the OOB chart, and adjust some grids - most -% notably the OOB and turn track grids. Other than that, we will not -% do much. +% We will put all the counters on the OOB chart, and adjust some grids +% - most notably the OOB and turn track grids. Other than that, we +% will not do much. % % \begin{verbatim} % `kpsewhich wgexport.py` export.pdf export.json \ @@ -344,37 +387,61 @@ % Now for the script: % % \begin{verbatim} -% def patch(build,data,vmod,verbose=False): -% # We need to import the export module :-) -% import wgexport as wg +% # --- We may need to import the export module --- +% # +% # from wgexport import * % -% # Get the game and last free GPID -% game = wg.get_game(build) -% gpid = int(game.getAttribute('nextPieceSlotId')) +% def patch(build,data,vmod,verbose=False,**kwargs): +% # --- Get the game --- +% game = build.getGame() % -% # Get all boards -% maps = wg.get_maps(build) -% board = maps['Board'] +% # --- Get the maps --- +% maps = game.getMaps() % -% mkeys = wg.get_masskey(board,'name') -% wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png') -% wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png') +% # --- Get the main board --- +% board = maps['Board'] +% +% # --- Get the mass keys --- +% mkeys = board.getMassKeys() +% mkeys['Eliminate']['icon'] = 'eliminiate-icon.png' +% mkeys['Flip'] ['icon'] = 'flip-icon.png' % -% # Get main map +% # --- Get the dead-pool map --- % pool = maps['DeadMap'] -% wg.set_node_attr(pool,icon='pool-icon.png') +% pool['icon'] = 'pool-icon.png' +% pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png' % -% mkeys = wg.get_masskey(pool,'name') -% wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png') -% -% # Get the OOB map -% oob = wg.get_chartwindows(game)['OOBs'] -% wg.set_node_attr(oob,icon='oob-icon.png') +% # --- Get the OOB map --- +% oob = game.getChartWindows()['OOBs'] +% oob['icon'] = 'oob-icon.png' +% +% +% # +% # EOF +% # % \end{verbatim} % % In the \texttt{patch.py} script we can use all the functionality % provided by \texttt{wgexport.py}. Elements are XML elements that we -% apply \texttt{xml.dom.minidom} operations on. +% apply \texttt{xml.dom.minidom} operations on. +% +% Note, if we need to use classes, etc.\ from \texttt{wgexport.py}, +% then we ought to import that module into our patch script, as shown +% in the top comment above. In the example patch script we do not +% need that, so we leave it out. +% +% One can do quite complicated things in VASSAL, which can be set-up +% in the patch script. By default, the \texttt{wgexport} script sets +% up the pieces and boards, and if one has defined regions with +% specific piece names, then the script will likewise place the pieces +% there (as with our \texttt{game turn} example above). +% +% One can, for example, setup the module so that the game turn marker +% automatically flips or progresses when the turn track widget reach +% specific points. One can in principle also set it up so that when a +% specific turn or phase is reach, then pieces are moved from the OOB +% to the board. The more one can do like that, the more the game is +% automated. % % \iffalse % Local Variables: diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.pdf b/macros/latex/contrib/wargame/doc/tutorial/game.pdf index 14bcde8f98..f4e8a22e2a 100644 Binary files a/macros/latex/contrib/wargame/doc/tutorial/game.pdf and b/macros/latex/contrib/wargame/doc/tutorial/game.pdf differ diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.sty b/macros/latex/contrib/wargame/doc/tutorial/game.sty index 0c8fcc0165..83a5c02185 100644 --- a/macros/latex/contrib/wargame/doc/tutorial/game.sty +++ b/macros/latex/contrib/wargame/doc/tutorial/game.sty @@ -899,7 +899,7 @@ % \begin{figure} % \centering % \begin{tikzpicture} -% \chit[game turn] +% \chit[game turn chit] % \end{tikzpicture} % \caption{The \textsf{wargame} game turn counter} % \label{fig:plain-game-turn} @@ -910,8 +910,8 @@ % % \begin{macrocode} \tikzset{ - game turn/.append style={a}, - game turn flipped/.style={game turn,b} + game turn chit/.append style={a}, + game turn chit flipped/.append style={b} } % \end{macrocode} % @@ -920,7 +920,7 @@ % \begin{figure} % \centering % \begin{tikzpicture} -% \doublechits{{game turn}}{1}{1.24} +% \doublechits{{game turn chit}}{1}{1.24} % \end{tikzpicture} % \caption{Modified game turn, front and back} % \label{fig:game-turn} @@ -1346,6 +1346,8 @@ text=titlefg, font=\sffamily\bfseries\Huge, scale=1.5, + inner sep=0pt, + outer sep=0pt, transform shape, }, sub title/.style={ @@ -1354,6 +1356,8 @@ scale=1.5, transform shape, text width=5cm, + inner sep=0pt, + outer sep=0pt, align=right } } @@ -1859,7 +1863,7 @@ turn, transform shape, anchor=east, - outer sep=5mm, + outer sep=2mm, scale=.7}, } % \end{macrocode} @@ -1921,7 +1925,7 @@ % % \begin{verbatim} % \begin{tikzpicture}[oob turn/.append style={anchor=west}] -% \oob{\alla}{4}{1.24}{0} +% \oob*{\alla}{4}{1.24}{0} % \end{tikzpicture} % \begin{tikzpicture} % \oob{\allb}{4}{1.24}{0} @@ -1950,31 +1954,31 @@ at (0,2.5) {};% % Game title \node[% - below right=5mm and 5mm of oob frame.north west, - title,scale=.8]{A Game}; + below right=5mm and 5mm of oob frame.north west, + title,scale=.8]{A Game}; % Chart title \node[% - below left=5mm and 5mm of oob frame.north east, - sub title,scale=1.2,transform shape]{% + below left=5mm and 5mm of oob frame.north east, + sub title,scale=1.2,transform shape]{% Order of Battle}; % Turn title \node[font=\sffamily\bfseries\Large, - white,transform shape] - at (0,1) {Turn}; + anchor=south, + white,transform shape] at (0,1) {Turn}; % \begin{scope}[% - transform shape, - oob turn/.append style={anchor=west},% - shift={(-1.35,0)}, - zone scope=A OOB]% + transform shape, + oob turn/.append style={anchor=west},% + shift={(-1.4,0)}, + zone scope=A OOB]% \oobemptyturn% \oob*{\alla}{4}{1.4}{.2}% \end{scope}% % \begin{scope}[% - transform shape, - shift={(1.35,0)}, - zone scope=B OOB]% + transform shape, + shift={(1.4,0)}, + zone scope=B OOB]% \oobrealturn% \oob{\allb}{4}{1.4}{.2}% \end{scope}% diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.tex b/macros/latex/contrib/wargame/doc/tutorial/game.tex index 6db72bff27..062ef41c1a 100644 --- a/macros/latex/contrib/wargame/doc/tutorial/game.tex +++ b/macros/latex/contrib/wargame/doc/tutorial/game.tex @@ -3,6 +3,7 @@ \usepackage{game} \usepackage[margin=1cm,bottom=2cm,top=2cm,a4paper]{geometry} \newcommand\Tikz{Ti{\itshape k}Z/PGF} +\let\TikZ\Tikz \setlength{\parskip}{.5ex} \setlength{\parindent}{0em} \newcommand\tightlist{\setlength{\itemsep}{3pt} @@ -113,8 +114,8 @@ the macros \verb+\alla+ and \verb+\allb+ yet again. \end{verbatim} As a final bonus, we also make a board that includes our three charts: -The OOB, CRT, and TE. We will not demonstrate the code here, but take -a look in the sources if you are curious. +The OOB, CRT, and TEC. We will not demonstrate the code here, but take +a look in the sources if you are curious. \newcommand\fullboard[1][]{% \begin{tikzpicture}[#1,rotate=90]% @@ -126,13 +127,23 @@ a look in the sources if you are curious. \inneroob \end{scope} % - \node[anchor=south west,minimum width=6.2cm,minimum height=10cm, - line width=1pt,sub title,scale=.66,draw,align=center, - text depth=9.7cm,fill=neutral,transform shape] - at (oob frame.south west) + \node[ + anchor=south west, + minimum width=6.2cm, + minimum height=10.5cm, + line width=1pt, + sub title, + scale=.66, + draw, + align=center, + text depth=9.7cm, + fill=neutral, + transform shape, + above right=1pt of oob frame.south west + ] (charts) {{\LARGE Charts}}; % - \node[above right=5mm and 4mm of oob frame.south west, + \node[above right=5mm and 4mm of charts.south west, inner sep=0pt, transform shape,scale=.75,anchor=south west] (crt) {\crt}; % @@ -185,7 +196,7 @@ corrections, and so on are very welcomed. \end{tikzpicture} \begin{tikzpicture} - \doublechits{{game turn}}{1}{1.24} + \doublechits{{game turn chit}}{1}{1.24} \end{tikzpicture} \end{center} diff --git a/macros/latex/contrib/wargame/doc/tutorial/patch.py b/macros/latex/contrib/wargame/doc/tutorial/patch.py index 0fcdd49645..b01cba6d14 100644 --- a/macros/latex/contrib/wargame/doc/tutorial/patch.py +++ b/macros/latex/contrib/wargame/doc/tutorial/patch.py @@ -1,31 +1,33 @@ -def patch(build,data,vmod,verbose=False): - # We need to import the export module :-) - import wgexport as wg +# --- We may need to import the export module --- +# +# from wgexport import * - # Get the game and last free GPID - game = wg.get_game(build) - gpid = int(game.getAttribute('nextPieceSlotId')) +def patch(build,data,vmod,verbose=False,**kwargs): + # --- Get the game --- + game = build.getGame() - # Get all boards - maps = wg.get_maps(build) - board = maps['Board'] + # --- Get the maps --- + maps = game.getMaps() - mkeys = wg.get_masskey(board,'name') - wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png') - wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png') + # --- Get the main board --- + board = maps['Board'] + + # --- Get the mass keys --- + mkeys = board.getMassKeys() + mkeys['Eliminate']['icon'] = 'eliminate-icon.png' + mkeys['Flip'] ['icon'] = 'flip-icon.png' - # Get main map + # --- Get the dead-pool map --- pool = maps['DeadMap'] - wg.set_node_attr(pool,icon='pool-icon.png') + pool['icon'] = 'pool-icon.png' + pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png' - mkeys = wg.get_masskey(pool,'name') - wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png') - - # Get the OOB map - oob = wg.get_chartwindows(game)['OOBs'] - wg.set_node_attr(oob,icon='oob-icon.png') - - + # --- Get the OOB map --- + oob = game.getChartWindows()['OOBs'] + oob['icon'] = 'oob-icon.png' + + +# +# EOF +# - - diff --git a/macros/latex/contrib/wargame/doc/wargame.pdf b/macros/latex/contrib/wargame/doc/wargame.pdf index c25e83c175..01a58c597b 100644 Binary files a/macros/latex/contrib/wargame/doc/wargame.pdf and b/macros/latex/contrib/wargame/doc/wargame.pdf differ diff --git a/macros/latex/contrib/wargame/source/Makefile b/macros/latex/contrib/wargame/source/Makefile index faca441091..e11c477bf7 100644 --- a/macros/latex/contrib/wargame/source/Makefile +++ b/macros/latex/contrib/wargame/source/Makefile @@ -2,7 +2,7 @@ # # NAME := wargame -VERSION := 0.3 +VERSION := 0.5 LATEX_FLAGS := -interaction=nonstopmode \ -file-line-error \ --synctex=15 \ @@ -16,8 +16,21 @@ SOURCES := wargame.ins \ wargame.dtx \ package.dtx \ util/core.dtx \ + util/misc.dtx \ + util/compound.dtx \ + util/bb.dtx \ + util/tikz.dtx \ + util/randomid.dtx \ + util/icons.dtx \ + util/export.dtx \ chit/shape.dtx \ chit/misc.dtx \ + chit/modifiers.dtx \ + chit/stack.dtx \ + chit/oob.dtx \ + chit/table.dtx \ + chit/battle.dtx \ + chit/dice.dtx \ chit/elements.dtx \ chit/core.dtx \ hex/shape.dtx \ @@ -42,6 +55,7 @@ SOURCES := wargame.ins \ hex/terrain/rough.dtx \ hex/board.dtx \ hex/coord.dtx \ + hex/split.dtx \ natoapp6c/shape.dtx \ natoapp6c/symbols.dtx \ natoapp6c/list.dtx \ @@ -105,15 +119,6 @@ PKG_FILES := wargame.sty \ wgexport.cls \ $(TILES_PDF) -# beach.png \ -# city.png \ -# mountains.png \ -# sea.png \ -# woods.png \ -# light_woods.png \ -# rough.png \ -# swamp.png \ -# town.png DOC_FILES := wargame.pdf symbols.pdf compat.pdf ifdef VERBOSE @@ -130,6 +135,10 @@ else ifdef := $(CI_JOB_ID) endif +%.pdf:%.tex + @echo "LATEX $< -> $@" + $(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR) + %.aux:%.tex @echo "LATEX $< -> $@" $(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR) @@ -152,10 +161,6 @@ endif @echo "LATEX $*.tex -> $@ (via $<)" $(MUTE)$(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR) -%.pdf:%.tex - @echo "LATEX $< -> $@" - $(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR) - %.ind:%.idx @echo "INDEX $< -> $@ $<" @@ -174,8 +179,9 @@ package: $(PKG_FILES) everything: all symbols.pdf compat.pdf tests/test.pdf tutorial: tutorial/game.pdf -tutorial/game.pdf: all - $(MUTE)$(MAKE) -C tutorial Game.vmod $(REDIR) +tutorial/game.pdf: all + @echo "MAKE -C tutorial Game.vmod" + $(MUTE)$(MAKE) -C tutorial Game.vmod clean: @echo "CLEAN" @@ -198,6 +204,7 @@ clean: $(MUTE)rm -rf __pycache__ $(MUTE)rm -rf $(ctandir) tmp $(MUTE)rm -f ctan.tex $(NAME).ctan.tar.gz + $(MUTE)$(MAKE) -C tutorial clean @@ -220,7 +227,9 @@ wargame.idx: wargame.dtx wargame.sty $(TILES_PDF) wargame.pdf: wargame.aux wargame.ind fast: wargame.idx symbols.aux: symbols.tex wargame.sty +symbols.pdf: symbols.aux compat.aux: compat.tex $(TABLES:%=cmp_%.tex) wargame.sty +compat.pdf: compat.aux test.aux: test.tex wargame.sty beach.pdf: beach.tex @@ -269,11 +278,11 @@ bindist:install logo.png:logo.pdf - pdftocairo -png $< + pdftocairo -transp -png $< mv logo-1.png logo.png README.md.version:README.md - @echo "Versioned README.md" + @echo "Versioned README.md $(VERSION)" $(MUTE)$(SED) 's/^# \(.*\)/# \1\n## Version $(VERSION)/' \ < $< | pandoc --to markdown > $@ @@ -289,6 +298,7 @@ ctandir: all tutorial/game.pdf README.md.version $(MUTE)mkdir -p $(ctandir)/$(NAME) $(MUTE)cp tmp/$(docdir)/README.md $(ctandir)/$(NAME)/ $(MUTE)(cd tmp && zip -q -r ../$(ctandir)/$(NAME).tds.zip *) + $(MUTE)rm tmp/$(docdir)/README.md $(MUTE)(cd tmp && cp -a $(docdir) ../$(ctandir)/$(NAME)/doc) $(MUTE)(cd tmp && cp -a $(srcdir) ../$(ctandir)/$(NAME)/source) $(MUTE) rm -rf tmp diff --git a/macros/latex/contrib/wargame/source/chit/battle.dtx b/macros/latex/contrib/wargame/source/chit/battle.dtx new file mode 100644 index 0000000000..5057d620a5 --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/battle.dtx @@ -0,0 +1,77 @@ +% \iffalse +% <*chit> +% -------------------------------------------------------------------- +% \fi +% \subsubsection{Battle markers} +% +% Takes 1 arguments - the identifier. +% +% Define \spec{every battle marker} to change the style. +% +% \begin{macrocode} +\tikzset{% + battle marker/.pic={ + \node[shape=circle, + font=\sffamily\bfseries, + inner sep=0pt, + minimum size=5mm, + draw=black, + fill=yellow!85!black, + every battle marker/.try] at (-.3,.3) {% + \ifnum#1>0\relax #1\fi% + }; + }, + battle marker/.style={ + chit={full={battle marker=#1},frame={draw=none}}}, +} +% \end{macrocode} +% +% Takes two arguments - the odds and the fill colour. The latter is +% useful to differentiate the severity of an attack. +% +% Define \spec{every odds marker} to change the style. +% +% \begin{macrocode} +\tikzset{% + pics/odds marker/.style args={#1,#2}{ + code={ + \node[shape=circle, + font=\sffamily\bfseries\large, + inner sep=0pt, + minimum size=8mm, + draw=black, + fill=#2, + every odds marker/.try] at (.2,-.2) {#1}; + } + }, + odds marker/.style args={#1,#2}{ + chit={full={odds marker={#1,#2}},frame={draw=none}}}, +} +% \end{macrocode} +% +% Takes two arguments - the result and the fill colour. The latter is +% useful to differentiate the severity of an attack. +% +% Define \spec{every result marker} to change the style. +% +% \begin{macrocode} +\tikzset{ + pics/result marker/.style args={#1,#2}{ + code={ + \message{^^JResults marker #1 (#2)} + \node[shape=circle, + font=\sffamily\bfseries\large, + inner sep=0pt, + minimum size=8mm, + draw=black, + fill=#2, + every result marker/.try] at (0,0) {#1};}}, + result marker/.style args={#1,#2}{ + chit={full={result marker={#1,#2}},frame={draw=none}}} +} +% \end{macrocode} +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/chit/core.dtx b/macros/latex/contrib/wargame/source/chit/core.dtx index 7a0189074e..9181e6ede7 100644 --- a/macros/latex/contrib/wargame/source/chit/core.dtx +++ b/macros/latex/contrib/wargame/source/chit/core.dtx @@ -39,8 +39,14 @@ % % \input{chit/shape.dtx} % \input{chit/elements.dtx} +% \input{chit/modifiers.dtx} +% \input{chit/stack.dtx} +% \input{chit/oob.dtx} +% \input{chit/table.dtx} +% \input{chit/battle.dtx} +% \input{chit/dice.dtx} % \input{chit/misc.dtx} -% +% % \iffalse % % -------------------------------------------------------------------- diff --git a/macros/latex/contrib/wargame/source/chit/dice.dtx b/macros/latex/contrib/wargame/source/chit/dice.dtx new file mode 100644 index 0000000000..0c07c91d4f --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/dice.dtx @@ -0,0 +1,286 @@ +% \iffalse +% <*chit> +% -------------------------------------------------------------------- +% \fi +% \subsubsection{Dice} +% +% First, a regular 6-sided dice with configurable number of dots. Use +% like +% +% \begin{Syntax} +% \cs{pic}\oarg{pic options}\{dice=\meta{eyes}\} +% \end{Syntax} +% +% For example: +% +% \begin{enumerate} +% \item \tikz{\pic[transform shape]{dice=1};} +% \item \tikz{\pic[transform shape]{dice=2};} +% \item \tikz{\pic[transform shape]{dice=3};} +% \item \tikz{\pic[transform shape]{dice=4};} +% \item \tikz{\pic[transform shape]{dice=5};} +% \item \tikz{\pic[transform shape]{dice=6};} +% \end{enumerate} +% +% \begin{macrocode} +\tikzset{ + dice bg/.style={ + % /utils/exec={ + % \pgfgettransformentries{% + % \wg@jaca}{% + % \wg@jacb}{% + % \wg@jacc}{% + % \wg@jacd}{% + % \wg@tmp}{% + % \wg@tmp}% + % \pgfmathsetmacro{\wg@tmp}{% + % sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))} + % \xdef\wg@tmp{\wg@tmp}},% + fill=black, + draw=none, + minimum width=1cm, + minimum height=1cm, + scale rounded corners, + rounded corners=.1cm, + inner sep=0pt, + transform shape}, + dice fg/.style={ + fill=white, + shape=circle, + inner sep=0pt, + minimum size=.2cm, + transform shape}, + pics/dice/.style={ + code={ + \node[dice bg] (dice bg) {}; + \ifodd#1\node[dice fg] at (dice bg) {};\fi + \ifnum#1>1% + \node[dice fg] at ($(dice bg)+(-45:.4)$){};% + \node[dice fg] at ($(dice bg)+(135:.4)$){};% + \fi% + \ifnum#1>3% + \node[dice fg] at ($(dice bg)+( 45:.4)$){};% + \node[dice fg] at ($(dice bg)+(-135:.4)$){};% + \fi% + \ifnum#1=6% + \node[dice fg] at ($(dice bg)+(-.282,0)$){}; + \node[dice fg] at ($(dice bg)+( .282,0)$){}; + \fi + } + }, + pics/dice/.default=3 +} +\newcommand\dicemark[2][scale=.5]{% + \tikz[baseline={($(dice bg.south east)!.25!(dice bg.north east)$)},#1]{ + \pic[transform shape]{dice=3};}} +% \end{macrocode} +% +% Now some shapes of different dice. This was originally done by +% \href{https://tex.stackexchange.com/questions/144193/how-do-i-put-a-die-roll-at-the-top-of-each-page#answer-144474}{David +% Carlisle}. Usage is for example +% +% \begin{Syntax} +% \cs{node}[shape=\meta{dice},\meta{node options}] \marg{value}; +% \end{Syntax} +% +% where \meta{dice} is one of \texttt{d4}, \texttt{d6}, \texttt{d8}, +% \texttt{d10}, \texttt{d12}, or \texttt{d20}. +% +% \def\DiceExample#1{% +% \begin{tikzpicture}[ +% die/.style={ +% shape=#1, +% scale line widths, +% line width=1pt, +% transform shape, +% scale=.7 +% }, +% fg die/.style={ +% die, +% fill=##1, +% draw=gray!25!white, +% text=white}, +% die shadow/.style={ +% die, +% fill=black, +% draw=none, +% opacity=.5}] +% \node[die shadow,rotate=10] at (.20,.07){}; +% \node[fg die=red,rotate=10] at (.15,.10){}; +% \node[die shadow,rotate=-10] at (-.08,-.11){}; +% \node[fg die=blue,rotate=-10] at (-.15,-.1){}; +% \draw(-.5,-.5)rectangle(.6,.5); +% \end{tikzpicture} +% } +% +% \paragraph{Tetrahedron} +% +% \DiceExample{d4} +% +% \begin{macrocode} +\pgfdeclareshape{d4}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ + % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathmoveto{\pgfpoint{0cm}{.4cm}} + \pgfpathlineto{\pgfpoint{.433cm}{-.35cm}} + \pgfpathlineto{\pgfpoint{-.433cm}{-.35cm}} + \pgfpathlineto{\pgfpoint{0cm}{.4cm}} + % \pgfusepath{draw} %draw border + % \pgfusepath{draw} %draw rectangle + }} +% \end{macrocode} +% +% \paragraph{Cubic} +% +% \DiceExample{d6} +% +% \begin{macrocode} +\pgfdeclareshape{d6}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ + % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathrectanglecorners{\pgfpoint{.4cm}{.4cm}}{\pgfpoint{-.4cm}{-.4cm}} + % \pgfusepath{draw} %draw rectangle + }} +% \end{macrocode} +% +% \paragraph{Octahedron} +% +% \DiceExample{d8} +% +% \begin{macrocode} +\pgfdeclareshape{d8}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ + % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathmoveto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{.433cm}{.25cm}} + \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}} + \pgfpathlineto{\pgfpoint{-.433cm}{.25cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}} + \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + % \pgfusepath{draw} %draw interiaor + }} +% \end{macrocode} +% +% \paragraph{Decahedron} +% +% \DiceExample{d10} +% +% \begin{macrocode} +\pgfdeclareshape{d10}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ + % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathmoveto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{.294cm}{-.154cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.3cm}} + \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{.475cm}{.1cm}} + \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{-.475cm}{-.1cm}} + \pgfpathlineto{\pgfpoint{-.475cm}{.1cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + \pgfpathmoveto{\pgfpoint{.294cm}{-.154cm}} + \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}} + \pgfpathmoveto{\pgfpoint{-.475cm}{-.1cm}} + \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}} + \pgfpathmoveto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.3cm}} + % \pgfusepath{draw} %draw interiaor + }} +% \end{macrocode} +% +% \paragraph{Dodecahedron} +% +% \DiceExample{d12} +% +% \begin{macrocode} +\pgfdeclareshape{d12}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathmoveto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{0.294cm}{.405cm}} + \pgfpathlineto{\pgfpoint{.475cm}{.173cm}} + \pgfpathlineto{\pgfpoint{.475cm}{-.173cm}} + \pgfpathlineto{\pgfpoint{.294cm}{-.405cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{-.294cm}{-.405cm}} + \pgfpathlineto{\pgfpoint{-.475cm}{-.173cm}} + \pgfpathlineto{\pgfpoint{-.475cm}{.173cm}} + \pgfpathlineto{\pgfpoint{-.294cm}{.405cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{0cm}{.349cm}} + \pgfpathlineto{\pgfpoint{.332cm}{.108cm}} + \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}} + \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}} + \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}} + \pgfpathlineto{\pgfpoint{0cm}{.349cm}} + \pgfpathmoveto{\pgfpoint{.475cm}{.173cm}} + \pgfpathlineto{\pgfpoint{.332cm}{.108cm}} + \pgfpathmoveto{\pgfpoint{.294cm}{-.405cm}} + \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}} + \pgfpathmoveto{\pgfpoint{-.294cm}{-.405cm}} + \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}} + \pgfpathmoveto{\pgfpoint{-.475cm}{.173cm}} + \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}} + % \pgfusepath{draw} %draw interiaor + }} +% \end{macrocode} +% +% \paragraph{Icosohedron} +% +% \DiceExample{d20} +% +% \begin{macrocode} +\pgfdeclareshape{d20}{ + \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center + \anchor{text}{ % this is used to center the text in the node + \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}} + \backgroundpath{ % draw border + \pgfpathmoveto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{.454cm}{.262cm}} + \pgfpathlineto{\pgfpoint{.454cm}{-.262cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{-.454cm}{-.262cm}} + \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}} + \pgfpathlineto{\pgfpoint{0cm}{.5cm}} + \pgfpathlineto{\pgfpoint{0cm}{.292cm}} + \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}} + \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}} + \pgfpathlineto{\pgfpoint{0cm}{.292cm}} + \pgfpathlineto{\pgfpoint{.454cm}{.262cm}} + \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}} + \pgfpathlineto{\pgfpoint{0cm}{-.5cm}} + \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}} + \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}} + \pgfpathlineto{\pgfpoint{0cm}{.292cm}} + \pgfpathmoveto{\pgfpoint{.454cm}{-.262cm}} + \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}} + \pgfpathmoveto{\pgfpoint{-.454cm}{-.262cm}} + \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}} + % \pgfusepath{draw} %draw interiaor + }} +% \end{macrocode} +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/chit/elements.dtx b/macros/latex/contrib/wargame/source/chit/elements.dtx index 8708ca2a15..6585c94064 100644 --- a/macros/latex/contrib/wargame/source/chit/elements.dtx +++ b/macros/latex/contrib/wargame/source/chit/elements.dtx @@ -5,6 +5,12 @@ % % \subsubsection{Predefined \texttt{chit} element pictures} % +% \begin{macrocode} +\DeclareRobustCommand\chit@sep[2][/]{% + \foreach[count=\is] \s in {#2}{% + \ifnum\is>1\relax#1\fi% + \s}} +% \end{macrocode} % \begin{TikzKey}{/tikz/pics/chit/1 factor, % /tikz/pics/chit/2 factors, % /tikz/pics/chit/2 factors artillery, @@ -28,24 +34,32 @@ pics/chit/2 factors artillery/.style args={#1,#2,#3}{% code={ \chit@dbg{4}{ Chit 2 factors w/artillery: `#1' `#2' `#3'}% - \node[chit/factor,chit/2 factors,pic actions]{% - #1$\overset{\text{\scriptsize #3}}{\text{--}}$#2};}}, + \node[chit/factor,chit/2 factors]{% + {#1}$\overset{\text{\scriptsize #3}}{\text{--}}${#2}};}}, pics/chit/3 factors/.style args={#1,#2,#3}{% code={ \chit@dbg{4}{ Chit 3 factors: `#1' `#2' `#3'}% - \node[chit/factor,chit/3 factors,pic actions]{#1-#2-#3};}}, + \node[chit/factor,chit/3 factors]{#1-#2-#3};}}, pics/chit/4 factors/.style args={#1,#2,#3,#4}{% code={ \chit@dbg{4}{ Chit 3 factors: `#1' `#2' `#3' `#4'}% - \node[chit/factor,chit/4 factors,pic actions]{#1-#2-#3-#4};}}, + \node[chit/factor,chit/4 factors]{#1-#2-#3-#4};}}, chit/identifier/.pic={ \chit@dbg{4}{ Chit identifier: `#1'}% \node[chit/identifier,pic actions]{#1}; }, + chit/identifiers/.pic={ + \chit@dbg{4}{ Chit identifiers: `#1'}% + \node[chit/identifier,pic actions]{\chit@sep{#1}}; + }, chit/small identifier/.pic={ \chit@dbg{4}{ Chit small identifier: `#1'}% \node[chit/small identifier,pic actions]{#1}; }, + chit/small identifiers/.pic={ + \chit@dbg{4}{ Chit small identifiers: `#1'}% + \node[chit/small identifier,pic actions]{\chit@sep{#1}}; + }, chit/identifier macro/.pic={% \chit@dbg{4}{ Chit identifier macro: \meaning#1} \edef\chit@i@tmp{#1} @@ -69,32 +83,35 @@ \tikzset{% chit/factor/.style={ shape=rectangle, - font=\sffamily\bfseries\large, + font=\sffamily\bfseries\fontsize{12}{14}\selectfont, anchor=base, inner sep=0, %text=pgfstrokecolor, draw=none, fill=none, + transform shape, }, chit/1 factor/.style={}, chit/2 factors/.style={}, chit/3 factors/.style={}, - chit/4 factors/.style={text/.append style=\small}, + chit/4 factors/.style={text/.append style=\fontsize{10}{12}\selectfont}, chit/identifier/.style={ shape=rectangle, - font=\sffamily\bfseries\scriptsize, + font=\sffamily\bfseries\fontsize{8}{9}\selectfont, inner sep=0, % text=pgfstrokecolor, draw=none, fill=none, + transform shape, }, chit/small identifier/.style={ shape=rectangle, - font=\sffamily\bfseries\tiny, + font=\sffamily\bfseries\fontsize{6}{7}\selectfont, inner sep=0, % text=pgfstrokecolor, draw=none, fill=none, + transform shape, }, } % \end{macrocode} diff --git a/macros/latex/contrib/wargame/source/chit/misc.dtx b/macros/latex/contrib/wargame/source/chit/misc.dtx index de373864e6..a5b9a79b24 100644 --- a/macros/latex/contrib/wargame/source/chit/misc.dtx +++ b/macros/latex/contrib/wargame/source/chit/misc.dtx @@ -1,391 +1,5 @@ % \iffalse -% -------------------------------------------------------------------- -%<*chit> -% \fi -% \subsubsection{Other pictures} -% Pictures for frame, factors, left, right, and below. -% \begin{macrocode} -\tikzset{ - pics/chit/shade/.style={ - code={% - \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}}, - pics/chit/eliminate/.style={ - code={% - \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}}, - pics/chit/shade/.default=0.5, - pics/chit/eliminate/.default=0.25, -} -\def\shadechit(#1){% - \pic at (#1) {chit/shade};} -\def\eliminatechit(#1){% - \pic at (#1) {chit/eliminate};} -% \end{macrocode} -% -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% \subsubsection{Stacking of chits} -% -% Stacking of chits. The key \spec{chit/stack direction} sets the -% default direction to make the stack in. -% -% \begin{macrocode} -% offset, location, direction, list -\tikzset{% - chit/stack direction/.store in=\chit@stack@dir, - chit/stack direction/.initial={(.3,.3)}, -} -% \end{macrocode} -% -% Now the code -% -% \begin{macrocode} -\def\chit@stack@dir{(.3,.3)} -\def\stackchits(#1){% - \@ifnextchar({\st@ckchits{#1}}{\st@ckchits{#1}(.3,.3)}%) -} -\def\st@ckchits#1(#2)#3{ - \chit@dbg{2}{Stacking chits `#1', `#2', `#3'} - \edef\xy{#1} - \chit@dbg{4}{Stack start at \xy} - \foreach[count=\i from 0] \c/\o in {#3} {% - \ifx\c\empty\else% - \edef\ccc{\c} - \chit@dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'} - \expandafter\ccc(\xy) - %% - \ifx\c\o\else - %\chit@dbg{0}{Option: \o} - \edef\ccc{\o} - \expandafter\ccc(\xy) - \fi - \expandafter\ccc(\xy) - \tikzmath{% - coordinate \cc; - \cc = (\xy) + (#2);} - \xdef\xy{\cc} - \fi - } -} -% \end{macrocode} -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% \subsubsection{Making order of battle charts} -% -% -% Macros for making OOBs -% -% Style for turns -% -% \begin{macrocode} -\tikzset{ - chit/oob turn/.pic={\node[pic actions]{#1};}} -% \end{macrocode} -% -% -% current c, current r, n-columns, cell size, y -% -% \begin{macrocode} -\def\chit@oob@cellupdate(#1,#2)#3#4#5{% - \edef\f{\ifwg@oob@inv-1\else1\fi}% - \chit@dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)} - \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}% - \xdef#5{\pgfmathresult} - \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}% - \xdef#2{\pgfmathresult}% - \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}% - \xdef#1{\pgfmathresult}% - \chit@dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1} -} -% \end{macrocode} -% -% current c, current r, cell size, extra vertical spacing -% -% \begin{macrocode} -\def\chit@oob@rowupdate(#1,#2)#3#4{% - \chit@dbg{2}{ Row update c=`#1',r=`#2',s=`#3',e=`#4'} - %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}% - \pgfmathparse{#2-#3)}% - \xdef#2{\pgfmathresult}% - %\xdef#1{0} - \chit@dbg{2}{ \space\space-> update `\string#2'=#2} -} -% \end{macrocode} -% current c, current r, cell size, extra spacing -% -% \begin{macrocode} -\def\chit@oob@turnupdate(#1,#2)#3#4{% - \chit@dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'} - % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}% - \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)} - \xdef#2{\pgfmathresult}% - \xdef#1{0}% - \chit@dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2} -} -% \end{macrocode} -% chit list, n-colls, cell size, extra vertical spacing -% -% This expects a list of lists of chits, one list per turn; the -% maximum number of columns; the size of cells, extra spacing between -% turns. -% -% Note, the list of lists leaf elements should be styles for the -% chits. -% -% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the -% number as argument. -% -% -% \begin{macrocode} -\newif\ifwg@oob@inv\wg@oob@invfalse -\def\wg@star@oob{\wg@oob@invtrue\wg@oob} -\def\wg@nostar@oob{\wg@oob@invfalse\wg@oob} -\def\oob{% - \@ifstar{\wg@star@oob% - }{\wg@nostar@oob% - }% -} -\def\wg@oob#1#2#3#4{ - \def\r{0} - \chit@dbg{1}{OOB: `#1'} - \foreach[count=\ti from 0] \t/\y in #1{ - \xdef\o{\r} - \def\c{0} - \ifx\t\y\def\y{0}\fi - \chit@dbg{1}{Turn \ti\space(\r,\t,y=\y):'} - \ifwg@oob@inv% - \pic at ( .5,\r) {chit/oob turn=\ti};% - \else - \pic at (-.5,\r) {chit/oob turn=\ti};% - \fi% - \ifx\t\empty\else% - \foreach \u/\m in \t{ - %% \chit@dbg{2}{ `\u'=`\m'} - \ifx\u\empty\else - \ifx\m\@empty\def\m{1}\fi - \ifx\u\m\def\m{1}\fi - \foreach \n in {1,...,\m}{% - \ifx\u\chit@blank\else - \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r); - \fi - \chit@oob@cellupdate(\c,\r){#2}{#3}{\y} - } - \fi - } - \fi - \chit@dbg{1}{ End of chits in turn - \ti\space(c=`\c',r=`\r',o='\o',y='\y')} - % IF no units where given, then we force \c to be non-zero so that - % \chit@oob@turnupdate increments the row - \ifx\t\@empty - \def\c{#3} - \chit@dbg{2}{ Turn is empty, set c=`\c'} - \fi - %\ifnum\y<0% No explicit number of rows given - % \def\c{#3} - % \chit@dbg{2}{ No explicit number of rows given, set c=`\c'} - %\fi - % In case the user gave and explicit number of rows, add the rows - % that are missing. \y is initially set to the number of - % requested rows, and then decremented every time we go down one - % row. So if the number of rows we did so far is N, and the - % requested number of rows is M, then the loop below adds M-N - % rows. - \ifnum\y>0% - \chit@dbg{2}{ Looping rows from 2 to \y, break when row > \y}% - \foreach \rr in {2,...,\y}{ - %\ifnum\rr>\y% A little funny, but \y can be negative! - % \chit@dbg{2}{ \space Breaking loop \rr\space > \y}% - % \breakforeach% - %\else% - \chit@oob@rowupdate(\c,\r){#3}{#4} - %\fi - } - \fi - % This will zero \c. However, if on entry |\c|>0, then we also - % increment the row - \chit@oob@turnupdate(\c,\r){#3}{#4} - \chit@dbg{1}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')} - } - \chit@dbg{2}{End of OOB (c=`\c',r=`\r',y=`\y')} -} -% \end{macrocode} -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% \subsubsection{Table of chits} -% -% \begin{macrocode} -\tikzset{ - chit/cell background/.style={fill=black}, - blank chit/.style={/chit/frame={draw=none,fill=none}}, -} -% \end{macrocode} -% -% These macros are used when we set tables of chits. This allows us -% to define blank spaces in the table by giving the element -% \texttt{blank~chit}. -% -% \begin{macrocode} -\def\chit@blank{blank chit} -\def\chit@cellbg(#1,#2)#3{% - \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3); -} -% \end{macrocode} -% -% \begin{Macro}{\ifchits@reset} -% -% This `if' controls whether to reset the coordinates to the origin -% when \cs{chits} is called. If true, then reset for a new table. -% -% \begin{macrocode} -\newif\ifchits@reset\chits@resettrue -% \end{macrocode} -% \end{Macro} -% -% \begin{Macro}{\chits,\@chits,\chit@sng@cellupdate} -% \begin{macrocode} -\def\chit@sng@cellupdate(#1,#2)#3#4{% - \chit@dbg{2}{Current `#1' vs `#4'*(`#3'+1)} - \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}% - \xdef#2{\pgfmathresult}% - \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}% - \xdef#1{\pgfmathresult}% -} -% \end{macrocode} -% -% The stared version (\cs{chits*}) of this macro continues the -% previously set chit table. -% -% \begin{macrocode} -\def\chits{% - \@ifstar{\chits@resetfalse\@chits}{\chits@resettrue\@chits}} -% \end{macrocode} -% -% \begin{macrocode} -\def\@chits#1#2#3{ - \ifchits@reset - \def\r{0}% - \def\c{0}% - \fi - \chit@dbg{1}{Chits to make: #1}% - \foreach[count=\ti from 0] \t/\x in #1{% - \chit@dbg{2}{Turn `\t' with option `\x'} - \ifx\t\empty\else% - \foreach \u/\m in \t{% - \ifx\u\empty\else% - \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% - \ifx\m\@empty\def\m{1}\fi% - \ifx\u\m\def\m{1}\fi% - \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% - \foreach \n in {1,...,\m}{% - \ifx\u\chit@blank% - \chit@dbg{3}{Ignoring blank chit:\u}% - \else% - \chit@cellbg(\c,\r){#3}% - \chit[\u=\ti](\c,\r)% - \chit@sng@cellupdate(\c,\r){#2}{#3}% - \fi% - }% - \fi% - }% - \fi% - }% -} -% \end{macrocode} -% \end{Macro} -% -% \begin{Macro}{\doublechits, -% \@doublechits, -% \chit@dbl@cellupdate, -% \chit@dbl@flip} -% \begin{enumerate} -% \item coordinates -% \item coordinates -% \item cell-size -% \end{enumerate} -% -% \begin{macrocode} -\def\chit@dbl@flip(#1,#2)#3{% - \pgfmathparse{-#1}% - \xdef\mc{\pgfmathresult}% -} -% \end{macrocode} -% -% \begin{enumerate} -% \item coordinates -% \item coordinates -% \item Number of columns -% \item cell-size -% \end{enumerate} -% -% \begin{macrocode} -\def\chit@dbl@cellupdate(#1,#2)#3#4{% - \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}% - \xdef#2{\pgfmathresult}% - \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}% - \xdef#1{\pgfmathresult}% -} -% \end{macrocode} -% -% \begin{enumerate} -% \item List of list of keys -% \item Number of columns -% \item size of each cell -% \end{enumerate} -% -% The stared version (\cs{doublechits*}) of this macro continues the -% previously set chit table. -% -% \begin{macrocode} -\def\doublechits{% - \@ifstar{\chits@resetfalse\@doublechits}{\chits@resettrue\@doublechits}} -% \end{macrocode} -% -% \begin{macrocode} -\def\@doublechits#1#2#3{% - \chit@dbg{1}{Setting double-sided chits: #1} - \ifchits@reset - \pgfmathparse{-(#2-.5)*#3} - \xdef\c{\pgfmathresult} - \def\r{0} - \fi - - \foreach[count=\ti from 0] \t/\x in #1{ - \ifx\t\empty\else% - \foreach \u/\m in \t{ - \ifx\u\empty\else - \ifx\m\@empty\def\m{1}\else% - \ifx\u\m\def\m{1}\fi\fi - \chit@dbg{2}{`\u'=`\m' (\c,\r)} - \foreach \n in {1,...,\m}{% - \ifx\u\chit@blank - \chit@dbg{3}{Ignoring blank chit:\u} - \else - \chit@cellbg(\c,\r){#3} - \chit[\u=\ti](\c,\r) - \chit@dbl@flip(\c,\r){#3} - \chit@cellbg(\mc,\r){#3} - \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r) - \chit@dbl@cellupdate(\c,\r){#2}{#3} - \fi - } - \fi - } - \fi - } - \draw[dashed](0,-3*#3/4)--(0,\r-#3/4); - \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[anchor=west]{Back}; - \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[anchor=east]{Front}; -} -% \end{macrocode} -% \end{Macro} -% -% \iffalse +% <*chit> % -------------------------------------------------------------------- % \fi % \subsubsection{Some utilities} @@ -395,17 +9,32 @@ % % \begin{macrocode} \tikzset{ - number chit/.pic={ - \node[shape=rectangle,font=\sffamily\bfseries\LARGE]{% - \begin{tabular}{c} #1\end{tabular}};}, - game turn/.pic={ - \node[shape=rectangle,font=\sffamily\bfseries]{% - \begin{tabular}{c} Game\\Turn\end{tabular}};}, - game turn/.style={ - /chit/full={game turn}, + chit/text base/.style={ + shape=rectangle, + inner sep=0pt, + align=center, + text width=1.1cm}, + chit/number/.style={ + chit/text base, + font=\sffamily\bfseries\fontsize{12}{14}\selectfont}, + chit/game turn/.style={ + chit/text base, + font=\sffamily\bfseries}, + chit/text/.style={ + chit/text base, + font=\sffamily\bfseries}, + chit/small text/.style={ + chit/text base, + font=\sffamily\bfseries\fontsize{9}{10}\selectfont}, + chit/number/.pic={\node[chit/number]{#1};}, + chit/game turn/.pic={\node[chit/game turn]{Game\\Turn};}, + chit/text/.pic={\node[chit/text]{#1};}, + chit/small text/.pic={\node[chit/small text]{#1};}, + game turn chit/.style={ + /chit/full={chit/game turn}, color=black, fill=white}, - game turn flipped/.style={game turn}, + game turn chit flipped/.style={game turn chit}, dummy chit/.style={fill=white}, } % \end{macrocode} @@ -435,7 +64,7 @@ % % \begin{macrocode} \DeclareRobustCommand\zocmark[1][]{% - \tikz[baseline=(current bounding box.center),scale=.1,#1]{% + \tikz[baseline=($(current bounding box.center)!.5!(current bounding box.south)$),scale=.1,#1]{% \begin{scope}[hex/first row and column are=0, hex/row direction is=normal, hex/column direction is=normal, @@ -450,6 +79,10 @@ \end{scope}}} % \end{macrocode} % +% Dummy implementations of zones hooks when exporting. Here, these do +% nothing, but in the \textsf{wgexport} class these are +% re-implemented. +% % \begin{macrocode} \tikzset{ zone point/.code n args={3}{}, @@ -457,5 +90,5 @@ % \end{macrocode} % % \iffalse -% +% % \fi diff --git a/macros/latex/contrib/wargame/source/chit/modifiers.dtx b/macros/latex/contrib/wargame/source/chit/modifiers.dtx new file mode 100644 index 0000000000..b42ee5790f --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/modifiers.dtx @@ -0,0 +1,42 @@ +% \iffalse +% -------------------------------------------------------------------- +% <*chit> +% \fi +% \subsubsection{Modifications to chits} +% +% These defines overlays one can add on top of chits, for example to +% shade a chit, put a semi-transparent red cover to indicate +% elimination, and similar. +% +% \begin{macrocode} +\tikzset{ + pics/chit/shade/.style={ + code={% + \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}}, + pics/chit/eliminate/.style={ + code={% + \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}}, + pics/chit/shade/.default=0.5, + pics/chit/eliminate/.default=0.25, + dummy chit/.style={draw=none,fill=none,chit={}}, +} +\def\shadechit{% + \@ifnextchar[{\sh@dechit}{\sh@dechit[.5]}%] +} +\def\eliminatechit{% + \@ifnextchar[{\elimin@techit}{\elimin@techit[.25]}%] +} +\def\sh@dechit[#1](#2){% + % \message{^^JShading chit with opacity `#1'}% + \pic[transform shape] at (#2) {chit/shade=#1};% + \@ifnextchar;{\@gobble}{}} +\def\elimin@techit[#1](#2){% + \pic[transform shape] at (#2) {chit/eliminate=#1};% + \@ifnextchar;{\@gobble}{}} +% \end{macrocode} +% +% +% \iffalse +% -------------------------------------------------------------------- +% +% \fi diff --git a/macros/latex/contrib/wargame/source/chit/oob.dtx b/macros/latex/contrib/wargame/source/chit/oob.dtx new file mode 100644 index 0000000000..d640dadebf --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/oob.dtx @@ -0,0 +1,172 @@ +% \iffalse +% <*chit> +% -------------------------------------------------------------------- +% \fi +% \subsubsection{Making order of battle charts} +% +% +% Macros for making OOBs +% +% Style for turns +% +% \begin{macrocode} +\tikzset{ + chit/oob turn/.pic={\node[pic actions]{#1};}} +% \end{macrocode} +% +% +% current c, current r, n-columns, cell size, y +% +% \begin{macrocode} +\def\chit@oob@cellupdate(#1,#2)#3#4#5{% + \edef\f{\ifwg@oob@inv-1\else1\fi}% + \chit@dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)} + \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}% + \xdef#5{\pgfmathresult} + \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}% + \xdef#2{\pgfmathresult}% + \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}% + \xdef#1{\pgfmathresult}% + \chit@dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1} +} +% \end{macrocode} +% +% current c, current r, cell size, extra vertical spacing +% +% \begin{macrocode} +\def\chit@oob@rowupdate(#1,#2)#3#4{% + \chit@dbg{2}{ Row update c=`#1',r=`#2',s=`#3',e=`#4'} + %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}% + \pgfmathparse{#2-#3)}% + \xdef#2{\pgfmathresult}% + %\xdef#1{0} + \chit@dbg{2}{ \space\space-> update `\string#2'=#2} +} +% \end{macrocode} +% current c, current r, cell size, extra spacing +% +% \begin{macrocode} +\def\chit@oob@turnupdate(#1,#2)#3#4{% + \chit@dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'} + % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}% + \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)} + \xdef#2{\pgfmathresult}% + \xdef#1{0}% + \chit@dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2} +} +% \end{macrocode} +% chit list, n-colls, cell size, extra vertical spacing +% +% This expects a list of lists of chits, one list per turn; the +% maximum number of columns; the size of cells, extra spacing between +% turns. +% +% Note, the list of lists leaf elements should be styles for the +% chits. +% +% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the +% number as argument. +% +% +% \begin{macrocode} +\newif\ifwg@oob@inv\wg@oob@invfalse +\def\chit@oob@spacer{hspace} +\def\wg@star@oob{\wg@oob@invtrue\wg@oob} +\def\wg@nostar@oob{\wg@oob@invfalse\wg@oob} +\def\oob{% + \@ifstar{\wg@star@oob% + }{\wg@nostar@oob% + }% +} +% \end{macrocode} +% +% The inner macro of \cs{oob}. The arguments are +% \begin{enumerate} +% \item The list of lists of chits styles +% \item The maximum number of columns +% \item The width of each cell +% \item Additional row spacing between turns +% \end{enumerate} +% +% \begin{macrocode} +\def\wg@oob#1#2#3#4{ + \def\r{0} + \chit@dbg{2}{OOB: `#1'} + \foreach[count=\ti from 0] \t/\y in #1{ + \xdef\o{\r} + \def\c{0} + \ifx\t\y\def\y{0}\fi + \chit@dbg{2}{Turn \ti\space(\r,\t,y=\y):'} + \ifwg@oob@inv% + \pic[transform shape] at ( .5*#3,\r) {chit/oob turn=\ti};% was dx=0.5 + \else + \pic[transform shape] at (-.5*#3,\r) {chit/oob turn=\ti};% was dx=-0.5 + \fi% + \ifx\t\empty\else% + \foreach \u/\m in \t{ + %% \chit@dbg{2}{ `\u'=`\m'} + \ifx\u\empty\else + \ifx\m\@empty\def\m{1}\fi + \ifx\u\m\def\m{1}\fi + \foreach \n in {1,...,\m}{% + \chit@dbg{2}{OOB Chit is `\u'}% + \ifx\u\chit@oob@spacer% + \chit@dbg{3}{Chit `\u' is spacer `\chit@oob@spacer'} + \pgfmathparse{\c+#4}% + \xdef\c{\pgfmathresult}% + \else + \ifnum\chitdbglvl>2% + \node[minimum width=#3cm,minimum height=#3cm, + draw,transform shape] at (\c,\r) {}; + \fi + \ifx\u\chit@blank\else% + \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);% + \fi% + \chit@oob@cellupdate(\c,\r){#2}{#3}{\y} + \fi + } + \fi + } + \fi + \chit@dbg{1}{ End of chits in turn + \ti\space(c=`\c',r=`\r',o='\o',y='\y')} + % IF no units where given, then we force \c to be non-zero so that + % \chit@oob@turnupdate increments the row + \ifx\t\@empty + \def\c{#3} + \chit@dbg{2}{ Turn is empty, set c=`\c'} + \fi + %\ifnum\y<0% No explicit number of rows given + % \def\c{#3} + % \chit@dbg{2}{ No explicit number of rows given, set c=`\c'} + %\fi + % In case the user gave and explicit number of rows, add the rows + % that are missing. \y is initially set to the number of + % requested rows, and then decremented every time we go down one + % row. So if the number of rows we did so far is N, and the + % requested number of rows is M, then the loop below adds M-N + % rows. + \ifnum\y>0% + \chit@dbg{2}{ Looping rows from 2 to \y, break when row > \y}% + \foreach \rr in {2,...,\y}{ + %\ifnum\rr>\y% A little funny, but \y can be negative! + % \chit@dbg{2}{ \space Breaking loop \rr\space > \y}% + % \breakforeach% + %\else% + \chit@oob@rowupdate(\c,\r){#3}{#4} + %\fi + } + \fi + % This will zero \c. However, if on entry |\c|>0, then we also + % increment the row + \chit@oob@turnupdate(\c,\r){#3}{#4} + \chit@dbg{2}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')} + } + \chit@dbg{3}{End of OOB (c=`\c',r=`\r',y=`\y')} + \@ifnextchar;{\@gobble}{}} +% \end{macrocode} +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/chit/shape.dtx b/macros/latex/contrib/wargame/source/chit/shape.dtx index 8ad0614cb8..8a3d70fbd0 100644 --- a/macros/latex/contrib/wargame/source/chit/shape.dtx +++ b/macros/latex/contrib/wargame/source/chit/shape.dtx @@ -4,6 +4,19 @@ % \fi % % \subsubsection{The \texttt{chit} key namespace} +% +% Some stuff to consider wrt.~line widths. Setting the line width in +% the \texttt{chit} scope overrides frame settings. The frame stroke +% can be larger but not smaller. Setting the stroke width in the +% symbol scope sets it for the symbol only. Thus, to get a thin +% border, we need to +% \begin{itemize} +% \item Set a small line width in the top chit scope. +% \item Possible set a larger line width in the frame sub-scope. +% \item Set a larger line width in the symbol sub-scope. +% \end{itemize} +% I do not know why this is. +% % \begin{TikzKey}{/chit/full, % /chit/symbol, % /chit/left, @@ -13,12 +26,15 @@ % /chit/lower left, % /chit/lower right, % /chit/factors, +% /chit/extra, % /chit/setup, +% /chit/bevel, % /chit/id} % % The parts of a chit % % \begin{macrocode} +\newif\ifchit@clip\chit@cliptrue \tikzset{% /chit/.search also={/tikz}, /chit/.cd, @@ -36,6 +52,21 @@ setup/.store in=\chit@setup, setup/.initial=,% id/.store in=\chit@id, id/.initial=,% frame/.store in=\chit@frame, frame/.initial=,% + extra/.store in=\chit@extra, extra/.initial=,% + bev/.store in=\chit@bevel, bev/.initial=, + bevel fraction/.store in=\chit@bevel@frac,bevel fraction/.initial=10, + bevel/.is choice, + bevel/none/.style = {/chit/bev=}, + bevel/north west/.style = {/chit/bev=1}, + bevel/north east/.style = {/chit/bev=2}, + bevel/south west/.style = {/chit/bev=3}, + bevel/south east/.style = {/chit/bev=4}, + bevel/NW/.style = {/chit/bev=1}, + bevel/NE/.style = {/chit/bev=2}, + bevel/SW/.style = {/chit/bev=3}, + bevel/SE/.style = {/chit/bev=4}, + bevel/.default = north west, + clip/.is if=chit@clip% } % \end{macrocode} % \end{TikzKey} @@ -69,7 +100,28 @@ chit/lower right/.style={chit/parts,anchor=south east}, chit/setup/.style={chit/parts}, chit/full/.style={chit/parts}, + chit/frame/.try={draw=pgfstrokecolor}, + chit/bevel highlight/.style={fill=white,opacity=.25}, + chit/bevel shadow/.style={fill=black,opacity=.25}, } +\def\chit@bevel@frac{10} +\newif\ifchit@draw@frame\chit@draw@frametrue +\tikzset{ + chit/frame style/.search also={/tikz}, + chit/frame style/.cd, + none/.code={\chit@draw@framefalse}, + draw/.code={% + \chit@dbg{2}{Frame draw option `#1'} + \edef\tikz@temp{#1}% + \ifx\tikz@temp\tikz@nonetext% + \chit@draw@framefalse% + \else% + \chit@draw@frametrue% + \tikzset{/tikz/draw=#1} + \fi + } +} + % \end{macrocode} % \end{TikzKey} % @@ -116,7 +168,7 @@ % \begin{macrocode} \def\chit@n@to#1#2{% %% Without a following start square bracket '[' by-pass to final - \chit@dbg{4}{Chit NATO App6(c) first step `#1' `#2'} + \chit@dbg{1}{Chit NATO App6(c) first step `#1' `#2'} \@ifnextchar[{% %\message{^^JStart square bracket}% \@chit@n@to{#1}{#2}}{% @@ -129,7 +181,7 @@ % % \begin{macrocode} \def\@chit@n@to@#1#2#3\@end@chit@n@to{% - \chit@dbg{4}{Chit NATO App6(c) w/o offset: + \chit@dbg{1}{Chit NATO App6(c) w/o offset: ^^J Options: #3 ^^J ID: #1 ^^J Position: #2} @@ -143,7 +195,7 @@ % % \begin{macrocode} \def\@chit@n@to#1#2[#3]{% - \chit@dbg{4}{Chit NATO App6(c) second step `#1' `#2' `#3'} + \chit@dbg{1}{Chit NATO App6(c) second step `#1' `#2' `#3'} \@ifnextchar({\@@chit@n@to{#1}{#2}{#3}}{\@@chit@n@to{#1}{#2}{#3}(0,0)}%) } % \end{macrocode} @@ -152,7 +204,7 @@ % % \begin{macrocode} \def\@@chit@n@to#1#2#3(#4)\@end@chit@n@to{% - \chit@dbg{4}{Chit NATO App6(c) w/offset: + \chit@dbg{1}{Chit NATO App6(c) w/offset: ^^J Options: #3 ^^J ID: #1 ^^J Position: #2 @@ -186,8 +238,39 @@ % % Now follows the actual \text{chit} shape. This is rather long, so % we will break it up a bit -% -% +% \begin{macrocode} +\def\chit@bevel@path#1{ + \scope[#1] + \wg@tmpc=\wg@tmpa\multiply\wg@tmpc by \chit@bevel@frac + \wg@tmpd=\wg@tmpb\multiply\wg@tmpd by \chit@bevel@frac + \divide\wg@tmpc100 + \divide\wg@tmpd100 + \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Move down along edge + \wg@tmpb=-\wg@tmpb + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Move left along edge + \wg@tmpa=-\wg@tmpa + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Move in and up + \advance\wg@tmpa\wg@tmpc% + \advance\wg@tmpb\wg@tmpd% + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Move right, but in + \advance\wg@tmpa-\wg@tmpc\wg@tmpa=-\wg@tmpa% + \advance\wg@tmpa-\wg@tmpc% + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Move up but down + \advance\wg@tmpb-\wg@tmpd\wg@tmpb=-\wg@tmpb% + \advance\wg@tmpb-\wg@tmpd% + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \pgfclosepath% + \pgfusepath{fill} + \endscope +} + +% \end{macrocode} +% % The first thing is we declare some saved anchors. These are % computed (and defined as internal macros) when the shape is % instantised. The anchors give the centre and north east corner of @@ -224,7 +307,7 @@ \let\chitframeopt\pgfutil@empty% \@ifundefined{chit@frame}{}{% \edef\chitframeopt{\chit@frame}} - \n@to@pp@dbg{3}{Chit Frame options: \meaning\chitframeopt}% + \chit@dbg{3}{Chit Frame options: \meaning\chitframeopt}% } % \end{macrocode} % @@ -296,6 +379,7 @@ \backgroundpath{% %% This is the outline of the chit only. The rest of the chit is %% made on the foreground "path". + \chit@dbg{1}{Chit drawing background path} \northeast% \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% @@ -314,29 +398,38 @@ % % \begin{macrocode} \behindforegroundpath{% - \chit@dbg{4}{% - Chit foreground: \meaning\id - ^^J ID (set): \meaning\chit@id - ^^J Symbol: \meaning\chit@symbol - ^^J Full: \meaning\chit@full - ^^J Factors: \meaning\chit@factors - ^^J Left: \meaning\chit@left - ^^J Right: \meaning\chit@right - ^^J Upper left: \meaning\chit@upper@left - ^^J Lower left: \meaning\chit@lower@left - ^^J Upper right: \meaning\chit@upper@right - ^^J Lower right: \meaning\chit@lower@right} + \chit@dbg{1}{Chit drawing foreground path} + % \chit@dbg{4}{% + % Chit foreground: \meaning\id + % ^^J ID (set): \meaning\chit@id + % ^^J Symbol: \meaning\chit@symbol + % ^^J Full: \meaning\chit@full + % ^^J Factors: \meaning\chit@factors + % ^^J Left: \meaning\chit@left + % ^^J Right: \meaning\chit@right + % ^^J Upper left: \meaning\chit@upper@left + % ^^J Lower left: \meaning\chit@lower@left + % ^^J Upper right: \meaning\chit@upper@right + % ^^J Lower right: \meaning\chit@lower@right + % ^^J Extra: \meaning\chit@extra + % ^^J Bevel: \meaning\chit@bevel + % ^^J Frame: \meaning\chit@frame} + \chit@dbg{1}{Chit report} \chit@report{} + \chit@dbg{1}{Chit start scope} \pgfscope % - \northeast% - \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% - \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \pgfclosepath - \pgfusepath{clip} + \ifchit@clip% + \chit@dbg{1}{Chit clip path} + \northeast% + \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% + \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \pgfclosepath% + \pgfusepath{clip}% + \fi% % \end{macrocode} % % If we do not have the \texttt{symbol} key set, then we set the @@ -346,19 +439,22 @@ \@ifundefined{chit@symbol}{% %% Draw full stuff \@ifundefined{chit@full}{}{% + \chit@dbg{1}{Chit draw full image: `\meaning\chit@full'} \center\wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% \wg@pic@all{\chit@full}{}{\the\wg@tmpa,\the\wg@tmpb}{chit/full}}% - }{% + }{% With NATO symbol % \end{macrocode} % % Otherwise, we put in a node with shape \texttt{natoapp6c} and pass % the \texttt{symbol} key--value pairs as options. % % \begin{macrocode} + \chit@dbg{1}{Chit draw symbol image} \edef\symid{\id symbol}% \symbol% \edef\args{{\symid}{\the\pgf@x,\the\pgf@y}\chit@symbol}% \chit@dbg{6}{Arguments to chit NATO symbol: \meaning\args}% + \chit@dbg{1}{Chit draw nato image} \expandafter\chit@n@to\args\@end@chit@n@to% \chit@dbg{6}{After making NATO symbol in chit}% % \end{macrocode} @@ -378,6 +474,7 @@ % \begin{macrocode} % Put in left of symbol \@ifundefined{chit@left}{}{% + \chit@dbg{1}{Chit draw left: `\meaning\chit@left'} \begin{scope}[] \pgfpointanchor{\symid}{west}% \wg@tmpa=\pgf@x\advance\wg@tmpa-\margin% @@ -386,6 +483,7 @@ \end{scope}}% % Put in right of symbol \@ifundefined{chit@right}{}{% + \chit@dbg{1}{Chit draw left: `\meaning\chit@right'} \begin{scope}[] \pgfpointanchor{\symid}{east}% \wg@tmpa=\pgf@x\advance\wg@tmpa+\margin% @@ -416,24 +514,28 @@ % \begin{macrocode} % Put in upper left corner \@ifundefined{chit@upper@left}{}{% + \chit@dbg{1}{Chit draw upper left: `\meaning\chit@upper@left'} \begin{scope}[] \wg@pic@all{\chit@upper@left}{}{-\the\wg@tmpa,\the\wg@tmpb}{% chit/upper left}% \end{scope}} % Put in upper right corner \@ifundefined{chit@upper@right}{}{% + \chit@dbg{1}{Chit draw upper right: `\meaning\chit@upper@right'} \begin{scope}[] \wg@pic@all{\chit@upper@right}{}{\the\wg@tmpa,\the\wg@tmpb}{% chit/upper right}% \end{scope}} % Put in lower left corner \@ifundefined{chit@lower@left}{}{% + \chit@dbg{1}{Chit draw lower left: `\meaning\chit@lower@left'} \begin{scope}[] \wg@pic@all{\chit@lower@left}{}{-\the\wg@tmpa,-\the\wg@tmpb}{% chit/lower left}% \end{scope}} % Put in lower right corner \@ifundefined{chit@lower@right}{}{% + \chit@dbg{1}{Chit draw lower right: `\meaning\chit@lower@right'} \begin{scope}[] \wg@pic@all{\chit@lower@right}{}{\the\wg@tmpa,-\the\wg@tmpb}{% chit/lower right}% @@ -448,24 +550,62 @@ % \begin{macrocode} % Put in factors \@ifundefined{chit@factors}{}{% + \chit@dbg{1}{Chit draw factors: `\meaning\chit@factors'} \advance\wg@tmpb-\margin% \begin{scope}[] \wg@pic@all{\chit@factors}{}{0,-\the\wg@tmpb}{chit/factors}% \end{scope}}% - }% + % Put in extra + \@ifundefined{chit@extra}{}{% + \chit@dbg{1}{Chit draw extra: `\meaning\chit@extra'} + \begin{scope}[] + \wg@pic@all{\chit@extra}{}{0,0}{chit/factors}% + \end{scope}}% + }% End of full or symbol \endpgfscope% + % Make bevel? + \@ifundefined{chit@bevel}{\let\chit@bevel\empty}{} + \ifx\chit@bevel\empty\else% + \chit@dbg{1}{Chit draw bevel} + %% South east bevel + \northeast% + \wg@tmpa=-\pgf@x\wg@tmpb=-\pgf@y% + \ifcase\chit@bevel\relax% + \or% 1 + \or\wg@tmpa=-\wg@tmpa% 2 + \or\wg@tmpb=-\wg@tmpb% 3 + \or\wg@tmpa=-\wg@tmpa\wg@tmpb=-\wg@tmpb%4 + \fi + \chit@bevel@path{chit/bevel highlight} + %% North west bevel + \northeast% + \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% + \ifcase\chit@bevel\relax% + \or% 1 + \or\wg@tmpa=-\wg@tmpa% 2 + \or\wg@tmpb=-\wg@tmpb% 3 + \or\wg@tmpa=-\wg@tmpa\wg@tmpb=-\wg@tmpb%4 + \fi + \chit@bevel@path{chit/bevel shadow} + \fi % Draw frame? - \edef\tmp@opt{[\chitframeopt]} + \chit@dbg{1}{Chit draw frame: `\meaning\chitframeopt'} + \edef\tmp@opt{[chit/frame style/.cd,chit/frame/.try,\chitframeopt]} + \chit@dbg{1}{Chit draw frame: `\meaning\tmp@opt} \expandafter\scope\tmp@opt - \northeast% - \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% - \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% - \pgfclosepath - \pgfusepath{stroke} - \endscope + \northeast% + \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% + \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + \pgfclosepath% + \chit@dbg{3}{Line width for frame: `\the\pgflinewidth'} + \ifchit@draw@frame\pgfusepath{stroke}\fi% + \chit@draw@frametrue% + %\iftikz@mode@fill\pgfusepath{fill}\fi% + \endscope% + \chit@dbg{1}{Chit end of shape} } } % \end{macrocode} @@ -527,7 +667,7 @@ ^^J Name: `#3'} \let\name\pgfutil@empty% \chit@dbg{1}{=== Before chit node}% - \node[draw,chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {}; + \node[chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {}; \chit@dbg{2}{=== After chit node}% \ifx|#3|\relax% \else% diff --git a/macros/latex/contrib/wargame/source/chit/stack.dtx b/macros/latex/contrib/wargame/source/chit/stack.dtx new file mode 100644 index 0000000000..bc8f480e31 --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/stack.dtx @@ -0,0 +1,54 @@ +% \iffalse +% <*chit> +% -------------------------------------------------------------------- +% \fi +% \subsubsection{Stacking of chits} +% +% Stacking of chits. The key \spec{chit/stack direction} sets the +% default direction to make the stack in. +% +% \begin{macrocode} +% offset, location, direction, list +\tikzset{% + chit/stack direction/.store in=\chit@stack@dir, + chit/stack direction/.initial={(.3,.3)}, +} +% \end{macrocode} +% +% Now the code +% +% \begin{macrocode} +\def\chit@stack@dir{(.3,.3)} +\def\stackchits(#1){% + \@ifnextchar({\st@ckchits{#1}}{\st@ckchits{#1}(.3,.3)}%) +} +\def\st@ckchits#1(#2)#3{% + \chit@dbg{2}{Stacking chits `#1', `#2', `#3'}% + \edef\xy{#1}% + \chit@dbg{4}{Stack start at \xy}% + \foreach[count=\i from 0] \c/\o in {#3} {% + \ifx\c\empty\else% + \edef\ccc{\c}% + \chit@dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}% + \expandafter\ccc(\xy)% + %% + \ifx\c\o\else% + %\chit@dbg{0}{Option: \o} + \edef\ccc{\o}% + \expandafter\ccc(\xy)% + \fi + \expandafter\ccc(\xy)% + \tikzmath{% + coordinate \cc;% + \cc = (\xy) + (#2);} + \xdef\xy{\cc}% + \fi% + }% + \@ifnextchar;{\@gobble}{}% +} +% \end{macrocode} +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/chit/table.dtx b/macros/latex/contrib/wargame/source/chit/table.dtx new file mode 100644 index 0000000000..7094e436ed --- /dev/null +++ b/macros/latex/contrib/wargame/source/chit/table.dtx @@ -0,0 +1,176 @@ +% \iffalse +% <*chit> +% -------------------------------------------------------------------- +% \fi +% \subsubsection{Table of chits} +% +% \begin{macrocode} +\tikzset{ + chit/cell background/.style={fill=black}, + blank chit/.style={/chit/frame={draw=none,fill=none}}, +} +% \end{macrocode} +% +% These macros are used when we set tables of chits. This allows us +% to define blank spaces in the table by giving the element +% \texttt{blank~chit}. +% +% \begin{macrocode} +\def\chit@blank{blank chit} +\def\chit@cellbg(#1,#2)#3{% + \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3); +} +% \end{macrocode} +% +% \begin{Macro}{\ifchits@reset} +% +% This `if' controls whether to reset the coordinates to the origin +% when \cs{chits} is called. If true, then reset for a new table. +% +% \begin{macrocode} +\newif\ifchits@reset\chits@resettrue +% \end{macrocode} +% \end{Macro} +% +% \begin{Macro}{\chits,\@chits,\chit@sng@cellupdate} +% \begin{macrocode} +\def\chit@sng@cellupdate(#1,#2)#3#4{% + \chit@dbg{2}{Current `#1' vs `#4'*(`#3'+1)} + \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}% + \xdef#2{\pgfmathresult}% + \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}% + \xdef#1{\pgfmathresult}% +} +% \end{macrocode} +% +% The stared version (\cs{chits*}) of this macro continues the +% previously set chit table. +% +% \begin{macrocode} +\def\chits{% + \@ifstar{\chits@resetfalse\@chits}{\chits@resettrue\@chits}} +% \end{macrocode} +% +% \begin{macrocode} +\def\@chits#1#2#3{ + \ifchits@reset + \def\r{0}% + \def\c{0}% + \fi + \chit@dbg{1}{Chits to make: #1}% + \foreach[count=\ti from 0] \t/\x in #1{% + \chit@dbg{2}{Turn `\t' with option `\x'} + \ifx\t\empty\else% + \foreach \u/\m in \t{% + \ifx\u\empty\else% + \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% + \ifx\m\@empty\def\m{1}\fi% + \ifx\u\m\def\m{1}\fi% + \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% + \foreach \n in {1,...,\m}{% + \ifx\u\chit@blank% + \chit@dbg{3}{Ignoring blank chit:\u}% + \else% + \chit@cellbg(\c,\r){#3}% + \chit[\u=\ti](\c,\r)% + \chit@sng@cellupdate(\c,\r){#2}{#3}% + \fi% + }% + \fi% + }% + \fi% + }% + \@ifnextchar;{\@gobble}{}} +% \end{macrocode} +% \end{Macro} +% +% \begin{Macro}{\doublechits, +% \@doublechits, +% \chit@dbl@cellupdate, +% \chit@dbl@flip} +% \begin{enumerate} +% \item coordinates +% \item coordinates +% \item cell-size +% \end{enumerate} +% +% \begin{macrocode} +\def\chit@dbl@flip(#1,#2)#3{% + \pgfmathparse{-#1}% + \xdef\mc{\pgfmathresult}% +} +% \end{macrocode} +% +% \begin{enumerate} +% \item coordinates +% \item coordinates +% \item Number of columns +% \item cell-size +% \end{enumerate} +% +% \begin{macrocode} +\def\chit@dbl@cellupdate(#1,#2)#3#4{% + \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}% + \xdef#2{\pgfmathresult}% + \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}% + \xdef#1{\pgfmathresult}% +} +% \end{macrocode} +% +% \begin{enumerate} +% \item List of list of keys +% \item Number of columns +% \item size of each cell +% \end{enumerate} +% +% The stared version (\cs{doublechits*}) of this macro continues the +% previously set chit table. +% +% \begin{macrocode} +\def\doublechits{% + \@ifstar{\chits@resetfalse\@doublechits}{\chits@resettrue\@doublechits}} +% \end{macrocode} +% +% \begin{macrocode} +\def\@doublechits#1#2#3{% + \chit@dbg{1}{Setting double-sided chits: #1} + \ifchits@reset + \pgfmathparse{-(#2-.5)*#3} + \xdef\c{\pgfmathresult} + \def\r{0} + \fi + + \foreach[count=\ti from 0] \t/\x in #1{ + \ifx\t\empty\else% + \foreach \u/\m in \t{ + \ifx\u\empty\else + \ifx\m\@empty\def\m{1}\else% + \ifx\u\m\def\m{1}\fi\fi + \chit@dbg{2}{`\u'=`\m' (\c,\r)} + \foreach \n in {1,...,\m}{% + \ifx\u\chit@blank + \chit@dbg{3}{Ignoring blank chit:\u} + \else + \chit@cellbg(\c,\r){#3} + \chit[\u=\ti](\c,\r) + \chit@dbl@flip(\c,\r){#3} + \chit@cellbg(\mc,\r){#3} + \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r) + \chit@dbl@cellupdate(\c,\r){#2}{#3} + \fi + } + \fi + } + \fi + } + \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);% + \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[transform shape,anchor=west]{Back};% + \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[transform shape,anchor=east]{Front};% + \@ifnextchar;{\@gobble}{}} +% \end{macrocode} +% \end{Macro} +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/hex/board.dtx b/macros/latex/contrib/wargame/source/hex/board.dtx index fd0af295fb..7c791a72b3 100644 --- a/macros/latex/contrib/wargame/source/hex/board.dtx +++ b/macros/latex/contrib/wargame/source/hex/board.dtx @@ -18,82 +18,261 @@ % % % \begin{macrocode} -\tikzset{ - hex/board frame/.style={draw} -} \def\boardframe{% - \@ifnextchar[{\bo@rdframe}{\bo@rdframe[0]}%} + \@ifnextchar[{\bo@rdframe}{\bo@rdframe[0]}%] +} +% \end{macrocode} +% +% Below is our new implementation of \cs{boardframe}. This is split +% into parts. +% +% +% First, a macro that will define the path around rectangular placed +% hexes. This takes 4 mandatory arguments: lower left column and row, +% and upper right column and row, in that order. It also accepts an +% optional argument. If this is not empty, then it is assumed to be a +% style to apply, and hexes will be drawn using that style. The style +% will be passed the hex coordinates and can react accordingly. +% +% \begin{macrocode} +\def\bo@rdfr@me{ + \@ifnextchar[{\bo@rdfr@me@}{\bo@rdfr@me@[]}%] +} +\def\bo@rdfr@me@u(#1)#2#3#4#5{ + \hex@coords@conv{#1} + % \hex@dbg{0}{#1 -> `\hex@x',`\hex@y'} + \pgfmathparse{min(#2,\hex@x)}\xdef#2{\pgfmathresult} + \pgfmathparse{min(#3,\hex@y)}\xdef#3{\pgfmathresult} + \pgfmathparse{max(#4,\hex@x)}\xdef#4{\pgfmathresult} + \pgfmathparse{max(#5,\hex@y)}\xdef#5{\pgfmathresult} + \hex@dbg{2}{#1 -> ll=`#2',`#3', ur=`#4',`#5'} +} +\def\bo@rdfr@me@[#1]#2#3#4#5{ + % Define rtmp and a ctmp to by directions + \pgfmathparse{int(\hex@coords@row@fac)}\edef\rtmp{\pgfmathresult} + \pgfmathparse{int(\hex@coords@col@fac)}\edef\ctmp{\pgfmathresult} + % Define vertices for path + \def\ctfv{SW} + \def\ctsv{SE} + \def\cbfv{NE} + \def\cbsv{NW} + \def\rrfv{E} + \def\rrsv{NE} + \def\rlfv{W} + \def\rlsv{SW} + % Swap around some definitions based on the row direction + \ifnum\rtmp<0 + \let\max@short\hex@bot@short@col + \let\min@short\hex@top@short@col + \let\swp\ctfv\let\ctfv\cbsv\let\cbsv\swp + \let\swp\ctsv\let\ctsv\cbfv\let\cbfv\swp + \def\rrsv{SE} + \def\rlsv{NW} + \else + \let\max@short\hex@top@short@col + \let\min@short\hex@bot@short@col + \fi + % Swap around some definitions based on the column direction + \ifnum\ctmp<0 + \let\swp\ctfv\let\ctfv\ctsv\let\ctsv\swp + \let\swp\cbfv\let\cbfv\cbsv\let\cbsv\swp + \let\swp\rrfv\let\rrfv\rlsv\let\rlsv\swp + \let\swp\rrsv\let\rrsv\rlfv\let\rlfv\swp + \fi + % Define tmp = 0 if no shorts, 1 if top short, 2 if both + \pgfmathparse{ifthenelse(\hex@got@top@short, + ifthenelse(\hex@got@bot@short,2,1),0)}\edef\tmp{\pgfmathresult} + % If top-short, set factors + \ifnum\tmp=1 + \def\mnf{-1} + \def\mxf{-1} + \def\mnn{} + \def\mxn{} + % If both short, set factors + \else\ifnum\tmp=2 + \def\mnf{\rtmp} + \def\mxf{(-\rtmp)} + % If inverse rows, set factors + \ifnum\rtmp<0 + \def\mnn{} + \def\mxn{not} + \else + \def\mnn{not} + \def\mxn{} + \fi + % If none is short + \else + \def\mnf{1} + \def\mxf{1} + \def\mnn{not} + \def\mxn{not} + \fi\fi + % Define row@mn to give least row of column + \def\row@mn##1{% + \pgfmathparse{int(#3+\mnf* + \hex@coords@row@fac*\min@short(##1)* + \mnn(\min@short(\hex@coords@col@off)))} + \edef\lr{\pgfmathresult}} + % Define row@mx to give largest row of column + \def\row@mx##1{% + \pgfmathparse{int(#5+\mxf* + \hex@coords@row@fac*\max@short(##1)* + \mxn(\max@short(\hex@coords@col@off)))} + \edef\ur{\pgfmathresult}} + % + % + % Below defines a path around the perimeter of the hexes. + % + \def\@llx{10000} + \def\@lly{10000} + \def\@urx{-10000} + \def\@ury{-10000} + % Start with an empty path + \def\p{} + % Loop across least row (can be top if \rtmp<0) + \foreach \c in {#2,...,#4}{% + \row@mn{\c} + \row@mx{\c} + % \message{^^JColumn: `\c' -> `\lr',`\ur' (#3,#5)} + } + \foreach \c in {#2,...,#4}{% + \row@mn{\c} + \xdef\p{\p + (hex cs:c=\c,r=\lr,v=\ctfv)-- + (hex cs:c=\c,r=\lr,v=\ctsv)--} + \bo@rdfr@me@u(c=\c,r=\lr,v=\ctfv)\@llx\@lly\@urx\@ury + \bo@rdfr@me@u(c=\c,r=\lr,v=\ctsv)\@llx\@lly\@urx\@ury + } + % Go up (down if \rtmp<0) right side + \row@mn{#4} + \row@mx{#4} + \foreach \r in {\lr,...,\ur}{% + \xdef\p{\p + (hex cs:c=#4,r=\r,v=\rrfv)-- + (hex cs:c=#4,r=\r,v=\rrsv)--} + \bo@rdfr@me@u(c=#4,r=\r,v=\rrfv)\@llx\@lly\@urx\@ury + \bo@rdfr@me@u(c=#4,r=\r,v=\rrsv)\@llx\@lly\@urx\@ury + } + % Go across largest row (can be bottom if \rtmp<0) + \foreach \c in {#4,...,#2}{% + \row@mx{\c} + % \message{^^JColumn: `\c', max:`\ur'} + \xdef\p{\p + (hex cs:c=\c,r=\ur,v=\cbfv)-- + (hex cs:c=\c,r=\ur,v=\cbsv)--} + \bo@rdfr@me@u(c=\c,r=\ur,v=\cbfv)\@llx\@lly\@urx\@ury + \bo@rdfr@me@u(c=\c,r=\ur,v=\cbsv)\@llx\@lly\@urx\@ury + } + % Go up (down if \rtmp<0) left side. + \row@mn{#2} + \row@mx{#2} + \foreach \r in {\ur,...,\lr}{% + \xdef\p{\p + (hex cs:c=#2,r=\r,v=\rlfv)-- + (hex cs:c=#2,r=\r,v=\rlsv)--} + \bo@rdfr@me@u(c=#2,r=\r,v=\rlfv)\@llx\@lly\@urx\@ury + \bo@rdfr@me@u(c=#2,r=\r,v=\rlsv)\@llx\@lly\@urx\@ury + } + % End path with cycle + \edef\p{\p cycle} + % Define global path + \global\let\hex@board@path\p + \hex@dbg{3}{Hex board path: `\meaning\hex@board@path'} + % If an optional argument was given, then use that to actually make + % hexes. + \ifx|#1|\else + \foreach[count=\nc] \c in {#2,...,#4}{% + \row@mn{\c} + \row@mx{\c} + \foreach \r in {\lr,...,\ur}{% + \hex[#1={\c,\r}](c=\c,r=\r) + } + } + \fi +} +% \end{macrocode} +% +% +% This is a no operations style used as default for the macro +% \cs{boardhexes} below. +% +% \begin{macrocode} +\tikzset{% + /hex/board/no op/.style args={#1,#2}{}} +% \end{macrocode} +% +% This macro will make the actual hexes using the specified, optional, +% style. It builds on \cs{bo@rdfr@me} above. +% +% \begin{macrocode} +\def\boardhexes{% + \@ifnextchar[{\bo@rdhexes}{\bo@rdhexes[board/no op]}%] } +\def\bo@rdhexes[#1](#2)(#3){% + \hex@coords@conv{#2} + \edef\llc{\hex@col} + \edef\llr{\hex@row} + \hex@coords@conv{#3} + \edef\urc{\hex@col} + \edef\urr{\hex@row} + \bo@rdfr@me[#1]{\llc}{\llr}{\urc}{\urr}} +% \end{macrocode} +% +% Creates a board frame using \cs{bo@rdfr@me}. +% +% \begin{macrocode} +\tikzset{board frame bb/.code={ + \pgfkeys{ + %/tikz/local bounding box=tmp board frame, + /tikz/transform shape, + /tikz/execute at end scope={% + % \hex@dbg{1}{Getting board frame BB} + %\wg@get@bb{tmp board frame} + \global\let\llx\@llx + \global\let\lly\@lly + \global\let\urx\@urx + \global\let\ury\@ury + % \hex@dbg{0}{Board bounding box (\llx,\lly)x(\urx,\ury)} + }}}} + \def\bo@rdframe[#1](#2)(#3){% \hex@coords@conv{#2} - \edef\llx{\hex@x} - \edef\lly{\hex@y} \edef\llc{\hex@col} \edef\llr{\hex@row} - \edef\ellc{\hex@eff@col} - \edef\ellr{\hex@eff@row} % \hex@coords@conv{#3} - \edef\urx{\hex@x} - \edef\ury{\hex@y} \edef\urc{\hex@col} \edef\urr{\hex@row} - \edef\eurc{\hex@eff@col} - \edef\eurr{\hex@eff@row} % \def\margin{#1} % - \hex@dbg{2}{% - Board Hex range: (\llc,\llr)x(\urc,\urr) - ^^JEffective range: (\ellc,\ellr)x(\eurc,\eurr) - ^^JBB: (\llx,\lly)x(\urx,\ury)}% - \ifnum\hexdbglvl>1 - %\draw[red,very thick](hex cs:c=\llc,r=\llr) rectangle(hex cs:c=\urc,r=\urr); - \draw[red,ultra thick,dashed](\llx,\lly) rectangle(\urx,\ury); - \draw[->,very thick,blue] (0,0) -- (0,1) (0,0) -- (1,0); - \fi - % Calculate how many half hex hides to add to the "bottom" + % This will store the bounding box in tmp node `board frame' + \bo@rdfr@me{\llc}{\llr}{\urc}{\urr}% + \begin{scope}[board frame bb] + \expandafter\path\hex@board@path; + \end{scope} + \hex@dbg{1}{Board frame LL: -> `\llx',`\lly'} + \pgfmathparse{\llx+ifthenelse(\llx<0,-1,1)*\margin}\edef\llx{\pgfmathresult} + \pgfmathparse{\lly+ifthenelse(\lly<0,-1,1)*\margin}\edef\lly{\pgfmathresult} % - \def\oddeven{isodd} - \ifnum\hex@coords@row@fac<0\def\oddeven{iseven}\fi% - \pgfmathparse{ - ifthenelse(\hex@got@bot@short(\ellc), - ifthenelse(\hex@bot@short@col(\llc)*not(\oddeven(\ellc)),2, - ifthenelse(\hex@bot@short@col(\llc),0,1)), - ifthenelse(\oddeven(\ellc),1,2))} - \edef\olly{\pgfmathresult}% - \hex@dbg{2}{Delta lly: \olly half heights} - % Calculate how many half hex heights to add to the "top" - \def\oddeven{iseven} - \ifnum\hex@coords@row@fac<0\def\oddeven{isodd}\fi% + \hex@dbg{1}{Board frame UR: -> `\urx',`\ury'} + \pgfmathparse{\urx+ifthenelse(\urx<0,-1,1)*\margin}\edef\urx{\pgfmathresult} + \pgfmathparse{\ury+ifthenelse(\ury<0,-1,1)*\margin}\edef\ury{\pgfmathresult} % - \pgfmathparse{ - ifthenelse(\hex@got@top@short(\urc), - ifthenelse(\hex@top@short@col(\urc)*\oddeven(\eurc),0, - ifthenelse(\hex@top@short@col(\urc),2,1)), - ifthenelse(\oddeven(\eurc),1,2))} - \edef\oury{\pgfmathresult}% - \hex@dbg{2}{Delta ury: \oury half heights} - % Calculate new LLY and URY - \pgfmathparse{\lly-\hex@coords@row@fac*(\olly*\hex@yy+\margin)} - \edef\lly{\pgfmathresult} - \pgfmathparse{\ury+\hex@coords@row@fac*(\oury*\hex@yy+\margin)} - \edef\ury{\pgfmathresult} - % Calculate new LLX and URX - \pgfmathparse{\llx-1-\margin}\edef\llx{\pgfmathresult} - \pgfmathparse{\urx+1+\margin}\edef\urx{\pgfmathresult} - % Calculate width and height \pgfmathparse{\urx-\llx}\edef\w{\pgfmathresult} \pgfmathparse{\ury-\lly}\edef\h{\pgfmathresult} + %% Print to the log \hex@dbg{0}{Board Frame: (\llx,\lly)x(\urx,\ury) (\w x\h) (\llc,\llr)x(\urc,\urr)} + %% Possibly draw \draw[hex/board frame/.try](\llx,\lly) rectangle(\urx,\ury); + %% Store macros \xdef\boardXmin{\llx}% \xdef\boardYmin{\lly}% \xdef\boardXmax{\urx}% \xdef\boardYmax{\ury}% -} - +} % \end{macrocode} +% % \end{Macro} % % \begin{Macro}{\boardclip} @@ -104,108 +283,28 @@ % \cs{boardclip}\marg{nx}\marg{ny}\marg{preaction} % \end{Syntax} % \begin{macrocode} -\def\boardpath(#1)(#2){% +\def\boardpath(#1)(#2){ \hex@coords@reset% \tikzset{/hex/coords/.cd, #1} - \edef\llx{\hex@col} - \edef\lly{\hex@row} + \edef\llc{\hex@col} + \edef\llr{\hex@row} %% \hex@coords@reset% \tikzset{/hex/coords/.cd, #2} - \edef\urx{\hex@col} - \edef\ury{\hex@row} - \let\board@odd\@undefined% - \hex@dbg{1}{Board BB in hex: (\llx,\lly)x(\urx,\ury)} - %% - \def\fv{south} - \def\sv{north} - \ifnum\hex@coords@row@fac<0 - \def\fv{north} - \def\sv{south} - \fi - - \edef\hex@board@path{(hex cs:c=\llx,r=\lly,v=\fv\space west)} - %% First the left side - \foreach \r in {\lly,...,\ury} {% - \edef\t{ - --(hex cs:c=\llx,r=\r,v=west) - --(hex cs:c=\llx,r=\r,v=\sv\space west)} - \wg@addto@macro{\hex@board@path}{\t}} - %% Then for top of board - \foreach \c in {\llx,...,\urx} {% - % To be done - %\pgfmathparse{int(ifthenelse(\hex@bot@short@col(\c),1,0))} - %\edef\tmp{\pgfmathresult} - %\ifnum\tmp>0 - %\edef\t{ - % --(hex cs:c=\c,r=\ury,v=\sv\space west) - % --(hex cs:c=\c,r=\ury,v=\sv\space east)} - %\else - \edef\t{ - --(hex cs:c=\c,r=\ury,v=\fv\space east) - --(hex cs:c=\c,r=\ury,v=\fv\space west)} - %\fi - \wg@addto@macro{\hex@board@path}{\t}} - %% Then for right of board - \foreach \r in {\ury,...,\lly} {% - \edef\t{ - --(hex cs:c=\urx,r=\r,v=east) - --(hex cs:c=\urx,r=\r,v=\fv\space east)} - \wg@addto@macro{\hex@board@path}{\t}} - - %% Then for bottom of board - \edef\t{--(hex cs:r=\lly,c=\urx,v=\fv\space west)} - \wg@addto@macro{\hex@board@path}{\t} - \foreach \c in {\urx,...,\llx} {% - \pgfmathparse{int(ifthenelse(\hex@bot@short@col(\c),1,0))} - \edef\tmp{\pgfmathresult} - \ifnum\tmp>0 - \edef\t{ - --(hex cs:c=\c,r=\lly,v=\sv\space east) - --(hex cs:c=\c,r=\lly,v=\sv\space west)} - \else - \edef\t{ - --(hex cs:c=\c,r=\lly,v=\fv\space east) - --(hex cs:c=\c,r=\lly,v=\fv\space west)} - \fi - \wg@addto@macro{\hex@board@path}{\t}} - - \def\t{--cycle} - \wg@addto@macro{\hex@board@path}{\t} + \edef\urc{\hex@col} + \edef\urr{\hex@row} + + % This will store the bounding box in tmp node `board frame' + \bo@rdfr@me{\llc}{\llr}{\urc}{\urr}% + %% Use the path to extract the bounding box + %\begin{scope}[local bounding box=board frame] + % \expandafter\path\hex@board@path; + %\end{scope} \global\let\hexboardpath\hex@board@path } -%\def\boardclip#1#2#3{% -% \pgfmathparse{int(#1-1)}\xdef\board@range{\pgfmathresult,...,0}% -% %% \show\board@range -% \draw \ifx|#3|\else[preaction={#3}]\fi% -% [clip] -% % [decorate,decoration={show path construction, -% % moveto code={\fill[red](\tikzinputsegmentfirst) circle(2pt) -% % node [fill=none,below]{moveto};}, -% % lineto code={\draw[thick,blue,->](\tikzinputsegmentfirst)-- -% % (\tikzinputsegmentlast) node [above] {lineto};}, -% % curveto code={\draw[thick,green,->](\tikzinputsegmentfirst).. -% % controls(\tikzinputsegmentsupporta) and -% % (\tikzinputsegmentsupportb) -% % ..(\tikzinputsegmentlast) node[above]{curveto};}, -% % closepath code={\draw[thick,orange,->](\tikzinputsegmentfirst)-- -% % (\tikzinputsegmentlast) node [above] {closepath};} -% % }] -% (hex cs:r=0,c=0,v=south west) -% %% First the left side -% \foreach \r in {0,1,...,#2} {% -% --(hex cs:c=0,r=\r,v=west)--(hex cs:c=0,r=\r,v=north west)} -% %% Then for top of board -% \foreach \c in {0,1,...,#1} {% -% --(hex cs:r=#2,c=\c,v=north west)--(hex cs:c=\c,r=#2,v=north east)} -% %% Then for right of board -% \foreach \r in {#2,...,0} {% -% --(hex cs:c=#1,r=\r,v=east)--(hex cs:c=#1,r=\r,v=south east)} -% %% Then for bottom of board -% --(hex cs:r=0,c=#1,v=south west) \foreach \c in \board@range {% -% --(hex cs:r=0,c=\c,v=south east) --(hex cs:c=\c,r=0,v=south west) } -% --cycle; } -%% New definition - much simpler + +% \end{macrocode} +% \begin{macrocode} \def\boardclip(#1)(#2)#3{% \boardpath(#1)(#2) \draw \ifx|#3|\else[preaction={#3}]\fi% diff --git a/macros/latex/contrib/wargame/source/hex/coord.dtx b/macros/latex/contrib/wargame/source/hex/coord.dtx index b996ffa3c2..147ae82892 100644 --- a/macros/latex/contrib/wargame/source/hex/coord.dtx +++ b/macros/latex/contrib/wargame/source/hex/coord.dtx @@ -278,11 +278,10 @@ % \begin{macrocode} \pgfmathparse{int(\hex@coords@col@fac*(\hex@col+\hex@coords@col@off))}% \xdef\hex@eff@col{\pgfmathresult}% - \hex@dbg{2}{Effective column: \hex@coords@col@fac * (\hex@col - + \hex@dbg{2}{Effective column: \hex@coords@col@fac * (\hex@col + \hex@coords@col@off) -> \hex@eff@col}% \pgfmathparse{\hex@eff@col*1.5}% \xdef\hex@x{\pgfmathresult}% - \expandafter\pgf@x=\hex@x cm% % \end{macrocode} % % And then for the $y$ coordinate and set the dimension \cs{pgf@y}. @@ -304,17 +303,21 @@ \hex@dbg{2}{Effective row: \hex@coords@row@fac * (\hex@row + \hex@coords@row@off) -> \hex@eff@row}% \pgfmathparse{(2*\hex@eff@row-mod(round((\hex@col+\hex@coords@col@off)),2))*\hex@yy}% + \pgfmathparse{(2*\hex@eff@row-mod(abs(round(\hex@col+\hex@coords@col@off)),2))*\hex@yy}% \xdef\hex@y{\pgfmathresult}% - \expandafter\pgf@x=\hex@y cm% % \end{macrocode} % % If we have a vertex specification add that location to the current % coordinates. If not, set the point. % % \begin{macrocode} - \ifx\hex@vtx\@empty\pgfpointxy{\hex@x}{\hex@y}\else% - \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{% - \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@vtx}{1}}}\fi% + \ifx\hex@vtx\@empty\else% + \pgfmathparse{\hex@x+\hex@off*cos(\hex@vtx)}\xdef\hex@x{\pgfmathresult} + \pgfmathparse{\hex@y+\hex@off*sin(\hex@vtx)}\xdef\hex@y{\pgfmathresult} + \fi% + % \ifx\hex@vtx\@empty\pgfpointxy{\hex@x}{\hex@y}\else% + % \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{% + % \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@vtx}{1}}}\fi% % \end{macrocode} % % If we have an edge specification add that location to the current @@ -322,20 +325,28 @@ % % \begin{macrocode} \ifx\hex@edg\@empty\else% - \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{% - \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@edg}{\hex@yy}}}\fi + \pgfmathparse{\hex@x+\hex@off*\hex@yy*cos(\hex@edg)}% + \xdef\hex@x{\pgfmathresult}% + \pgfmathparse{\hex@y+\hex@off*\hex@yy*sin(\hex@edg)}% + \xdef\hex@y{\pgfmathresult}% + \fi% + % \ifx\hex@edg\@empty\else% + % \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{% + % \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@edg}{\hex@yy}}}\fi % \end{macrocode} % % For debugging, we can print out stuff. % % \begin{macrocode} + \pgfpointxy{\hex@x}{\hex@y} \hex@dbg{2}{Hex coordinates: #1 - ^^J c=\hex@col - ^^J r=\hex@row - ^^J v=\hex@vtx - ^^J e=\hex@edg - ^^J x=\hex@x - ^^J y=\hex@y}% + ^^J c=`\hex@col' + ^^J r=`\hex@row' + ^^J v=`\hex@vtx' + ^^J e=`\hex@edg' + ^^J o=`\hex@off' + ^^J x=`\hex@x' + ^^J y=`\hex@y'}% \global\let\hex@x\hex@x% \global\let\hex@y\hex@y% \global\let\hex@row\hex@row% diff --git a/macros/latex/contrib/wargame/source/hex/core.dtx b/macros/latex/contrib/wargame/source/hex/core.dtx index ee04290955..89179ef9fd 100644 --- a/macros/latex/contrib/wargame/source/hex/core.dtx +++ b/macros/latex/contrib/wargame/source/hex/core.dtx @@ -89,6 +89,7 @@ % \input{hex/extra.dtx} % \input{hex/paths.dtx} % \input{hex/board.dtx} +% \input{hex/split.dtx} % \iffalse % % \fi diff --git a/macros/latex/contrib/wargame/source/hex/extra.dtx b/macros/latex/contrib/wargame/source/hex/extra.dtx index 05f7215297..f2e6e1cb75 100644 --- a/macros/latex/contrib/wargame/source/hex/extra.dtx +++ b/macros/latex/contrib/wargame/source/hex/extra.dtx @@ -18,7 +18,7 @@ % \begin{macrocode} \tikzset{% hex/fortress/.pic={ - \path[draw,pic actions] + \path[draw,solid,pic actions] (0: .9) -- (0: .7) -- (60: .7) -- ( 60:.9) -- ( 60:.7) -- diff --git a/macros/latex/contrib/wargame/source/hex/paths.dtx b/macros/latex/contrib/wargame/source/hex/paths.dtx index 833a4a245a..813ab869fa 100644 --- a/macros/latex/contrib/wargame/source/hex/paths.dtx +++ b/macros/latex/contrib/wargame/source/hex/paths.dtx @@ -45,19 +45,45 @@ } }% }% - +% \end{macrocode} +% +% A decoration to make a fortification line +% +% \begin{macrocode} +\pgfdeclaredecoration{fortification}{initial} +{ + \state{initial}[width=4\pgflinewidth] + { + \pgfpathlineto{\pgfpoint{2\pgflinewidth}{0}} + \pgfpathlineto{\pgfpoint{2\pgflinewidth}{2\pgflinewidth}} + \pgfpathlineto{\pgfpoint{4\pgflinewidth}{2\pgflinewidth}} + \pgfpathlineto{\pgfpoint{4\pgflinewidth}{0}} + } + \state{final} + { + \pgfpathlineto{\pgfpointdecoratedpathlast} + } +} +% \end{macrocode} +% +% Roads, railroads, rivers, borders. +% +% \begin{macrocode} \tikzset{ hex/road/.style={ rounded corners=3\pgflinewidth,% .25cm, color=black, transform shape, scale line widths, - thick}, + thick, + every hex road/.try, + }, hex/railroad/.style={ %scale line widths, rounded corners=.25cm, color=gray!50!black, transform shape, + every hex railroad/.try, postaction={draw,decorate}, decoration={ticks, segment length=9\pgflinewidth, @@ -67,29 +93,42 @@ hex/river/.style={ color=blue, scale line widths, + scale rounded corners, line width=3pt, transform shape, + every hex river/.try, decorate, decoration={random steps, - segment length=.3cm, - amplitude=.15cm, + segment length=3\pgflinewidth, + amplitude=1.5\pgflinewidth, pre=lineto, post=lineto, - pre length=.05cm, - post length=.05cm}, - rounded corners=.08cm}, + pre length=.5\pgflinewidth, + post length=.5\pgflinewidth}, + rounded corners=.75\pgflinewidth}, hex/border/.style={ color=gray, - rounded corners=3pt, dashed, transform shape, scale line widths, - very thick + very thick, + rounded corners=3\pgflinewidth, + every hex border/.try }, - every river/.style={}, - every road/.style={}, - every railroad/.style={}, - every border/.style={}, + % + % Fortification line + % + hex/fortified line/.style={ + draw=brown!50!black, + scale line widths, + line width=2pt, + every hex fortification line/.try, + decoration={fortification,raise=-2\pgflinewidth}, + decorate}, + % every river/.style={}, + % every road/.style={}, + % every railroad/.style={}, + % every border/.style={}, } % \end{macrocode} % @@ -97,25 +136,30 @@ % % \begin{macrocode} \def\road{% - \hex@dbg{3}{Road} + %\hex@dbg{3}{Road} \@ifnextchar[{\road@}{\road@[]}%] } \def\road@[#1]{\draw[hex/road,every hex road/.try,#1]} \def\railroad{% - \hex@dbg{3}{Rail road} + %\hex@dbg{3}{Rail road} \@ifnextchar[{\railroad@}{\railroad@[]}%] } \def\railroad@[#1]{\draw[hex/railroad,every hex railroad/.try,#1]} \def\river{% - \hex@dbg{3}{River} + %\hex@dbg{3}{River} \@ifnextchar[{\river@}{\river@[]}%] } -\def\river@[#1]{\draw[hex/river,every hex river/.try,#1]} +\def\river@[#1]{\draw[hex/river,#1]} \def\border{% \hex@dbg{3}{Border} \@ifnextchar[{\border@}{\border@[]}%] } \def\border@[#1]{\draw[hex/border,every hex border/.try,#1]} +\def\fortifiedline{% + \@ifnextchar[{\fortifiedline@}{\fortifiedline@[]}%] +}% +\def\fortifiedline@[#1]{% + \draw[hex/fortified line,every hex fortified line/.try,#1]} % \end{macrocode} % \end{Macro} % @@ -209,10 +253,10 @@ markings, mark=between positions 0 and 1 step 0.75*\hex@scale*\hex@dy with { \node [single arrow, - single arrow head extend=3pt, + single arrow head extend=.1*\hex@scale*\hex@dy, fill=#1, - inner sep=\hex@scale*.5mm, - minimum width=\hex@scale*2mm, + inner sep=0.05*\hex@scale*\hex@dy, + minimum width=0.02*\hex@scale*\hex@dy, minimum height=\hex@scale*\hex@dy/2, transform shape]{}; } @@ -236,10 +280,10 @@ markings, mark=between positions 0 and 1 step 0.5*\hex@scale*\hex@dy with { \node [single arrow, - single arrow head extend=3pt, + single arrow head extend=.1*\hex@scale*\hex@dy, fill=#1, - inner sep=\hex@scale*.5mm, - minimum width=\hex@scale*2mm, + inner sep=0.05*\hex@scale*\hex@dy, + minimum width=0.02*\hex@scale*\hex@dy, minimum height=\hex@scale*\hex@dy/3, transform shape]{}; } @@ -270,7 +314,7 @@ anchor=west, inner sep=\hex@scale*.25mm, outer sep=.3*\hex@scale*\hex@dy, - minimum width=\hex@scale*2mm, + minimum width=0.02*\hex@scale*\hex@dy, minimum height=1.4*\hex@scale*\hex@dy, transform shape]{}; } @@ -300,10 +344,10 @@ transform shape] {};}, mark=between positions 0 and 1 step 0.75*\hex@scale*\hex@dy with { \node [single arrow, - single arrow head extend=\hex@scale*3pt, + single arrow head extend=.1*\hex@scale*\hex@dy, fill=#1, - inner sep=\hex@scale*1mm, - minimum width=\hex@scale*3mm, + inner sep=0.05*\hex@scale*\hex@dy, + minimum width=0.02*\hex@scale*\hex@dy, minimum height=\hex@scale*\hex@dy/2, transform shape]{}; } @@ -331,7 +375,7 @@ fill=#2, transform shape, text=#1, - font=\sffamily\bfseries\Large}, + font=\sffamily\bfseries\fontsize{14.4}{17}\selectfont}, hex/move cost/.default={black}{none}, % Argument is fill colour % \end{macrocode} diff --git a/macros/latex/contrib/wargame/source/hex/ridges.dtx b/macros/latex/contrib/wargame/source/hex/ridges.dtx index 3914ff6ce5..24552e36e2 100644 --- a/macros/latex/contrib/wargame/source/hex/ridges.dtx +++ b/macros/latex/contrib/wargame/source/hex/ridges.dtx @@ -9,23 +9,31 @@ % A hex can be decorated with up to 6 ridges --- one for each edge of % the hexagon. The first thing is to set up the graphics style to use % for the ridges. We use the \spec{wave} decoration. +% +% If rounded corners are set for ridges, (e.g., via \spec{every hex +% ridges}), then it should be \spec{0pt} or \spec{4pt} (roughly 2mm) +% or larger. Otherwise, one will get a ``dimension too large'' +% error. % % \begin{macrocode} \tikzset{% - hex/ridges/.style={ + hex/ridges pre/.style={ line cap=round, draw=pgfstrokecolor, - rounded corners=.25cm, - scale line widths, + solid, + /hex/ridges/.cd,% + radius=0.85,% + n=4, + R=.25, + }, + hex/ridges/.style={ + get scale, decoration={ path has corners=true, - pre=waves, - post=waves, - pre length=-.1cm, - post length=0cm, waves, - radius=.2cm, - segment length=.2cm}, + radius=\wg@scale\hex@r@R, + segment length=\wg@scale\hex@r@s, + }, decorate}} % \end{macrocode} % @@ -57,6 +65,7 @@ south/.is if=hex@r@s, south east/.is if=hex@r@se, radius/.store in=\hex@r@r, + curve radius/.store in=\hex@r@w, NE/.is if=hex@r@ne, N/.is if=hex@r@n, NW/.is if=hex@r@nw, @@ -64,6 +73,8 @@ S/.is if=hex@r@s, SE/.is if=hex@r@se, r/.store in=\hex@r@r, + n/.store in=\hex@r@n, + R/.store in=\hex@r@w, } % \end{macrocode} % @@ -74,10 +85,11 @@ % the routine below is handcrafted since it is relatively simple. % % \begin{macrocode} +\newdimen\hex@r@s +\newdimen\hex@r@R \def\hex@do@ridges{% \edef\hex@r@tmp{[ - /hex/ridges/.cd,% - radius=0.8,% + hex/ridges pre, /tikz/every hex ridges/.try, \hex@ridges]} \expandafter\scope\hex@r@tmp% @@ -89,44 +101,70 @@ ^^Jsouth =\ifhex@r@s yes\else no\fi ^^Jsouth east=\ifhex@r@se yes\else no\fi ^^Jradius =\hex@r@r + ^^Jn =\hex@r@n } + \pgfmathparse{\hex@r@r/\hex@r@n}\xdef\hex@r@t{\pgfmathresult} + \hex@r@s=\hex@r@t cm + \hex@r@R=\hex@r@w cm \def\hex@r@p{} % Hand written algorithm \ifhex@r@ne - \def\hex@r@p{(0:\hex@r@r)--(60:\hex@r@r)} + \ifhex@r@se + \xdef\hex@r@p{(0:\hex@r@r)--(60:\hex@r@r)} + \else + \xdef\hex@r@p{($(0:\hex@r@r)+(-60:\hex@r@t/2)$)--(60:\hex@r@r)} + \fi + \hex@dbg{4}{Ridge along north east edge: `\hex@r@p'} \fi \ifhex@r@n - \hex@dbg{4}{Ridge along north edge: `\hex@r@p'} \ifhex@r@ne\else - \xdef\hex@r@p{\hex@r@p ( 60:\hex@r@r)} + \xdef\hex@r@p{\hex@r@p ($( 60:\hex@r@r)+(0:\hex@r@t/2)$)} \fi \xdef\hex@r@p{\hex@r@p --(120:\hex@r@r)} + \hex@dbg{4}{Ridge along north edge: `\hex@r@p'} \fi \ifhex@r@nw \ifhex@r@n\else - \xdef\hex@r@p{\hex@r@p (120:\hex@r@r)} + \xdef\hex@r@p{\hex@r@p ($(120:\hex@r@r)+(60:\hex@r@t/2)$)} \fi \xdef\hex@r@p{\hex@r@p --(180:\hex@r@r)} + \hex@dbg{4}{Ridge along north west: `\hex@r@p'} \fi \ifhex@r@sw \ifhex@r@nw\else - \xdef\hex@r@p{\hex@r@p (180:\hex@r@r)} + \xdef\hex@r@p{\hex@r@p ($(180:\hex@r@r)+(120:\hex@r@t/2)$)} + \fi + \ifhex@r@s + \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)} + \else + \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)} \fi - \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)} + \hex@dbg{4}{Ridge along south west: `\hex@r@p'} \fi \ifhex@r@s \ifhex@r@sw\else - \xdef\hex@r@p{\hex@r@p (240:\hex@r@r)} + \xdef\hex@r@p{\hex@r@p ($(240:\hex@r@r)+(-\hex@r@t/2,0)$)} \fi - \xdef\hex@r@p{\hex@r@p --(300:\hex@r@r)} + \ifhex@r@se + \xdef\hex@r@p{\hex@r@p --(300:\hex@r@r)} + \else + \xdef\hex@r@p{\hex@r@p --(300.5:\hex@r@r)} + \fi + \hex@dbg{4}{Ridge along south: `\hex@r@p'} \fi \ifhex@r@se \ifhex@r@s\else - \xdef\hex@r@p{\hex@r@p (300:\hex@r@r)} + \xdef\hex@r@p{\hex@r@p ($(300:\hex@r@r)+(-120:\hex@r@t/2)$)} + \fi + \ifhex@r@ne + \xdef\hex@r@p{\hex@r@p --cycle} + \else + \xdef\hex@r@p{\hex@r@p --(.5:\hex@r@r)} \fi - \xdef\hex@r@p{\hex@r@p --(360:\hex@r@r)} + \hex@dbg{4}{Ridge along se: `\hex@r@p'} \fi \hex@dbg{3}{ Ridges path: \hex@r@p} + % \draw[red] \hex@r@p; \draw[hex/ridges] \hex@r@p; \endscope% End of ridges scope } diff --git a/macros/latex/contrib/wargame/source/hex/shape.dtx b/macros/latex/contrib/wargame/source/hex/shape.dtx index 0d31da837f..104dbf23c6 100644 --- a/macros/latex/contrib/wargame/source/hex/shape.dtx +++ b/macros/latex/contrib/wargame/source/hex/shape.dtx @@ -14,6 +14,77 @@ % such as terrain and so on. % % \begin{macrocode} +\tikzset{% + /hex/.cd, + bev/.store in=\hex@bevel, bev/.initial=, + bevel fraction/.store in=\hex@bevel@frac,bevel fraction/.initial=10, + bevel/.is choice, + bevel/none/.style = {/hex/bev=}, + bevel/north west/.style = {/hex/bev=1}, + bevel/north east/.style = {/hex/bev=2}, + bevel/south west/.style = {/hex/bev=3}, + bevel/south east/.style = {/hex/bev=4}, + bevel/NW/.style = {/hex/bev=1}, + bevel/NE/.style = {/hex/bev=2}, + bevel/SW/.style = {/hex/bev=3}, + bevel/SE/.style = {/hex/bev=4}, + bevel/.default = {north west}, +} +\def\hex@bevel@frac{10} +\tikzset{ + hex/bevel highlight/.style={fill=white,opacity=.25}, + hex/bevel shadow/.style={fill=black,opacity=.25}, +} +% \end{macrocode} +% +% +% +% \begin{macrocode} +\newdimen\wg@tmpe +\newdimen\wg@tmpf +\newdimen\wg@tmpg +\def\hex@bevel@path#1{% + \scope[#1] + \wg@tmpe=\wg@tmpa\multiply\wg@tmpe by \hex@bevel@frac + \wg@tmpf=\wg@tmpb\multiply\wg@tmpf by \hex@bevel@frac + \wg@tmpg=\wg@tmpc\multiply\wg@tmpg by \hex@bevel@frac + \divide\wg@tmpe100 + \divide\wg@tmpf100 + \divide\wg@tmpg100 + % Start + \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Left + \pgfpathlineto{\pgfqpoint{-\wg@tmpa}{\wg@tmpb}}% + % Left-down + \pgfpathlineto{\pgfqpoint{\wg@tmpc}{\wg@tmpd}}% + % Right down + \wg@tmpa=-\wg@tmpa% + \wg@tmpb=-\wg@tmpb% + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Up, in + \advance\wg@tmpa\wg@tmpe% + \advance\wg@tmpb\wg@tmpf% + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Left-down, in + \advance\wg@tmpc-\wg@tmpg + \pgfpathlineto{\pgfqpoint{\wg@tmpc}{\wg@tmpd}}% + % Left, down in + \advance\wg@tmpb-\wg@tmpf\wg@tmpb-\wg@tmpb% + \advance\wg@tmpb-\wg@tmpf + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % Start, down in + \advance\wg@tmpa-\wg@tmpe\wg@tmpa-\wg@tmpa% + \advance\wg@tmpa-\wg@tmpe + \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}% + % % + \pgfclosepath% + \pgfusepath{fill} + \endscope}% +% \end{macrocode} +% +% +% +% \begin{macrocode} \hex@dbg{5}{Base vertex: \hex@xx,\hex@yy} \hex@dbg{5}{Base edges: \hex@e@xx,\hex@e@yy} \pgfdeclareshape{hex/hex}{% @@ -187,6 +258,40 @@ % \begin{macrocode} \@ifundefined{hex@label}{\let\hex@label\empty}{} \ifx\hex@label\empty\else\hex@do@label\fi% +% \end{macrocode} +% +% \begin{macrocode} + \@ifundefined{hex@bevel}{\let\hex@bevel\empty}{} + \ifx\hex@bevel\empty\else% + \northeast + \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% + \west + \wg@tmpc=\pgf@x\wg@tmpd=\pgf@y% + \ifcase\hex@bevel\relax + \or%1 + \or\wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%2 + \or\wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%3 + \or% 4 + \wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc% + \wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd% + \fi + \hex@bevel@path{chit/bevel highlight} + \northeast + \wg@tmpa=-\pgf@x\wg@tmpb=-\pgf@y% + \west + \wg@tmpc=-\pgf@x\wg@tmpd=-\pgf@y% + \ifcase\hex@bevel\relax + \or%1 + \or\wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%2 + \or\wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%3 + \or% 4 + \wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc% + \wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd% + \fi + \hex@bevel@path{chit/bevel shadow} + \fi +% \end{macrocode} +% \begin{macrocode} \endscope% % \end{macrocode} % diff --git a/macros/latex/contrib/wargame/source/hex/split.dtx b/macros/latex/contrib/wargame/source/hex/split.dtx new file mode 100644 index 0000000000..3996b8848f --- /dev/null +++ b/macros/latex/contrib/wargame/source/hex/split.dtx @@ -0,0 +1,517 @@ +% \iffalse +% -------------------------------------------------------------------- +%<*hex> +% \fi +% +% \subsubsection{Board splitting} +% \label{sec:impl:hex:split} +% +% \begin{Macro}{\splitboard} +% +% Calculates how to split a board into sheets of paper. +% +% \begin{Syntax} +% \cs{splitboard}\oarg{options} +% \end{Syntax} +% +% where options are +% +% \begin{itemize} +% \item \spec{paper}=\meta{format}: Specifies the paper format. One +% of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default +% is \spec{a4}. +% \item \spec{landscape}: Sets the paper format to be in landscape +% mode (default is portrait). +% \item \spec{margin}=\meta{size in centimetres}: Size of margins on +% each sheet in centimetres \emph{without} unit. That is put +% \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}. +% This should be \emph{slightly} larger (by roughly 5\%) than the +% \emph{least} margin required by the printer used. \emph{Must} be +% given \emph{before} \spec{paper} to have any effect. +% \item \spec{ncol}=\meta{number of columns}: Sets the number of +% columns of sheets. +% \item \spec{nrow}=\meta{number of rows}: Set the number of rows of +% sheets. +% \item \spec{overlap}=\meta{size in centimetres}: Sets the size of +% the overlap between sheets in centimetres \emph{without} unit. +% That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is +% \spec{2}. +% \item \spec{image}=\meta{file name}: File name of the board image +% (a PDF). Default is \spec{board} +% \item \spec{output}=\meta{file name}: File name (without +% \spec{.tex} ending) to write calculated split to. +% \item \spec{standalone}: Boolean flag. If true, then output file +% will be a standalone document (i.e., has a \cs{documentclass}). +% \item \spec{scale}=\meta{scale}: Set scale of board. +% \end{itemize} +% +% The macro will produce a file named \cs{jobname}\spec{\_out.tex} +% which can be included in another document to generate the split +% board PDF. +% +% To use, make, for example, the file \spec{calcsplit.tex} with the +% content +% +% \begin{verbatim} +% \documentclass[11pt]{standalone} +% \usepackage{wargame} +% \usepackage{mystyle} +% \begin{document} +% \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1} +% \end{document} +% \end{verbatim} +% +% to calculate the split of \spec{board.pdf} over $2\times2$ letter +% paper sheets, with a non-printable margin of 7mm, and an overlap +% between the segments of 1cm. +% +% The final split document can then be +% +% \begin{verbatim} +% \documentclass[11pt]{article} +% \usepackage[letterpaper,margin=7mm]{geometry} +% \begin{document} +% \input{calcsplit_out} +% \end{document} +% \end{verbatim} +% +% +% If you need to scale down the board, define the style \spec{board +% scale}. E.g., +% +% \begin{verbatim} +% \tikzset{board scale/.style={scale=.9}} +% \end{verbatim} +% +% \end{Macro} +% +% +% Styles used for drawing things. +% +% \begin{macrocode} +\tikzset{% + % Margin must be <1cm + split/paper outline/.style={ + shape=rectangle, + draw=red!50!black, + line width=.5mm}, + split/effective outline/.style={ + shape=rectangle, + draw=green!50!black, + dashed, + line width=.5mm}, + split/board outline/.style={% + draw=magenta, + line width=.5mm, + dotted}, +} +% \end{macrocode} +% +% A scratch dimension used +% +% \begin{macrocode} +\newdimen\split@tmp +% \end{macrocode} +% +% Get upper right and lower left corners of node. Argument is node name. +% +% \begin{macrocode} +\def\split@getem#1{% + \draw (#1.north east);% + \pgfgetlastxy{\split@ulx}{\split@uly}% + \xdef\split@ulx{\split@ulx}% + \xdef\split@ulx{\split@ulx}% + \draw (#1.south west);% + \pgfgetlastxy{\split@lrx}{\split@lry}% + \xdef\split@lrx{\split@lrx}% + \xdef\split@lry{\split@lry}% +} +% \end{macrocode} +% +% Get board dimensions. Argument is node name. +% +% \begin{macrocode} +\def\split@getboard#1{% + \split@getem{#1}% + \xdef\split@bulx{\split@ulx}% + \xdef\split@buly{\split@uly}% + \xdef\split@blrx{\split@lrx}% + \xdef\split@blry{\split@lry}% + \split@w{\@percentchar\space Board: + (\split@bulx,\split@buly)(\split@blrx,\split@blry)}} +% \end{macrocode} +% +% Adjust placement of markers and cut lines. +% \begin{enumerate} +% \item Dimension to adjust +% \item Overlap dimension (with units) +% \end{enumerate} +% +% \begin{macrocode} +\def\split@adj#1#2{% + \split@tmp=#2% + \divide\split@tmp by 2% + \advance\split@tmp by #1% + \edef\t{\the\split@tmp}} +% \end{macrocode} +% +% Get initial offset in a direction. +% +% \begin{enumerate} +% \item Number of segments in direction +% \item Overlap in centimetres (without unit) +% \item Effective size, in centimetres (without unit), of sheets in +% direction +% \item Full size, in centimetres (without unit), of board in +% direction. +% \end{enumerate} +% +% \begin{macrocode} +\def\split@get@init#1#2#3#4{% + \pgfmathparse{((#1 * #3 - (#1 - 1) * #2) - #4)/2}% + \xdef\split@off{\pgfmathresult}% + \hex@dbg{2}{((#1 * #3 - (#1 - 1) * #2) - #4)/2 -> `\split@off'}} +% \end{macrocode} +% +% Get initial offset of first segment. +% +% \begin{enumerate} +% \item Number of rows +% \item Number of columns +% \item Overlap in centimetres (without unit) +% \item Effective height, in centimetres (without unit), of sheets +% \item Effective width, in centimetres (without unit), of sheets +% \item Full height, in centimetres (without unit), of board +% \item Full width, in centimetres (without unit), of board +% \end{enumerate} +% +% \begin{macrocode} +\def\split@getinit#1#2#3#4#5#6#7{% + \split@get@init{#1}{#3}{#4}{#6}\xdef\dy{\split@off cm} + \split@get@init{#2}{#3}{#5}{#7}\xdef\dx{\split@off cm}} +% \end{macrocode} +% +% Get coordinates of a segment +% +% \begin{enumerate} +% \item Column number +% \item Row number +% \item Overlap, in centimetres (without unit) +% \end{enumerate} +% +% \begin{macrocode} +\def\split@getcoords#1#2#3{% + \hex@dbg{2}{Getting coords `c#1r#2'}% + \split@getem{c#1r#2}% + \edef\sulx{\split@ulx}% + \edef\suly{\split@uly}% + \edef\slrx{\split@lrx}% + \edef\slry{\split@lry}% + \edef\mlx{\split@blrx}% + \edef\mrx{\split@bulx}% + \edef\mty{\split@buly}% + \edef\mby{\split@blry}% + \pgfmathparse{int(#1-1)}\edef\pc{\pgfmathresult}% + \pgfmathparse{int(#2-1)}\edef\pr{\pgfmathresult}% + \pgfmathparse{int(#1+1)}\edef\nc{\pgfmathresult}% + \pgfmathparse{int(#2+1)}\edef\nr{\pgfmathresult}% + \pgfutil@ifundefined{pgf@sh@ns@c\pc r#2}{}{% Left + \hex@dbg{3}{\space Getting left `c\pc r#2'}% + \split@getem{c\pc r#2}\split@adj{\split@ulx}{-#3}\edef\mlx{\t}}% + \pgfutil@ifundefined{pgf@sh@ns@c\nc r#2}{}{% Right + \hex@dbg{3}{\space Getting right `c\nc r#2'}% + \split@getem{c\nc r#2}\split@adj{\split@lrx}{#3}\edef\mrx{\t}}% + \pgfutil@ifundefined{pgf@sh@ns@c#1r\pr}{}{% Above + \hex@dbg{3}{\space Getting above `c#1 r\pr'}% + \split@getem{c#1r\pr}\split@adj{\split@lry}{#3} \edef\mty{\t}}% + \pgfutil@ifundefined{pgf@sh@ns@c#1r\nr}{}{% Below + \hex@dbg{3}{\space Getting below `c#1 r\nr'}% + \split@getem{c#1r\nr}\split@adj{\split@uly}{-#3}\edef\mby{\t}}% + \draw[fill=red] (\mlx,\mty) circle(.2);% + \draw[fill=green](\mrx,\mty) circle(.4);% + \draw[fill=blue] (\mlx,\mby) circle(.6);% + \draw[fill=cyan] (\mrx,\mby) circle(.8);% + \split@w{% + \@percentchar^^J% + \string\segment(\sulx,\suly)(\slrx,\slry){\mlx}{\mrx}{\mby}{\mty} + \@percentchar\space c#1r#2} +} +% \end{macrocode} +% +% Stream to write to +% +% \begin{macrocode} +\newwrite\split@calcout +% \end{macrocode} +% +% Short-hand for write outs. +% +% \begin{macrocode} +\def\split@w{\immediate\write\split@calcout} +% \end{macrocode} +% +% Open stream and set-up +% +% \begin{macrocode} +\def\split@header#1{% + \immediate\openout\split@calcout=#1.tex + \ifsplit@standalone + \pgfmathparse{\split@margin*.95}\edef\tmp{\pgfmathresult} + \split@w{\@percentchar\@percentchar\space These are made with + `calcsplit' with `-jobname \jobname'} + \split@w{ + ^^J\string\documentclass[twoside]{article} + ^^J\string\usepackage{geometry} + ^^J\string\geometry{papersize={\the\paperwidth,\the\paperheight},margin=\tmp cm} + ^^J\string\usepackage{wargame} + ^^J\string\setlength{\string\parindent}{0pt} + ^^J\string\setlength{\string\parskip}{0pt} + ^^J\string\begin{document} + ^^J\string\ignorespaces\@percentchar} + \fi + \split@w{\string\def\string\boardfile{\split@img}\@percentchar} + \split@w{\string\def\string\boardscale{\split@scale}\@percentchar} +} +% \end{macrocode} +% +% Write final stuff and close stream +% +% \begin{macrocode} +\def\split@footer{% + \ifsplit@standalone + \split@w{^^J\string\end{document}} + \fi + \split@w{^^J\@percentchar\@percentchar End of `\jobname'^^J} + \immediate\closeout\split@calcout +} +% \end{macrocode} +% +% Initial calculations. This draws the board and then extracts the +% dimensions of the board. It also defines some styles for drawing +% the board segments. +% +% \begin{macrocode} +\def\split@init#1{% + \node[scale=\split@scale, + inner sep=0pt, + outer sep=0pt, + anchor=north west, + transform shape](b){\includegraphics{#1}}; + \split@getboard{b} + %x + \split@tmp=\split@blrx cm\advance\split@tmp by -\split@bulx% + \wg@pt@to@cm{\split@tmp}\edef\split@bw{\pgfmathresult}% + \pgfmathparse{abs(\split@bw)}\edef\split@bw{\pgfmathresult}% + % + \split@tmp=\split@buly cm\advance\split@tmp by -\split@blry% + \wg@pt@to@cm{\split@tmp}\edef\split@bh{\pgfmathresult}% + \pgfmathparse{abs(\split@bh)}\edef\split@bh{\pgfmathresult}% + % + \wg@pt@to@cm{\paperwidth}\edef\split@pw{\pgfmathresult}% + \wg@pt@to@cm{\paperheight}\edef\split@ph{\pgfmathresult}% + % + \wg@pt@to@cm{\textwidth}\edef\split@ew{\pgfmathresult}% + \wg@pt@to@cm{\textheight}\edef\split@eh{\pgfmathresult}% + % + \hex@dbg{1}{Board: + (\split@bulx,\split@buly)(\split@blrx,\split@blry) \split@bw x\split@bh + ^^JPaper: \split@pw x\split@ph + ^^JEffective: \split@ew x\split@eh + } + \tikzset{ + split/paper size/.style={ + shape=rectangle, + minimum width=\paperwidth, + minimum height=\paperheight, + split/paper outline, + }, + split/effective size/.style={ + shape=rectangle, + minimum width=\textwidth, + minimum height=\textheight, + split/effective outline}, + split/board size/.style={ + shape=rectangle, + minimum width=\split@bw cm, + minimum height=\split@bh cm, + split/board outline}} + \node[board/.try,split/board size,anchor=north west] {}; +} +% \end{macrocode} +% +% Calculate effective sheet sizes from sheet dimensions and the +% defined margin. +% +% \begin{macrocode} +\def\split@text@dim#1{% + \textwidth=\paperwidth% + \textheight=\paperheight% + \advance\textwidth by -#1cm% + \advance\textwidth by -#1cm% + \advance\textheight by -#1cm% + \advance\textheight by -#1cm% + \global\textwidth=\textwidth% + \global\textheight=\textheight% +} +% \end{macrocode} +% +% Options for the \cs{splitboard} macro. +% +% \begin{macrocode} +\newif\ifsplit@standalone\split@standalonetrue +\tikzset{% + split/.search also={/tikz},% + split/.cd,% + margin/.store in=\split@margin, + paper/.is choice,% + paper/a4/.code={% + \hex@dbg{3}{A4 paper for split}% + \global\paperwidth=21cm% + \global\paperheight=29.7cm% + \split@text@dim{\split@margin}}, + paper/a3/.code={% + \hex@dbg{3}{A3 paper for split}% + \global\paperheight=42cm% + \global\paperwidth=29.7cm% + \split@text@dim{\split@margin}}, + paper/letter/.code={% + \hex@dbg{3}{Letter paper for split} + \paperheight=27.9cm,% + \paperwidth=21.6cm,% + \split@text@dim{\split@margin}},% + paper/tabloid/.code={% + \hex@dbg{3}{Tabloid paper for split}% + \paperheight=43.2cm,% + \paperwidth=27.9cm,% + \split@text@dim{\split@margin}}, + landscape/.code={% + \hex@dbg{3}{Landscape option for split} + \split@tmp=\paperheight + \global\paperheight=\paperwidth + \global\paperwidth=\split@tmp + \split@tmp=\textheight + \global\textheight=\textwidth + \global\textwidth=\split@tmp}, + standalone/.is if=split@standalone, + scale/.store in=\split@scale, + output/.store in=\split@out, + ncol/.store in=\split@ncol, + nrow/.store in=\split@nrow, + overlap/.store in=\split@ov, % Centimeter, no unit + image/.store in=\split@img, + paper/.default=a4, paper/.initial=a4, + margin/.default=.6, margin/.initial=.6, + ncol/.default=0, ncol/.initial=0, + nrow/.default=0, nrow/.initial=0, + overlap/.default=2, overlap/.initial=2, + image/.default=board, image/.initial=board, + output/.default=\jobname_out, + standalone/.default=true, + scale/.default=1, +} +% \end{macrocode} +% +% The actual macro. The argument is key-value pairs of options. +% +% \begin{macrocode} +\def\splitboard#1{% + \pgfkeys{/tikz/split/.cd,% + standalone,% + output,% + margin,% + paper,% + image,% + overlap,% + scale,% + ncol,% + nrow,% + #1} + \hex@dbg{1}{% + Paper: `\the\paperwidth'x`\the\paperheight' + ^^JEffective: `\the\textwidth'x`\the\textheight' + ^^JNcols: `\split@ncol' + ^^JNrows: `\split@nrow' + ^^JOverlap: `\split@ov' cm} + \split@header{\split@out} + \begin{tikzpicture} + \split@init{\split@img} + \split@getinit{% + \split@nrow}{% + \split@ncol}{% + \split@ov}{\split@eh}{\split@ew}{\split@bh}{\split@bw} + \node[split/effective size, + above left=\dy and \dx of b.north west, + anchor=north west] (c1r1) {}; + \node[split/paper size] at (c1r1) {}; + % + \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split@nrow}{% + \ifnum\r>1 + \hex@dbg{3}{Placing first column of row `\r'} + \node[split/effective size, + below=-\split@ov cm of c1r\pr.south west,anchor=north west] (c1r\r){}; + \node[split/paper size] at (c1r\r) {}; + \fi + \foreach \c [remember=\c as \pc (initially 1)] in {2,...,\split@ncol}{% + \ifnum\c>\split@ncol\else% + \ifnum\c>\pc + \hex@dbg{3}{Placing column `\c' (`\pc') of row `\r'} + \node[split/effective size, + right=-\split@ov cm of c\pc r\r.north east,anchor=north west] + (c\c r\r) {}; + \node[split/paper size] at (c\c r\r) {}; + \fi + \fi + } + } + \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split@nrow}{% + \foreach \c [remember=\c as \pc (initially 0)] in {1,...,\split@ncol}{% + \split@getcoords{\c}{\r}{\split@ov cm}}} + \end{tikzpicture} + \split@footer +} +% \end{macrocode} +% +% Macro used by the written file. +% +% +% \begin{enumerate} +% \item first coordinate (e.g., \spec{(hex ak:c=C,r=17)}) +% \item second coordinate (e.g., \spec{(hex ak:c=M,r=33)}) +% \item Crop mark left +% \item Crop mark right +% \item Crop mark bottom +% \item Crop mark top +% \end{enumerate} +% +% \begin{macrocode} +\def\segment(#1)(#2)#3#4#5#6{% + \begin{tikzpicture}% + \begin{scope} + \clip (#1) rectangle (#2); + \node[scale=\boardscale, + inner sep=0pt, + outer sep=0pt, + anchor=north west, + transform shape]{\includegraphics{\boardfile}}; + \end{scope} + \pgfinterruptboundingbox + \draw(#3,#6)--++( 0.0, 0.3); + \draw(#3,#6)--++(-0.3, 0.0); + \draw(#3,#5)--++( 0.0,-0.3); + \draw(#3,#5)--++(-0.3, 0.0); + \draw(#4,#6)--++( 0.0, 0.3); + \draw(#4,#6)--++( 0.3, 0.0); + \draw(#4,#5)--++( 0.0,-0.3); + \draw(#4,#5)--++( 0.3, 0.0); + \endpgfinterruptboundingbox + \end{tikzpicture}% + \cleardoublepage}% +% \end{macrocode} +% +% +% +% +% \iffalse +% +% -------------------------------------------------------------------- +% \fi diff --git a/macros/latex/contrib/wargame/source/hex/terrain.dtx b/macros/latex/contrib/wargame/source/hex/terrain.dtx index 42155ca5c9..4c486adf77 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain.dtx @@ -47,7 +47,7 @@ pic/.default=, image/.default=, code/.default=, - clip/.default, + clip/.default=, } \iffalse \tikzset{ @@ -111,8 +111,9 @@ % \begin{macrocode} \@ifundefined{hex@t@clip}{\let\hex@t@clip\empty}{} \ifx\hex@t@clip\empty\else% + \edef\hex@t@cc{\hex@t@clip}% \def\hex@t@c{} - \foreach \c in \hex@t@clip{% + \foreach \c in \hex@t@cc{% \hex@dbg{5}{Clipping to `\c'} \expandafter\wg@pic\c\@endwg@pic {}{\wg@tmpa,\wg@tmpb}{% save path=\hex@t@tmp}% @@ -132,8 +133,13 @@ %% macros are undefined, define them to be empty \@ifundefined{hex@t@pic}{\let\hex@t@pic\empty}{} \@ifundefined{hex@t@image}{\let\hex@t@image\empty}{} + \@ifundefined{hex@t@code}{\let\hex@t@code\empty}{} + \@ifundefined{hex@t@code}{\let\hex@t@code\empty}{} % \end{macrocode} % +% \begin{macrocode} + \ifx\hex@t@code\empty\else\hex@t@code\fi% +% \end{macrocode} % First we check if we have not got terrain images, but terrain % pictures. If we have that, we process these in turn. Note, the % user can give options to each terrain picture by preceding the @@ -147,7 +153,8 @@ % We have pictures \hex@dbg{5}{Terrain pictures}% \pgfpointorigin\wg@tmpa=\pgf@x\wg@tmpb=\pgf@y% - \wg@pic@all{\hex@t@pic}{}{\the\wg@tmpa,\the\wg@tmpb}{}% + \foreach \i in \hex@t@pic{% + \wg@pic@all{\i}{}{\the\wg@tmpa,\the\wg@tmpb}{}}% \fi% We have pictures. % \end{macrocode} % diff --git a/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx b/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx index 98e990c15c..a6a6905e1a 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx @@ -21,10 +21,14 @@ % \end{macrocode} % \end{TikzKey} % -% Now for the actual patterns. We go in the same order as above --- -% i.e, we start with the beach pattern. This is rather long. -% % \begin{TikzKey}{hex/terrain/beach} +% Now for the actual patterns. We go in the same order as above --- +% i.e, we start with the beach pattern. This is rather long. +% +% \begin{center} +% \includegraphics{wargame.beach} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/terrain/city.dtx b/macros/latex/contrib/wargame/source/hex/terrain/city.dtx index 1d373ea19d..0f5f88eea6 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/city.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/city.dtx @@ -5,6 +5,9 @@ % \begin{TikzKey}{hex/terrain/city} % And finally a city % +% \begin{center} +% \includegraphics{wargame.city} +% \end{center} % % \begin{macrocode} \ifhex@terrain@pic diff --git a/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx b/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx index ee15f5ce78..ededa87541 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx @@ -19,6 +19,10 @@ % \begin{TikzKey}{hex/terrain/light woods} % Next, we have light woods. % +% \begin{center} +% \includegraphics{wargame.light_woods} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx b/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx index 04f935bd92..f80b991e27 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx @@ -21,6 +21,10 @@ % And the mountains pattern. This is the same as the beach pattern, % only filled with a darker brown colour. % +% \begin{center} +% \includegraphics{wargame.mountains} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx b/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx index fe716add2f..a5fd0d6a06 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx @@ -19,6 +19,10 @@ % \begin{TikzKey}{hex/terrain/rough} % Roughs. Again, a bit long. % +% \begin{center} +% \includegraphics{wargame.rough} +% \end{center} +% % % \begin{macrocode} \ifhex@terrain@pic diff --git a/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx b/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx index b361789fa3..57ad7e9e65 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx @@ -18,6 +18,10 @@ % \begin{TikzKey}{hex/terrain/swamp} % Swamps. This is probably the shortest of the terrain patterns. % +% \begin{center} +% \includegraphics{wargame.swamp} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/terrain/town.dtx b/macros/latex/contrib/wargame/source/hex/terrain/town.dtx index 1186b54c81..5956583ba6 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/town.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/town.dtx @@ -5,6 +5,10 @@ % \begin{TikzKey}{hex/terrain/town} % A town. % +% \begin{center} +% \includegraphics{wargame.town} +% \end{center} +% % % \begin{macrocode} \ifhex@terrain@pic diff --git a/macros/latex/contrib/wargame/source/hex/terrain/village.dtx b/macros/latex/contrib/wargame/source/hex/terrain/village.dtx index 837b92642c..8aeddb2b9d 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/village.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/village.dtx @@ -9,6 +9,10 @@ % houses, and separate styles for regular and small roads. Note % that we draw using the stroke colour for roads and houses. % +% \begin{center} +% \includegraphics{wargame.village} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx b/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx index 9b0d08499b..a8faccddf3 100644 --- a/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx +++ b/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx @@ -19,6 +19,10 @@ % \begin{TikzKey}{hex/terrain/woods} % Regular woods. % +% \begin{center} +% \includegraphics{wargame.woods} +% \end{center} +% % \begin{macrocode} \ifhex@terrain@pic \tikzset{ diff --git a/macros/latex/contrib/wargame/source/hex/towns.dtx b/macros/latex/contrib/wargame/source/hex/towns.dtx index a73c5eed5c..222b5f58da 100644 --- a/macros/latex/contrib/wargame/source/hex/towns.dtx +++ b/macros/latex/contrib/wargame/source/hex/towns.dtx @@ -13,6 +13,7 @@ \tikzset{% hex/town/.style={ scale line widths, + solid, thin, fill=pgfstrokecolor, color=pgfstrokecolor}, @@ -21,7 +22,7 @@ shape=rectangle, above right=.1, color=pgfstrokecolor, - font=\sffamily\normalsize} + font=\sffamily\fontsize{11}{13}\selectfont} } % \end{macrocode} % @@ -46,11 +47,11 @@ % % \begin{macrocode} \tikzset{% - hex/town/village/.pic={\path[fill,pic actions] circle(.1);}, - hex/town/town/.pic={\path[fill,pic actions] circle(.2);}, + hex/town/village/.pic={\path[fill,solid,pic actions] circle(.1);}, + hex/town/town/.pic={\path[fill,solid,pic actions] circle(.2);}, hex/town/city/.pic={% - \path[fill,pic actions] circle(.25); - \path[draw,pic actions] circle(.35);} + \path[fill,solid,pic actions] circle(.25); + \path[draw,solid,pic actions] circle(.35);} } % \end{macrocode} % diff --git a/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx b/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx index d8d0c4fba7..22cb99cc97 100644 --- a/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx +++ b/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx @@ -419,6 +419,30 @@ } } % \end{macrocode} +% +% \begin{macrocode} +\pgfdeclareshape{natoapp6c friendly none}{% + \inheritsavedanchors[from=natoapp6c base] + \savedanchor\northeast{% + \pgf@x=1.5\n@to@pp@r% + \pgf@y=\n@to@pp@r} + \anchor{north east}{\northeast} + \anchor{north west}{\northeast\pgf@x=-\pgf@x} + \anchor{south east}{\northeast\pgf@y=-\pgf@y} + \anchor{south west}{\northeast\pgf@x=-\pgf@x\pgf@y=-\pgf@y} + \anchor{north}{\northeast\pgf@x=0cm} + \anchor{south}{\northeast\pgf@x=0cm\pgf@y=-\pgf@y} + \anchor{east}{\northeast\pgf@y=0cm} + \anchor{west}{\northeast\pgf@x=-\pgf@x\pgf@y=0cm} + \inheritanchor[from=natoapp6c base]{upper} + \inheritanchor[from=natoapp6c base]{lower} + \inheritanchor[from=natoapp6c base]{left} + \inheritanchor[from=natoapp6c base]{right} + \inheritanchor[from=natoapp6c base]{center} + \backgroundpath{} + \behindforegroundpath{} +} +% \end{macrocode} % \end{NatoAppFrame} % % \iffalse diff --git a/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx b/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx index 029be911b7..3e6d41821d 100644 --- a/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx +++ b/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx @@ -81,6 +81,7 @@ command/space/.style={cmd=space}, command/sub surface/.style={cmd=sub surface}, command/sea mine/.style={cmd=sub surface}, + command/none/.style={cmd=none}, } % \end{macrocode} % \end{NatoAppKey} @@ -164,7 +165,11 @@ % % \begin{macrocode} \tikzset{ - natoapp6c/parts/.style={draw,shape=rectangle,transform shape}, + natoapp6c/parts/.style={ + scale line widths, + draw, + shape=rectangle, + transform shape}, natoapp6c/main/.style={natoapp6c/parts}, natoapp6c/modifiers/.style={natoapp6c/parts,scale=.6}, natoapp6c/lower/.style={natoapp6c/parts}, @@ -607,13 +612,21 @@ % % \begin{macrocode} \providecommand\natoappmark[2][]{% - \tikz[scale=.25,#1]{\natoapp[faction=friendly,command=land,main=#2]}} + \tikz[transform shape, + scale=.25, + baseline=(natoapp6c mark.south east), + natoapp6c mark/.try, + #1]{%1 + \node[draw,transform + shape,natoapp6c={faction=friendly,command=land, + main=#2}] (natoapp6c mark){}}} +% \natoapp[faction=friendly,command=land,main=#2](0,0)(natoapp6c mark)}} % \end{macrocode} % \end{Macro} % % \begin{Macro}{\echelonmark} % \begin{macrocode} -\providecommand\echelonmark[2][]{\tikz[scale=.5,#1]{% +\providecommand\echelonmark[2][]{\tikz[transform shape,scale=.5,#1]{% \pic[scale line widths,line width=1pt] {natoapp6c/s/echelon=#2};}} % \end{macrocode} % \end{Macro} diff --git a/macros/latex/contrib/wargame/source/util/bb.dtx b/macros/latex/contrib/wargame/source/util/bb.dtx new file mode 100644 index 0000000000..83e72da7a9 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/bb.dtx @@ -0,0 +1,226 @@ +% \iffalse +% -------------------------------------------------------------------- +%<*utils> +% \fi +% +% \subsubsection{Bounding boxes} +% +% Bounding box dimensions +% +% +% \begin{macrocode} +\newdimen\wg@bb@minx +\newdimen\wg@bb@miny +\newdimen\wg@bb@maxx +\newdimen\wg@bb@maxy +% \end{macrocode} +% +% +% +% Enable or disable bounding box tracking +% +% \begin{macrocode} +\newif\ifwg@notrelevantforpathsize\wg@notrelevantforpathsizefalse +% \end{macrocode} +% +% +% \begin{Macro}{wg@resetbb} +% Reset the bounding box tracking dimensions +% +% \begin{macrocode} +\def\wg@resetbb{% + \global\wg@bb@minx=16000pt\relax% + \global\wg@bb@miny=16000pt\relax% + \global\wg@bb@maxx=-16000pt\relax% + \global\wg@bb@maxy=-16000pt\relax% +} +% \end{macrocode} +% \end{Macro} +% +% \begin{Macro}{\old@pgf@protocolsize} +% Save PGF's bounding box algorithm +% +% \begin{macrocode} +\let\old@pgf@protocolsize\pgf@protocolsizes +% \end{macrocode} +% \end{Macro} +% +% \begin{Macro}{\wg@protocolsizes} +% Our bounding box algorithm +% +% \begin{macrocode} +\def\wg@protocolsizes#1#2{% + \old@pgf@protocolsize{#1}{#2} + \ifwg@notrelevantforpathsize\else% + \ifdim#1<\wg@bb@minx\global\wg@bb@minx#1\fi% + \ifdim#1>\wg@bb@maxx\global\wg@bb@maxx#1\fi% + \ifdim#2<\wg@bb@miny\global\wg@bb@miny#2\fi% + \ifdim#2>\wg@bb@maxy\global\wg@bb@maxy#2\fi% + \fi +} +% \end{macrocode} +% \end{Macro} +% +% % \begin{environment}{getbbl} +% Environment that tracks the local bounding box +% +% \begin{macrocode} +\newenvironment{getbbl}{% + \wg@resetbb% + \wg@notrelevantforpathsizefalse% + \global\let\pgf@protocolsizes\wg@protocolsizes}{% + \gdef\pgf@sh@ns@L{rectangle} + \gdef\pgf@sh@np@L{% + \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}% + \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}% + } + \gdef\pgf@sh@nt@L{{1}{0}{0}{1}{0pt}{0pt}} + \gdef\pgf@sh@pi@L{\pgfpictureid} + \global\let\pgf@protocolsizes\old@pgf@protocolsize +} +% \end{macrocode} +% \end{environment} +% +% \begin{environment}{getbb} +% Environment to track global bounding box +% +% \begin{macrocode} +\newenvironment{getbb}{% + \wg@resetbb% + \wg@notrelevantforpathsizefalse% + \global\let\pgf@protocolsizes\wg@protocolsizes}{% + \gdef\pgf@sh@ns@M{rectangle} + \gdef\pgf@sh@np@M{% + \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}% + \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}% + } + \gdef\pgf@sh@nt@M{{1}{0}{0}{1}{0pt}{0pt}} + % \pgfgettransform\pgf@temp% + % \xdef\pgf@sh@nt@M{\pgf@temp} + % \pgfgettransformentries{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{\pgf@temp}{\pgf@temp} + % \message{^^JTransform of M: \meaning\pgf@temp} + % \xdef\pgf@sh@nt@M{{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{0pt}{0pt}}% + % \message{^^JTransform of M: \meaning\pgf@sh@nt@M} + \gdef\pgf@sh@pi@M{\pgfpictureid} + \global\let\pgf@protocolsizes\old@pgf@protocolsize +} +% \end{macrocode} +% \end{environment} +% +% \iffalse +% -------------------------------------------------------------------- +% \fi +% +% \subsubsection{Some utilities to get bounding boxes and the like} +% +% +% All coordinates, and such are recorded in centimetres. It is worth +% remembering that the Tikz coordinate system has the $y$ axis point +% upward, while typical image software has the $y$ axis point down. +% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch) +% resolution. +% +% That means that scaling factor becomes +% +% $$ +% \frac{150\mathrm{pixel}}{2.54\mathrm{cm}} = +% 59.055\frac{\mathrm{pixel}}{\mathrm{cm}} +% $$ +% +% \iffalse Using definition in terms of printers feet - the one to +% use! +% +% PNG: 1674 x 1101 +% PDF: "lower left": [-0.02107,-0.02107], +% "upper right": [28.31705,18.60843] +% Width: 28.31705+0.02107 = 28.33812 +% Height: 18.60843+0.02107 = 18.62950 +% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166 +% 1101 / 18.62950 = 59.09981480984460130438 +% Average = 59.08609399739914809302 +% +% Calculated = 150 / 2.54 = 59.05511811023622047244 +% +% Using 1/72.27 +% +% "lower left": [-0.02109,-0.02109], +% "upper right": [28.321,18.61102] +% Width: 28.321+0.02109=28.34209 +% Height: 18.61102+0.02109=18.63211 +% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267 +% 1101 / 18.63211 = 59.09153606328000425072 +% Average = 59.07781736180584381169 +% \fi +% +% Since we want to write all dimensions in centimetres, we need to be +% able to convert \texttt{pt} dimensions to centimetres. We make two +% macros to do that for us. +% +% The exact definition of 1pt is +% +% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1" +% = 0.03513\overline{6}$$ +% +% \begin{macrocode} +% 2.54 / 72.27 = .03514598035145980351 +% \def\wg@pt@to@cm#1{\pgfmathparse{#1 * 0.0351460}} +\def\wg@pt@to@cm#1{\pgfmathparse{#1 * 0.0351367}} +\def\ptpoint@to@cm#1#2{% + \wg@pt@to@cm{#1}\edef\x{\pgfmathresult}% + \wg@pt@to@cm{#2}\edef\y{\pgfmathresult}} +% \end{macrocode} +% +% The next macro gets an anchors coordinates and stores them (in units +% of centimetres) in \cs{tmp@x} and \cs{tmp@y} +% +% \begin{macrocode} +\def\wg@get@nchor#1#2{% + \wg@dbg{2}{Get anchor coordinates #1.#2} + \pgfpointanchor{#1}{#2}% + \wg@dbg{2}{ `\the\pgf@x',`\the\pgf@y'} + \pgfgetlastxy\tmp@x\tmp@y% + \wg@dbg{2}{ `\tmp@x',`\tmp@y'} + \wg@pt@to@cm{\tmp@x}\edef\tmp@x{\pgfmathresult} + \wg@pt@to@cm{\tmp@y}\edef\tmp@y{\pgfmathresult} +} +% \end{macrocode} +% +% This does the same as above, but transform to the global coordinate +% system. +% +% \begin{macrocode} +\def\wg@get@global@nchor#1#2{% + \pgfpointanchor{#1}{#2}% + \pgfgetlastxy\tmp@x\tmp@y% + \pgfpointtransformed{\pgfpoint{\tmp@x}{\tmp@y}} + \pgf@xa=\pgf@x + \pgf@ya=\pgf@y + %% \message{^^JAnchor #1.#2 @ (\the\pgf@xa,\the\pgf@ya)} + \wg@pt@to@cm{\the\pgf@xa}\edef\tmp@x{\pgfmathresult} + \wg@pt@to@cm{\the\pgf@ya}\edef\tmp@y{\pgfmathresult} +} +% \end{macrocode} +% +% This records the bounding box given by a named node. The result is +% stored in the macros \cs{llx}, \cs{lly}, \cs{urx}, and \cs{ury}. +% +% \begin{macrocode} +\def\wg@get@bb#1{% + \wg@get@nchor{#1}{south west} + \edef\llx{\tmp@x} + \edef\lly{\tmp@y} + \wg@get@nchor{#1}{north east} + \edef\urx{\tmp@x} + \edef\ury{\tmp@y} +} +\def\wglogbb#1{% + \wg@get@bb{#1}% + \message{^^J`#1' BB: (\llx,\lly) x (\urx,\ury)^^J}} +% \end{macrocode} +% +% +% +% \iffalse +% -------------------------------------------------------------------- +% +%\fi diff --git a/macros/latex/contrib/wargame/source/util/compound.dtx b/macros/latex/contrib/wargame/source/util/compound.dtx new file mode 100644 index 0000000000..aca6800644 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/compound.dtx @@ -0,0 +1,242 @@ +% \iffalse +% -------------------------------------------------------------------- +%<*utils> +% \fi +% +% \subsubsection{Pictures in compound nodes} +% +% \begin{Macro}{\wg@pic} +% The macro \cs{wg@pic} will render a \texttt{pic}. This is used by +% the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node +% shapes extensively. +% +% The arguments are +% \begin{enumerate} +% \item Prefix +% \item Position +% \item Fixed options +% \item User options +% \item Picture. +% \end{enumerate} +% +% That is, the macro expects calls like +% \begin{Syntax} +% \cs{wg@pic}\oarg{options}\meta{picture}\cs{@endwg@pic}\marg{prefix}\marg{position}\marg{options} +% \end{Syntax} +% +% Note the \cs{@endwg@pic} at the end of the call to swallow up +% \meta{picture}. Typically this macro is used as +% +% \begin{Syntax} +% \cs{edef}\cs{args}\{\meta{something}\} +% \cs{expandafter}\cs{wg@pic}\cs{args}\cs{@endwg@pic}\marg{prefix}\parg{position}\marg{options} +% \end{Syntax} +% +% where \meta{something} typically expands to \oarg{user +% option}\meta{picture} +% +% First, the top-level macro \cs{wg@pic} that looks for user +% options. +% +% \begin{macrocode} +\def\wg@pic{% + \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%] +} +% \end{macrocode} +% +% This macro then forwards to \cs{wg@@pic} to gobble up +% \meta{picture}. +% +% \begin{enumerate} +% \item User options +% \item Arguments +% \end{enumerate} +% +% \begin{macrocode} +\def\wg@@pic[#1]#2\@endwg@pic{% + \wg@dbg{2}{Options: `#1', picture: `#2'}% + \wg@@@pic{#1}{#2}% +} +% \end{macrocode} +% +% \begin{enumerate} +% \item User options +% \item Arguments +% \item Prefix +% \item Coordinates +% \item Fixed options +% \end{enumerate} +% +% \begin{macrocode} +\def\wg@@@pic#1#2#3#4#5{% + \ifx|#2|\wg@dbg{3}{No picture given}% + \else% + \wg@dbg{3}{^^JWG Pic: + ^^J User options: #1 + ^^J Picture: #2 + ^^J Prefix: #3 + ^^J Coordinates: #4 + ^^J Fixed options: #5}% + % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}% + \pic[#5,#1] at (#4) {#3#2};% + \ifwg@s@ve% + \pgf@relevantforpicturesizetrue% + \begin{getbbl}% + \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};% + \end{getbbl}% + \wg@dbg{5}{Clipping to local bounding box}% + \clip (L.south west) rectangle (L.north east);% + \pgf@relevantforpicturesizefalse \global\wg@s@vefalse% + \fi + \fi% + \wg@dbg{3}{End of WG Pic} +} +% \end{macrocode} +% \end{Macro} +% +% +% \begin{Macro}{\wg@pic@all} +% +% This macro sets all pictures in a list. +% +% \begin{enumerate} +% \item List +% \item Prefix +% \item Position +% \item Styles +% \end{enumerate} +% +% +% \begin{macrocode} +\def\wg@pic@all#1#2#3#4{% + \wg@dbg{2}{WG picture loop + ^^J List: \meaning#1 + ^^J Prefix: `#2' + ^^J Position: `#3' + ^^J Styles: `#4'} + \foreach \p in #1{% + \wg@dbg{2}{WG picture element: \meaning\p}% + \expandafter\wg@pic\p\@endwg@pic {#2}{#3}{#4}% + }% +} +% \end{macrocode} +% \end{Macro} +% +% \iffalse +% -------------------------------------------------------------------- +% \fi +% +% \subsubsection{Nodes in compound nodes} +% +% \begin{Macro}{\wg@node} +% The macro \cs{wg@node} will render a \texttt{node}. This can be +% used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} +% node shapes. +% +% The arguments are +% \begin{enumerate} +% \item Prefix +% \item Position +% \item Fixed options +% \item User options +% \item Body. +% \end{enumerate} +% +% That is, the macro expects calls like +% \begin{Syntax} +% \cs{wg@node}\oarg{options}\meta{body}\cs{@endwg@node}\marg{prefix}\marg{position}\marg{options} +% \end{Syntax} +% +% Note the \cs{@endwg@node} at the end of the call to swallow up +% \meta{body}. Typically this macro is used as +% +% \begin{Syntax} +% \cs{edef}\cs{args}\{\meta{something}\} +% \cs{expandafter}\cs{wg@node}\cs{args}\cs{@endwg@node}\marg{prefix}\parg{position}\marg{options} +% \end{Syntax} +% +% where \meta{something} typically expands to \oarg{user +% option}\meta{body} +% +% First, the top-level macro \cs{wg@node} that looks for user +% options. +% +% \begin{macrocode} +\def\wg@node{% + \@ifnextchar[{\wg@@node}{\wg@@node[]}%] +} +% \end{macrocode} +% +% This macro then forwards to \cs{wg@@node} to gobble up +% \meta{body}. +% +% \begin{enumerate} +% \item User options +% \item Arguments +% \end{enumerate} +% +% \begin{macrocode} +\def\wg@@node[#1]#2\@endwg@node{% + \wg@dbg{2}{Options: `#1', body: `#2'}% + \wg@@@node{#1}{#2}% +} +% \end{macrocode} +% +% \begin{enumerate} +% \item User options +% \item Arguments +% \item Prefix +% \item Coordinates +% \item Fixed options +% \end{enumerate} +% +% \begin{macrocode} +\def\wg@@@node#1#2#3#4#5{% + \ifx|#2|\wg@dbg{3}{No body given}% + \else% + \wg@dbg{3}{^^JWG Pic: + ^^J User options: #1 + ^^J Body: #2 + ^^J Prefix: #3 + ^^J Coordinates: #4 + ^^J Fixed options: #5}% + % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}% + \node[#5,#1] at (#4) {#3#2};% + \fi% + \wg@dbg{3}{End of WG Node} +} +% \end{macrocode} +% \end{Macro} +% +% +% \begin{Macro}{\wg@node@all} +% +% This macro sets all pictures in a list. +% +% \begin{enumerate} +% \item List +% \item Prefix +% \item Position +% \item Styles +% \end{enumerate} +% +% +% \begin{macrocode} +\def\wg@node@all#1#2#3#4{% + \wg@dbg{2}{WG picture loop + ^^J List: \meaning#1 + ^^J Prefix: `#2' + ^^J Position: `#3' + ^^J Styles: `#4'} + \foreach \p in #1{% + \wg@dbg{2}{WG picture element: \meaning\p}% + \expandafter\wg@node\p\@endwg@node {#2}{#3}{#4}% + }% +} +% \end{macrocode} +% \end{Macro} +% +% \iffalse +% -------------------------------------------------------------------- +% +%\fi diff --git a/macros/latex/contrib/wargame/source/util/core.dtx b/macros/latex/contrib/wargame/source/util/core.dtx index 1d95aa38c6..8ee6f84c85 100644 --- a/macros/latex/contrib/wargame/source/util/core.dtx +++ b/macros/latex/contrib/wargame/source/util/core.dtx @@ -12,1342 +12,20 @@ %<*utils> %\fi % -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Miscellaneous macros} -% -% \begin{Macro}{\wg@dbg} -% Debugging support. The counter \cs{wargamedbglvl} sets the debug -% level. The package code then uses \cs{wg@dbg} to print out -% debugging messages. This macro takes two arguments --- the first -% is the \emph{least} debug level at which the message is printed, and -% the second is the message it self. -% -% \begin{macrocode} -\newcount\wargamedbglvl\wargamedbglvl=0 -\def\wg@dbg#1#2{% - \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi} -% \end{macrocode} -% \end{Macro} -% -% \begin{Macro}{\wg@addto@macro} -% -% The macro \cs{wg@addto@macro}\marg{macro}\marg{other} adds the -% definition of the macro \meta{other} to the macro \meta{macro}. -% This uses the \cs{toks} trick of storing the \emph{tokens} of the -% definition of a \meta{macro} and \meta{other} into \spec{@} and -% expanding that token into the definition of \meta{macro}. -% Effectively, this means that the top-level definition of -% \meta{macro} and \meta{other} are expanded (i.e., macros used in -% the definition of either macro is \emph{not} expanded) and then -% that becomes the new definition of \meta{macro}. -% -% We will use this macro to do \emph{shallow} definitions of macros -% to contain keys and such. -% -% \begin{macrocode} -\long\def\wg@addto@macro#1#2{% - \begingroup - \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}% - \xdef#1{\the\toks@}% - \endgroup} -% \end{macrocode} -% \end{Macro} -% -% -% \begin{Macro}{\wg@sub@nchor} -% Get anchor from sub node. We cannot use \cs{pgfpointanchor} since -% that returns the anchor coordinates in the global coordinate -% system. -% -% \begin{macrocode} -\def\wg@sub@nchor#1#2{% - \wg@dbg{3}{^^JGet `#2' in `#1'}% - \@ifundefined{pgf@sh@ns@#1}{% - \pgf@x=0cm\pgf@y=0cm}{% - \pgf@process{% - \csname pgf@sh@ma@#1\endcsname% MW - \csname pgf@sh@np@#1\endcsname% - \pgf@sh@reanchor{\csname pgf@sh@ns@#1\endcsname}{#2}}}% - \wg@dbg{10}{-> \the\pgf@x,\the\pgf@y}% -} -% \end{macrocode} -% \end{Macro} -% -% -% Scratch dimensions -% -% \begin{macrocode} -\newdimen\wg@tmpa -\newdimen\wg@tmpb -\newdimen\wg@tmpc -\newdimen\wg@tmpd -% \end{macrocode} -% -% -% Macro to easy restore a saved path -% -% \begin{macrocode} -\def\settosave#1{ - \pgfsyssoftpath@setcurrentpath{#1}} -% \end{macrocode} -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Pictures in compound nodes} -% -% \begin{Macro}{\wg@pic} -% The macro \cs{wg@pic} will render a \texttt{pic}. This is used by -% the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node -% shapes extensively. -% -% The arguments are -% \begin{enumerate} -% \item Prefix -% \item Position -% \item Fixed options -% \item User options -% \item Picture. -% \end{enumerate} -% -% That is, the macro expects calls like -% \begin{Syntax} -% \cs{wg@pic}\oarg{options}\meta{picture}\cs{@endwg@pic}\marg{prefix}\marg{position}\marg{options} -% \end{Syntax} -% -% Note the \cs{@endwg@pic} at the end of the call to swallow up -% \meta{picture}. Typically this macro is used as -% -% \begin{Syntax} -% \cs{edef}\cs{args}\{\meta{something}\} -% \cs{expandafter}\cs{wg@pic}\cs{args}\cs{@endwg@pic}\marg{prefix}\parg{position}\marg{options} -% \end{Syntax} -% -% where \meta{something} typically expands to \oarg{user -% option}\meta{picture} -% -% First, the top-level macro \cs{wg@pic} that looks for user -% options. -% -% \begin{macrocode} -\def\wg@pic{% - \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%] -} -% \end{macrocode} -% -% This macro then forwards to \cs{wg@@pic} to gobble up -% \meta{picture}. -% -% \begin{enumerate} -% \item User options -% \item Arguments -% \end{enumerate} -% -% \begin{macrocode} -\def\wg@@pic[#1]#2\@endwg@pic{% - \wg@dbg{2}{Options: `#1', picture: `#2'}% - \wg@@@pic{#1}{#2}% -} -% \end{macrocode} -% -% \begin{enumerate} -% \item User options -% \item Arguments -% \item Prefix -% \item Coordinates -% \item Fixed options -% \end{enumerate} -% -% \begin{macrocode} -\def\wg@@@pic#1#2#3#4#5{% - \ifx|#2|\wg@dbg{3}{No picture given}% - \else% - \wg@dbg{3}{^^JWG Pic: - ^^J User options: #1 - ^^J Picture: #2 - ^^J Prefix: #3 - ^^J Coordinates: #4 - ^^J Fixed options: #5}% - % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}% - \pic[#5,#1] at (#4) {#3#2};% - \ifwg@s@ve% - \pgf@relevantforpicturesizetrue% - \begin{getbbl}% - \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};% - \end{getbbl}% - \wg@dbg{5}{Clipping to local bounding box}% - \clip (L.south west) rectangle (L.north east);% - \pgf@relevantforpicturesizefalse \global\wg@s@vefalse% - \fi - \fi% - \wg@dbg{3}{End of WG Pic} -} -% \end{macrocode} -% \end{Macro} -% -% -% \begin{Macro}{\wg@pic@all} -% -% This macro sets all pictures in a list. -% -% \begin{enumerate} -% \item List -% \item Prefix -% \item Position -% \item Styles -% \end{enumerate} -% -% -% \begin{macrocode} -\def\wg@pic@all#1#2#3#4{% - \wg@dbg{2}{WG picture loop - ^^J List: \meaning#1 - ^^J Prefix: `#2' - ^^J Position: `#3' - ^^J Styles: `#4'} - \foreach \p in #1{% - \wg@dbg{2}{WG picture element: \meaning\p}% - \expandafter\wg@pic\p\@endwg@pic {#2}{#3}{#4}% - }% -} -% \end{macrocode} -% \end{Macro} -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Nodes in compound nodes} -% -% \begin{Macro}{\wg@node} -% The macro \cs{wg@node} will render a \texttt{node}. This can be -% used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} -% node shapes. -% -% The arguments are -% \begin{enumerate} -% \item Prefix -% \item Position -% \item Fixed options -% \item User options -% \item Body. -% \end{enumerate} -% -% That is, the macro expects calls like -% \begin{Syntax} -% \cs{wg@node}\oarg{options}\meta{body}\cs{@endwg@node}\marg{prefix}\marg{position}\marg{options} -% \end{Syntax} -% -% Note the \cs{@endwg@node} at the end of the call to swallow up -% \meta{body}. Typically this macro is used as -% -% \begin{Syntax} -% \cs{edef}\cs{args}\{\meta{something}\} -% \cs{expandafter}\cs{wg@node}\cs{args}\cs{@endwg@node}\marg{prefix}\parg{position}\marg{options} -% \end{Syntax} -% -% where \meta{something} typically expands to \oarg{user -% option}\meta{body} -% -% First, the top-level macro \cs{wg@node} that looks for user -% options. -% -% \begin{macrocode} -\def\wg@node{% - \@ifnextchar[{\wg@@node}{\wg@@node[]}%] -} -% \end{macrocode} -% -% This macro then forwards to \cs{wg@@node} to gobble up -% \meta{body}. -% -% \begin{enumerate} -% \item User options -% \item Arguments -% \end{enumerate} -% -% \begin{macrocode} -\def\wg@@node[#1]#2\@endwg@node{% - \wg@dbg{2}{Options: `#1', body: `#2'}% - \wg@@@node{#1}{#2}% -} -% \end{macrocode} -% -% \begin{enumerate} -% \item User options -% \item Arguments -% \item Prefix -% \item Coordinates -% \item Fixed options -% \end{enumerate} -% -% \begin{macrocode} -\def\wg@@@node#1#2#3#4#5{% - \ifx|#2|\wg@dbg{3}{No body given}% - \else% - \wg@dbg{3}{^^JWG Pic: - ^^J User options: #1 - ^^J Body: #2 - ^^J Prefix: #3 - ^^J Coordinates: #4 - ^^J Fixed options: #5}% - % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}% - \node[#5,#1] at (#4) {#3#2};% - \fi% - \wg@dbg{3}{End of WG Node} -} -% \end{macrocode} -% \end{Macro} -% -% -% \begin{Macro}{\wg@node@all} -% -% This macro sets all pictures in a list. -% -% \begin{enumerate} -% \item List -% \item Prefix -% \item Position -% \item Styles -% \end{enumerate} -% -% -% \begin{macrocode} -\def\wg@node@all#1#2#3#4{% - \wg@dbg{2}{WG picture loop - ^^J List: \meaning#1 - ^^J Prefix: `#2' - ^^J Position: `#3' - ^^J Styles: `#4'} - \foreach \p in #1{% - \wg@dbg{2}{WG picture element: \meaning\p}% - \expandafter\wg@node\p\@endwg@node {#2}{#3}{#4}% - }% -} -% \end{macrocode} -% \end{Macro} -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Bounding boxes} -% -% Bounding box dimensions -% -% -% \begin{macrocode} -\newdimen\wg@bb@minx -\newdimen\wg@bb@miny -\newdimen\wg@bb@maxx -\newdimen\wg@bb@maxy -% \end{macrocode} -% -% -% -% Enable or disable bounding box tracking -% -% \begin{macrocode} -\newif\ifwg@notrelevantforpathsize\wg@notrelevantforpathsizefalse -% \end{macrocode} -% -% -% \begin{Macro}{wg@resetbb} -% Reset the bounding box tracking dimensions -% -% \begin{macrocode} -\def\wg@resetbb{% - \global\wg@bb@minx=16000pt\relax% - \global\wg@bb@miny=16000pt\relax% - \global\wg@bb@maxx=-16000pt\relax% - \global\wg@bb@maxy=-16000pt\relax% -} -% \end{macrocode} -% \end{Macro} -% -% \begin{Macro}{\old@pgf@protocolsize} -% Save PGF's bounding box algorithm -% -% \begin{macrocode} -\let\old@pgf@protocolsize\pgf@protocolsizes -% \end{macrocode} -% \end{Macro} -% -% \begin{Macro}{\wg@protocolsizes} -% Our bounding box algorithm -% -% \begin{macrocode} -\def\wg@protocolsizes#1#2{% - \old@pgf@protocolsize{#1}{#2} - \ifwg@notrelevantforpathsize\else% - \ifdim#1<\wg@bb@minx\global\wg@bb@minx#1\fi% - \ifdim#1>\wg@bb@maxx\global\wg@bb@maxx#1\fi% - \ifdim#2<\wg@bb@miny\global\wg@bb@miny#2\fi% - \ifdim#2>\wg@bb@maxy\global\wg@bb@maxy#2\fi% - \fi -} -% \end{macrocode} -% \end{Macro} -% -% % \begin{environment}{getbbl} -% Environment that tracks the local bounding box -% -% \begin{macrocode} -\newenvironment{getbbl}{% - \wg@resetbb% - \wg@notrelevantforpathsizefalse% - \global\let\pgf@protocolsizes\wg@protocolsizes}{% - \gdef\pgf@sh@ns@L{rectangle} - \gdef\pgf@sh@np@L{% - \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}% - \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}% - } - \gdef\pgf@sh@nt@L{{1}{0}{0}{1}{0pt}{0pt}} - \gdef\pgf@sh@pi@L{\pgfpictureid} - \global\let\pgf@protocolsizes\old@pgf@protocolsize -} -% \end{macrocode} -% \end{environment} -% -% \begin{environment}{getbb} -% Environment to track global bounding box -% -% \begin{macrocode} -\newenvironment{getbb}{% - \wg@resetbb% - \wg@notrelevantforpathsizefalse% - \global\let\pgf@protocolsizes\wg@protocolsizes}{% - \gdef\pgf@sh@ns@M{rectangle} - \gdef\pgf@sh@np@M{% - \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}% - \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}% - } - \gdef\pgf@sh@nt@M{{1}{0}{0}{1}{0pt}{0pt}} - % \pgfgettransform\pgf@temp% - % \xdef\pgf@sh@nt@M{\pgf@temp} - % \pgfgettransformentries{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{\pgf@temp}{\pgf@temp} - % \message{^^JTransform of M: \meaning\pgf@temp} - % \xdef\pgf@sh@nt@M{{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{0pt}{0pt}}% - % \message{^^JTransform of M: \meaning\pgf@sh@nt@M} - \gdef\pgf@sh@pi@M{\pgfpictureid} - \global\let\pgf@protocolsizes\old@pgf@protocolsize -} -% \end{macrocode} -% \end{environment} +% \input{util/misc.dtx} +% \input{util/compound.dtx} +% \input{util/bb.dtx} +% \input{util/tikz.dtx} +% \input{util/randomid.dtx} +% \input{util/icons.dtx} % % \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Other Tikz utilities} -% -% \begin{TikzKey}{tikz/reverseclip} -% -% A reverse clipping path. This is used to cut out stuff outside of -% path defined. -% -% \begin{macrocode} -\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) -- - (current bounding box.south east) -- - (current bounding box.south west) -- - (current bounding box.north west) -- - (current bounding box.north east)}] -% \end{macrocode} -% \end{TikzKey} -% -% \begin{TikzKey}{tikz/clip even odd rule} -% A reverse clipping path -% -% \begin{macrocode} -\tikzset{ - clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey -} -% \end{macrocode} -% \end{TikzKey} -% -% -% \begin{TikzKey}{tikz/invclip} -% -% Inverse clipping. This should be an option \emph{after} the path to -% do the inverse clipping by. This works by adding a \emph{large} -% (page) path to the current path, and then use that as clipping. -% -% \begin{macrocode} -\tikzset{ - invclip/.style={ - clip,insert path= - [clip even odd rule]{ - [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen) - } - }, -} -% \end{macrocode} -% \end{TikzKey} -% -% \begin{TikzKey}{save clip} -% -% An option for use with sub-elements of NATO App 6(c) or chit -% nodes. This will save the current path as a clipping path for the -% next paths to be drawn in the sub-element -% -% \begin{macrocode} -\newif\ifwg@s@ve\wg@s@vefalse -\tikzset{ - save clip/.is choice, - save clip/true/.code={\global\wg@s@vetrue}, - save clip/false/.code={\global\wg@s@vefalse}, - save clip/.default={true}, - save clip/.initial={false}, -} -% \end{macrocode} -% \end{TikzKey} -% -% \begin{TikzKey}{scale line widths} -% -% Scales any line width specified in the node options. -% -% Use like -% -% \begin{verbatim} -% \tikzset{ -% some/.style={ -% scale line widths, -% line width=1pt} -% } -% \end{verbatim} -% -% Note that the order is important. -% -% \begin{macrocode} -\tikzset{ - scale line widths/.style={% - /utils/exec=\def\tikz@semiaddlinewidth##1{% - \pgfgettransformentries{% - \wg@jaca}{% - \wg@jacb}{% - \wg@jacc}{% - \wg@jacd}{% - \wg@tmp}{% - \wg@tmp}% - \pgfmathsetmacro{\wg@jac}{sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))}% - \wg@dbg{4}{Scaling line width ##1 by \wg@jac} - \pgfmathsetmacro{\wg@lw}{\wg@jac*##1}% - \wg@dbg{4}{Scaled ##1 -> \wg@lw} - \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}% - \wg@dbg{4}{Added scaled option \wg@lw} - \pgfmathsetlength\pgflinewidth{\wg@lw pt} - \wg@dbg{4}{Did set line width \wg@lw pt} - }}, - relative line width/.style={% - /utils/exec=\def\tikz@semiaddlinewidth##1{% - \wg@dbg{4}{Relative line width #1 times ##1}% - \pgfmathsetmacro{\wg@lv}{#1*##1}% - \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}% - \pgfmathsetlength\pgflinewidth{\wg@lw pt}}} -} -% \end{macrocode} -% \end{TikzKey} -% -% \begin{TikzKey}{sub pic actions} -% -% This is key that propagates actions to sub pictures of pictures. -% The normal \texttt{pic actions} cannot be used as it causes an -% infinite loop. -% -% \begin{macrocode} -\tikzset{ - sub pic actions/.code={% - \tikz@picmode% - \edef\opts{% - \iftikz@mode@draw draw,\else draw=none,\fi - \iftikz@mode@fill fill\else fill=none\fi} - \wg@dbg{5}{^^JSub Mode: \meaning\tikz@picmode \meaning\opts} - \pgfset{/tikz/.cd} - \pgfkeysalsofrom{\opts} - }} -% \end{macrocode} -% \end{TikzKey} -% -% \begin{TikzKey}{wg/debug show} -% -% Show debugging information -% -% \begin{macrocode} -\tikzset{ - wg/debug show/.code={% - \extractcolorspec{pgfstrokecolor}{\wg@tmp@fg} - \def\wg@tmp@bg{none} - \@ifundefinedcolor{pgffillcolor}{}{ - \extractcolorspec{pgffillcolor}{\wg@tmp@bg}} - \begingroup - \tikz@mode - \wargamedbglvl=#1 - \wg@dbg{3}{Drawing with w/stroke `\wg@tmp@fg' - (\tikz@strokecolor,\iftikz@mode@draw\else not\space\fi drawing) - and fill `\wg@tmp@bg' (\tikz@fillcolor,\iftikz@mode@fill\else - not\space\fi filling)} - \endgroup - } -} -% \end{macrocode} -% \end{TikzKey} -% -% -% \iffalse -% -------------------------------------------------------------------- -% \fi -% -% \subsubsection{Random IDs} -% -% -% \begin{macrocode} -\def\wg@r@ndom@id{% - \def\wg@uuid{} - \foreach \i in {1,...,8}{% - \pgfmathparse{Hex(random(0,15))} - \xdef\wg@uuid{\wg@uuid\pgfmathresult}}} -% \end{macrocode} -% -% \iffalse % %\fi -% \iffalse -% ==================================================================== -% \fi -% -% \subsection{The \texttt{wgexport} class} -% \label{sec:impl:util} -% -% This document class is used for exporting game component to be used -% in a VASSAL module -% libraries. -% -% \iffalse -%<*exportcls> -%\fi -% -% Class identification and load \texttt{wargame} package -% -% \begin{macrocode} -\ProvidesClass{wgexport} -\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone} -\DeclareOption{noterrainpic}{% - \PassOptionsToPackage{\CurrentOption}{wargame}} -\DeclareOption{terrainpic}{% - \PassOptionsToPackage{\CurrentOption}{wargame}} -\DeclareOption*{% - \PassOptionsToClass{\CurrentOption}{standalone}} -\ProcessOptions\relax -\LoadClass{standalone} -\RequirePackage{wargame} -% \end{macrocode} -% -% We need a few utilities before we get to the actual environment. -% First, we need a tools to write out literal left and right curly -% braces. We do a bit of catcode hackery to accomplish that. -% -% \begin{macrocode} -\begingroup -\catcode`\^^I=12 -\def\@tabchar{^^I} -\catcode`<=1 \catcode`>=2 -\catcode`{=12 \catcode`}=12 -\gdef\@lbchar<{> -\gdef\@rbchar<}> -\endgroup -% \end{macrocode} -% -% Above, we temporarily set the tab, and left and right curly brace -% characters to be regular letters (12), and the catcodes of less than -% and greater than to be those of left and right curly braces -% respectively. We then define the macros \cs{@tabchar}, -% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters. -% \LaTeX already has \cs{@percentchar}. -% -% Everything we do should go inside this environment. The single -% optional argument is the file name stem of the output JSON file. -% -% \begin{macrocode} -\newenvironment{imagelist}[1][\jobname]{% - \newwrite\mk@out% - \def\mk@i{}% - \def\mk@w{\immediate\write\mk@out}% - \immediate\openout\mk@out=#1.json - \mk@w{[} -}{ - \mk@w{\mk@i \@lbchar "name":"End of list", "category": "<>", - "subcategory": "" \@rbchar } - \mk@w{]} - \immediate\closeout\mk@out -} -% \end{macrocode} -% -% -% Preceed all images (\textsf{tikzpicture}) with this command -% -% First argument is the name of the image. This can be anything. -% Note that for counters, if the name ends in \texttt{flipped} then it -% is considered the backside of a counter. -% -% Second argument is the type of image. Recognised types are -% -% \begin{itemize} -% \item \texttt{board} for boards -% \item \texttt{oob} for OOBs -% \item \texttt{chart} for charts -% \item \texttt{counter} for counters -% \item \texttt{front} for front page -% \end{itemize} -% -% Other types can be used, and the images will be exported, but the -% Python script pays no particular attention to those then. Use for -% example to prepare images for help or the like. -% -% The third argument is the sub type. This is most relevant for the -% counters. Sub types can be anything, but since the counters will -% receive different prototypes based on the sub type, it makes sense -% to divide into sub types a la -% -% \begin{itemize} -% \item factions -% \item common markers -% \end{itemize} -% -% The faction sub types should just be the name of the faction. -% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact. Spaces should not -% matter. -% -% For common markers, there are a few names that are recognised -% specifically by the Python script. These are -% -% \begin{itemize} -% \item \texttt{common} -% \item \texttt{all} -% \item \texttt{marker} -% \item \texttt{markers} -% \end{itemize} -% -% Counters that has these sub-types will no be considered to belong -% to any faction. -% -% Note that the Python script uses the faction names to guess the -% players of the game, and uses them in several places. -% -% -% \begin{macrocode} -\def\info{% - \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}} -\def\@@info#1#2#3#4{% - \chit@dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}% - \mk@w{ \@lbchar}% - \mk@w{ \space "name": "#2",}% - \mk@w{ \space "category": "#3",}% - \mk@w{ \space "subcategory": "#4", }% - \mk@w{ \space "number": \thepage #1}% - \let\oldmk@i\mk@i% - \ifx#1,\relax\edef\mk@i{\mk@i\space\space}\fi} -\def\end@info{% - \let\mk@i\oldmk@i% - \mk@w{ \space \@rbchar,}} -% \end{macrocode} -% -% Make separate images for each counter (single sided). -% -% \begin{macrocode} -\newcommand\chitimages[2][]{% - \begingroup% - \let\chit@report\do@chit@report% - \let\natoapp@report\do@natoapp@report% - \chit@dbg{2}{chits to make images of `#2'}% - \foreach[count=\ti from 0] \t/\x in #2{% - \ifx\t\empty\else% Ignore empty rows - \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')} - \ifx\t\x\def\x{#1}\fi% Take sub-category or default - \foreach \u/\m in \t{% - \ifx\u\empty\else% Ignore empty cells - \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% - \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined - \ifx\u\m\def\m{1}\fi% If the same as unit - \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% - %% We only make one copy of the chit, since we can duplicate - %% it in VASSAL - \info*{\u}{counter}{\x} - \begin{tikzpicture} - \chit[\u=\ti]% - \end{tikzpicture} - \end@info% - %% \foreach \n in {1,...,\m}{% Make a number of copies - %% \ifx\u\chit@blank% - %% \chit@dbg{3}{Ignoring blank chit:\u}% - %% \else% - %% \info{\u}{counter}{#2} - %% \begin{tikzpicture} - %% \chit[\u=\ti](\c,\r)% - %% \end{tikzpicture} - %% \fi% - %% }% - \fi% - }% - \chit@dbg{2}{End of inner loop}% - \fi% - }% - \chit@dbg{2}{End of outer loop}% - \endgroup% -} -% \end{macrocode} -% -% Make separate images for each counter (double sided). The back-side -% counters must be defined by append `\texttt{ flipped}' the front -% face name -% -% \begin{macrocode} -\newcommand\doublechitimages[2][]{% - \begingroup% - \let\chit@report\do@chit@report% - \let\natoapp@report\do@natoapp@report% - \foreach[count=\ti from 0] \t/\x in #2{% - \ifx\t\empty\else% Ignore empty rows - \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')} - \ifx\t\x\def\x{#1}\fi% Take sub-category or default - \foreach \u/\m in \t{% - \ifx\u\empty\else% Ignore empty cells - \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% - \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined - \ifx\u\m\def\m{1}\fi% If the same as unit - \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% - %% Flipped chit - \edef\s{\u\space flipped}% - %% We only make one copy of the chit, since we can duplicate - %% it in VASSAL - \info*{\u}{counter}{\x}% - \begin{tikzpicture}% - \chit[\u=\ti]% - \end{tikzpicture}% - \end@info% - \info*{\s}{counter}{\x}% - \begin{tikzpicture}% - \chit[\s=\ti]% - \end{tikzpicture}% - \end@info% - %% \foreach \n in {1,...,\m}{% Make a number of copies - %% \ifx\u\chit@blank% - %% \chit@dbg{3}{Ignoring blank chit:\u}% - %% \else% - %% \info{\u}{counter}{#2} - %% \begin{tikzpicture} - %% \chit[\u=\ti](\c,\r)% - %% \end{tikzpicture} - %% \fi% - %% }% - \fi% - }% - \fi% - }% - \endgroup% -} -% \end{macrocode} -% -% Special for boards, we have the environment \textsf{boardimage}. -% Like \cs{info} we must specify the name and sub-category of the -% board, but the category is assumed to be \texttt{board} (though the -% optional argument can specify a different category). -% -% Within this environment some specific styles are defined that allows -% the user to specify VASSAL zones on the board. For this to work -% properly, the parent \textsf{tikzpicture} \emph{must} have the style -% \texttt{zoned}. This style will record the bounding box of the -% picture which we will need to calculate VASSAL coordinates later -% on. -% -% Other styles are \texttt{zone scope}, to be applied to -% \texttt{scope}s in the picture, and \texttt{zone path} to be applied -% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the -% picture. These will record coordinates of these elements in side -% the picture. The Python script will then define VASSAL zones based -% on these coordinates. % -% For \texttt{zone scope} applied to a \texttt{scope}, what is -% recorded are +% \input{util/export.dtx} % -% \begin{itemize} -% \item The current coordinate transformation matrix -% \item The current translation -% \item The bounding box, within the current transformation and -% translation. -% \end{itemize} -% -% To define a zone in the board, simply enclose it in a -% -% \begin{verbatim} -% \begin{scope}[zone scope=name] -% ... -% \end{scope} -% \end{verbatim} -% -% The \meta{name} will be the name of the scope. If this contains the -% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone -% will get a hex grid with numbering attached to it. -% -% If the \meta{name} contains the sub-string \texttt{turn} (any case), -% then it is assumed to be a turn track and a rectangular grid will be -% attached. The column and row separator will be set to \texttt{T}, -% so that it won't collide with the main zone. Similar if \meta{name} -% contains \texttt{oob}, except the separator is set to \texttt{O}. -% -% If \meta{name} contains the sub-string \texttt{pool}, then it is -% assumed to be a pool of counters, and \emph{no} grid is attached. -% -% For \texttt{zone path} applied to a \texttt{path}, what is recorded -% is the path coordinates (as straight line segments) in the global -% coordinate system. -% -% Both styles take one argument --- the name of the zone. If that -% name contains the sub-string \texttt{hex} anywhere in the name, then -% the zone is assumed to contain a hex grid. Otherwise, a rectangular -% grid (of fixed size) will be applied to it. -% -% The environment \texttt{boardimage} also records the coordinate -% options currently in use (keys \texttt{hex/first row is}, -% \texttt{hex/row direction is}, and so on), as well as the current -% label option (as defined by \texttt{every hex} or \texttt{every hex -% node}). -% -% All coordinates, and such are recorded in centimetres. It is worth -% remembering that the Tikz coordinate system has the $y$ axis point -% upward, while typical image software has the $y$ axis point down. -% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch) -% resolution. -% -% That means that scaling factor becomes -% -% $$\frac{150\mathrm{pixel}}{2.54\mathrm{cm}}=59.055\frac{\mathrm{pixel}}{\mathrm{cm}}$$ -% % \iffalse -% Using definition in terms of printers feet - the one to use! -% -% PNG: 1674 x 1101 -% PDF: "lower left": [-0.02107,-0.02107], -% "upper right": [28.31705,18.60843] -% Width: 28.31705+0.02107 = 28.33812 -% Height: 18.60843+0.02107 = 18.62950 -% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166 -% 1101 / 18.62950 = 59.09981480984460130438 -% Average = 59.08609399739914809302 -% -% Calculated = 150 / 2.54 = 59.05511811023622047244 -% -% Using 1/72.27 -% -% "lower left": [-0.02109,-0.02109], -% "upper right": [28.321,18.61102] -% Width: 28.321+0.02109=28.34209 -% Height: 18.61102+0.02109=18.63211 -% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267 -% 1101 / 18.63211 = 59.09153606328000425072 -% Average = 59.07781736180584381169 +% EOF % \fi -% -% The information extracted is written to the -% \cs{jobname}\texttt{.json} file as a sub-object (with name given by -% the first optional argument) of the image object. In that way, we -% can later on easily get the information from our catalogue of -% images. -% -% Note, the styles \texttt{zoned}, \texttt{zone scope}, and -% \texttt{zone path} are defined in \texttt{wargame} to be dummies so -% that one can have them in the definition of the board without -% impact. -% -% Since we want to write all dimensions in centimetres, we need to be -% able to convert \texttt{pt} dimensions to centimetres. We make two -% macros to do that for us. -% -% The exact definition of 1pt is -% -% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1" -% = 0.03513\overline{6}$$ -% -% \begin{macrocode} -% 2.54 / 72.27 = .03514598035145980351 -% \def\pt@to@cm#1{\pgfmathparse{#1 * 0.0351460}} -\def\pt@to@cm#1{\pgfmathparse{#1 * 0.0351367}} -\def\ptpoint@to@cm#1#2{% - \pt@to@cm{#1}\edef\x{\pgfmathresult}% - \pt@to@cm{#2}\edef\y{\pgfmathresult}} -% \end{macrocode} -% \begin{macrocode} -\def\mk@get@anchor#1#2{% - \pgfpointanchor{#1}{#2}% - \pgfgetlastxy\tmp@x\tmp@y% - \pt@to@cm{\tmp@x}\edef\tmp@x{\pgfmathresult} - \pt@to@cm{\tmp@y}\edef\tmp@y{\pgfmathresult} -} -\def\mk@get@global@anchor#1#2{% - \pgfpointanchor{#1}{#2}% - \pgfgetlastxy\tmp@x\tmp@y% - \pgfpointtransformed{\pgfpoint{\tmp@x}{\tmp@y}} - \pgf@xa=\pgf@x - \pgf@ya=\pgf@y - \pt@to@cm{\the\pgf@xa}\edef\tmp@x{\pgfmathresult} - \pt@to@cm{\the\pgf@ya}\edef\tmp@y{\pgfmathresult} -} -\def\get@bb#1{% - % \pgfpointanchor{#1}{south west}% - % \pgfgetlastxy\tmp@llx\tmp@lly% - % \pgfpointanchor{#1}{north east}% - % \pgfgetlastxy\tmp@urx\tmp@ury% - % \pt@to@cm{\tmp@llx}\edef\llx{\pgfmathresult} - % \pt@to@cm{\tmp@lly}\edef\lly{\pgfmathresult} - % \pt@to@cm{\tmp@urx}\edef\urx{\pgfmathresult} - % \pt@to@cm{\tmp@ury}\edef\ury{\pgfmathresult} - \mk@get@anchor{#1}{south west} - \edef\llx{\tmp@x} - \edef\lly{\tmp@y} - \mk@get@anchor{#1}{north east} - \edef\urx{\tmp@x} - \edef\ury{\tmp@y} -} -% \end{macrocode} -% \begin{macrocode} -\def\mk@transform{% - \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy} - \pt@to@cm{\ptdx}\edef\dx{\pgfmathresult} - \pt@to@cm{\ptdy}\edef\dy{\pgfmathresult} - \mk@w{ \mk@i "xx": \mxx,} - \mk@w{ \mk@i "xy": \mxy,} - \mk@w{ \mk@i "yx": \myx,} - \mk@w{ \mk@i "yy": \myy,} - \mk@w{ \mk@i "dx": \dx,} - \mk@w{ \mk@i "dy": \dy,} -} -% \end{macrocode} -% \begin{macrocode} -\def\mk@bb#1{% - \get@bb{#1} - \mk@w{ \mk@i "lower left": [\llx,\lly],} - \mk@w{ \mk@i "upper right": [\urx,\ury],} - \begingroup - % \pgftransforminvert - % \pgfpointanchor{#1}{south west}% - % \pgfgetlastxy\tmp@llx\tmp@lly% - % \pgfpointtransformed{\pgfpoint{\tmp@llx}{\tmp@lly}} - % \pgf@xa=\pgf@x - % \pgf@ya=\pgf@y - % % - % \pgfpointanchor{#1}{north east}% - % \pgfgetlastxy\tmp@urx\tmp@ury% - % \pgfgetlastxy\tmp@llx\tmp@lly% - % \pgfpointtransformed{\pgfpoint{\tmp@urx}{\tmp@ury}} - % \pgf@xb=\pgf@x - % \pgf@yb=\pgf@y - % \pt@to@cm{\the\pgf@xa}\edef\llx{\pgfmathresult} - % \pt@to@cm{\the\pgf@ya}\edef\lly{\pgfmathresult} - % \pt@to@cm{\the\pgf@xb}\edef\urx{\pgfmathresult} - % \pt@to@cm{\the\pgf@yb}\edef\ury{\pgfmathresult}x - \mk@get@global@anchor{#1}{south west} - \mk@w{ \mk@i "global lower left": [\tmp@x,\tmp@y],} - \mk@get@global@anchor{#1}{north east} - \mk@w{ \mk@i "global upper right": [\tmp@x,\tmp@y]} - \endgroup -} -\def\mk@pos#1(#2){% - \hex@dbg{10}{^^JMarking `#2' with `#1' - start} - \coordinate[transform shape] (tmp) at (#2) {}; - \mk@get@anchor{tmp}{center} - \hex@dbg{3}{^^JMarking `#2' with `#1' - `\tmp@x',\tmp@y'} - \tikzset{zone point={#1}{\tmp@x}{\tmp@y}} -} -% \end{macrocode} -% -% For the key \texttt{zone path} to work, we need to be able to record -% the path as it moves along. To that end, we make a custom -% decoration that will do that for us, and, once the path is finished, -% write the path to our JSON file. -% -% \begin{macrocode} -\pgfdeclaredecoration{record path construction}{initial}{% - \state{initial}[width=0pt,next state=more]{ - \begingroup - \pgf@decorate@inputsegment@first - \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} - \xdef\wg@path{[\x,\y]} - \endgroup - }% - \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{% - \begingroup - \pgf@decorate@inputsegment@last - \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} - \xdef\wg@path{\wg@path,[\x,\y]} - \endgroup - } - \state{final}{% - \begingroup - \pgf@decorate@inputsegment@last - \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} - \xdef\wg@path{\wg@path,[\x,\y]} - \endgroup - \mk@w{ \mk@i "zone path \wg@record@path@name": \@lbchar} - \mk@w{ \mk@i\space "path": [\wg@path] \@rbchar,} - } -}% -% \end{macrocode} -% -% Now we can make our environment -% -% The first thing we do is to use the \cs{info} macro to mark the -% image. Then we open our JSON file. We make a short-hand macro for -% writing to that file. The macro \cs{bd@i} records the current -% indention (which is important in JSON) -% -% \begin{macrocode} -\newenvironment{boardimage}[3][board]{% - \def\bd@n{#2} - \newcount\mk@point - \mk@point=0 - \let\oomk@i\mk@i% - \let\markpos\mk@pos% -% \end{macrocode} -% -% -% Then, to extract the label option, we make a dummy \texttt{node} -% with the styles \texttt{every hex} and \texttt{every hex node}, so -% we can extract that option. -% -% \begin{macrocode} - \info{dummy}{<>}{}% - %\tikz{}% - \tikz{\scoped[every hex/.try,every hex node/.try]{% - \node[inner sep=0,outer sep=0]{% - \global\let\mk@label\hex@label}}}% -% \end{macrocode} -% -% The next thing we do is to make an object. The first things we put -% in are the units used (``cm''), and the grid options. -% -% \begin{macrocode} - \info*{#2}{#1}{#3}% - \mk@w{ \mk@i "zones": \@lbchar}% - \edef\mk@i{\mk@i\space} - %% Everything is made into centimeters - \mk@w{ \mk@i "units": "cm",} - \@ifundefined{mk@label}{}{\mk@w{ \mk@i "labels": "\mk@label",}} - %% Write out coordinate options as "coords" object - \mk@w{ \mk@i"coords": \@lbchar}% - \mk@w{ \mk@i "row": \@lbchar}% - \mk@w{ \mk@i\space "offset": \hex@coords@row@off,}% - \mk@w{ \mk@i\space "factor": \hex@coords@row@fac \@rbchar,}% - \mk@w{ \mk@i "column": \@lbchar}% - \mk@w{ \mk@i\space "offset": \hex@coords@col@off,}% - \mk@w{ \mk@i\space "factor": \hex@coords@col@fac,}% - \mk@w{ \mk@i\space "top short": "\hex@top@short@col",}% - \mk@w{ \mk@i\space "bottom short": "\hex@bot@short@col" \@rbchar}% - \mk@w{ \mk@i\@rbchar,}% -% \end{macrocode} -% -% We then monkey-patch \cs{boardframe} to also output coordinates to -% our JSON file. Note that this will probably be embedded in a -% different object. -% -% \begin{macrocode} - %% - \let\oldbo@rdframe\bo@rdframe% - \def\bo@rdframe[##1](##2)(##3){% - \oldbo@rdframe[##1](##2)(##3)% - \mk@w{ \mk@i"board frame": \@lbchar} - \mk@w{ \mk@i\space "lower left": [\llx,\lly],} - \mk@w{ \mk@i\space "upper right": [\urx,\ury],} - \mk@w{ \mk@i\space "margin": \margin,} - \mk@w{ \mk@i\space "width": \w,} - \mk@w{ \mk@i\space "height": \h \@rbchar,}}% -% \end{macrocode} -% -% Next, we make the style \texttt{zoned} to be applied to the -% \texttt{tikzpicture} environment. This records the bounding box of -% the full picture. -% -% \begin{macrocode} - \tikzset{ - zoned/.code={% Apply to whole picture - \pgfkeys{% - % This needs to be done in the picture! - /tikz/execute at end picture={% - \mk@w{ \mk@i "zoned": \@lbchar} - \mk@transform% - \mk@bb{current bounding box} - \mk@w{ \mk@i \@rbchar,} - } - } - }, -% \end{macrocode} -% -% The next style is the \texttt{zone scope}. At the start of the -% scope we record the current transformation matrix. Then we install -% a handler to extract the bounding box at the end of the scope. Note -% that we increase indention here. -% -% \begin{macrocode} - zone scope/.code={% - \mk@w{ \mk@i"zone scope ##1": \@lbchar} - \let\omk@i\mk@i - \edef\mk@i{\mk@i\space} - \mk@transform% - %\bd@w{ \@rbchar,} - \gdef\wg@export@box{##1}% - \pgfkeys{% - /tikz/local bounding box=wg export box, - /tikz/execute at end scope={ - \mk@bb{wg export box} - \let\mk@i\omk@i - \mk@w{ \mk@i\@rbchar,}}, - } % pgfkeys - }, % zone scope -% \end{macrocode} -% The next style gets the global coordinates of the current (0,0) -% point - f.ex. in a node - and outputs that -% \begin{macrocode} - zone point/.code n args={3}{ - \pgf@xa=##2 cm - \pgf@ya=##3 cm - \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}} - % \pgfpointtransformed{\pgfpoint{0pt}{0pt}} - \pgf@xa=\pgf@x - \pgf@ya=\pgf@y - \pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult} - \pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult} - \advance\mk@point1 - \global\mk@point=\mk@point - \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py] - \@rbchar, } - %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py} - }, - zone oob point/.code n args={3}{ - \pgf@xa=##2 cm - \pgf@ya=##3 cm - \advance\pgf@xa.1cm - \advance\pgf@ya.1cm - \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}} - % \pgfpointtransformed{\pgfpoint{0pt}{0pt}} - \pgf@xa=\pgf@x - \pgf@ya=\pgf@y - \pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult} - \pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult} - \advance\mk@point1 - \global\mk@point=\mk@point - \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py] - \@rbchar, } - %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py} - }, - zone global point/.code n args={3}{ - \advance\mk@point1 - \global\mk@point=\mk@point - \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py] - \@rbchar, } - }, -% \end{macrocode} -% -% The \texttt{zone path} style is a bit more simple, but only because -% the bulk of the work is done in a decoration. We need to be able to -% pass a name to that decoration, s we make a key for that. The user -% need not think about that though. -% -% \begin{macrocode} - /pgf/decoration/record path name/.store in=\wg@record@path@name, - zone path/.style={% - postaction={decorate,decoration={ - record path construction, - record path name=##1}} - } % zone path - }% tikzset -} -% \end{macrocode} -% -% That finishes the first part of the environment. At the end of the -% environment, we simple write the name of the picture, and close our -% JSON output. -% -% \begin{macrocode} -{% - \mk@w{ \mk@i "name": "\bd@n" }% - \let\mk@i\oomk@i% - \mk@w{ \mk@i\@rbchar}% - \end@info% -} -% \end{macrocode} -% -% -% TO BE DONE: We could add hooks to both the \texttt{hex} and -% \texttt{chit} shapes that would allow us to write out the settings -% for each of these. This would allow us to make data files that -% contain the information available in the \LaTeX{} code. For -% example, we could write a counters -% -% \begin{itemize} -% \item Left and right identifiers -% \item Upper left, upper right, lower left, and lower right -% identifiers. (some care must be taken if these contains graphics -% and not just text.) -% \item Factors -% \item NATO symbol -% \begin{itemize} -% \item Faction, command, echelon -% \item Mains -% \item Left, right, top, and bottom attributes and modifiers -% \item Below attribute -% \end{itemize} -% \end{itemize} -% -% If one then assumed that for example the upper left corner holds the -% start-up hex, then one could use that information. -% -% The code below exports the chit information to the JSON file. Not -% sure how to use it though. -% -% \begin{macrocode} -\tikzset{ - zone turn/.store in=\zone@turn, - zone mult/.store in=\zone@mult -} -\def\do@chit@report{% - \mk@w{ \mk@i "chit": \@lbchar} - \@ifundefined{id}{} {\mk@w{ \mk@i\space "id": "\id", }}% - \@ifundefined{chit@symbol}{} {\mk@w{ \mk@i\space "symbol": "true", }}% - \@ifundefined{chit@full}{} {\mk@w{ \mk@i\space "full": "\chit@full", }} - \@ifundefined{chit@factors}{} {\mk@w{ \mk@i\space "factors": "\chit@factors", }}% - \@ifundefined{chit@left}{} {\mk@w{ \mk@i\space "left": "\chit@left", }}% - \@ifundefined{chit@right}{} {\mk@w{ \mk@i\space "right": "\chit@right", }}% - \@ifundefined{chit@upper@left}{} {\mk@w{ \mk@i\space "upper left": "\chit@upper@left", }}% - \@ifundefined{chit@lower@left}{} {\mk@w{ \mk@i\space "lower left": "\chit@lower@left", }}% - \@ifundefined{chit@upper@right}{}{\mk@w{ \mk@i\space "upper right": "\chit@upper@right", }}% - \@ifundefined{chit@lower@right}{}{\mk@w{ \mk@i\space "lower right": "\chit@lower@right}", }% - \mk@w{ \mk@i\space "end": 0} - \@ifundefined{chit@symbol}{ - \mk@w{ \mk@i \@rbchar } - }{ - \mk@w{ \mk@i \@rbchar, }% NATOAPP6c will follow - }% -} -\def\do@natoapp@report{% - \mk@w{ \mk@i "natoapp6c": \@lbchar} - \@ifundefined{id}{}{\mk@w{ \mk@i\space "id": "\id", }} - \@ifundefined{natoapp@fac}{}{\mk@w{ \mk@i\space "faction": "\natoapp@fac", }} - \@ifundefined{natoapp@cmd}{}{\mk@w{ \mk@i\space "command": "\natoapp@cmd", }} - \@ifundefined{natoapp@ech}{}{\mk@w{ \mk@i\space "echelon": "\natoapp@ech", }} - \@ifundefined{natoapp@main}{}{\mk@w{ \mk@i\space "main": "\natoapp@main", }} - \@ifundefined{natoapp@left}{}{\mk@w{ \mk@i\space "left": "\natoapp@left", }} - \@ifundefined{natoapp@right}{}{\mk@w{ \mk@i\space "right": "\natoapp@right", }} - \@ifundefined{natoapp@upper}{}{\mk@w{ \mk@i\space "upper": "\natoapp@upper", }} - \@ifundefined{natoapp@lower}{}{\mk@w{ \mk@i\space "lower": "\natoapp@lower", }} - \@ifundefined{natoapp@below}{}{\mk@w{ \mk@i\space "below": "\natoapp@below", }} - \mk@w{ \mk@i\space "end": 0} - \mk@w{ \mk@i \@rbchar} -} -% \end{macrocode} -% \iffalse -% -%\fi -% + diff --git a/macros/latex/contrib/wargame/source/util/export.dtx b/macros/latex/contrib/wargame/source/util/export.dtx new file mode 100644 index 0000000000..fde067dfe8 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/export.dtx @@ -0,0 +1,817 @@ +% \iffalse +% ==================================================================== +% \fi +% +% \subsection{The \texttt{wgexport} class} +% \label{sec:impl:util} +% +% This document class is used for exporting game component to be used +% in a VASSAL module +% libraries. +% +% \iffalse +%<*exportcls> +%\fi +% +% Class identification and load \texttt{wargame} package +% +% \begin{macrocode} +\ProvidesClass{wgexport} +\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone} +\DeclareOption{noterrainpic}{% + \PassOptionsToPackage{\CurrentOption}{wargame}} +\DeclareOption{terrainpic}{% + \PassOptionsToPackage{\CurrentOption}{wargame}} +\DeclareOption*{% + \PassOptionsToClass{\CurrentOption}{standalone}} +\ProcessOptions\relax +\LoadClass{standalone} +\RequirePackage{wargame} +% \end{macrocode} +% +% We need a few utilities before we get to the actual environment. +% First, we need a tools to write out literal left and right curly +% braces. We do a bit of catcode hackery to accomplish that. +% +% \begin{macrocode} +\begingroup +\catcode`\^^I=12 +\def\@tabchar{^^I} +\catcode`<=1 \catcode`>=2 +\catcode`{=12 \catcode`}=12 +\gdef\@lbchar<{> +\gdef\@rbchar<}> +\endgroup +% \end{macrocode} +% +% Above, we temporarily set the tab, and left and right curly brace +% characters to be regular letters (12), and the catcodes of less than +% and greater than to be those of left and right curly braces +% respectively. We then define the macros \cs{@tabchar}, +% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters. +% \LaTeX already has \cs{@percentchar}. +% +% Everything we do should go inside this environment. The single +% optional argument is the file name stem of the output JSON file. +% +% \begin{macrocode} +\newenvironment{imagelist}[1][\jobname]{% + \newwrite\mk@out% + \def\mk@i{}% + \def\mk@w{\immediate\write\mk@out}% + \immediate\openout\mk@out=#1.json + \mk@w{[} +}{ + \mk@w{\mk@i \@lbchar "name":"End of list", "category": "<>", + "subcategory": "" \@rbchar } + \mk@w{]} + \immediate\closeout\mk@out +} +% \end{macrocode} +% +% +% Preceed all images (\textsf{tikzpicture}) with this command +% +% First argument is the name of the image. This can be anything. +% Note that for counters, if the name ends in \texttt{flipped} then it +% is considered the backside of a counter. +% +% Second argument is the type of image. Recognised types are +% +% \begin{itemize} +% \item \texttt{board} for boards +% \item \texttt{oob} for OOBs +% \item \texttt{chart} for charts +% \item \texttt{counter} for counters +% \item \texttt{front} for front page +% \end{itemize} +% +% Other types can be used, and the images will be exported, but the +% Python script pays no particular attention to those then. Use for +% example to prepare images for help or the like. +% +% The third argument is the sub type. This is most relevant for the +% counters. Sub types can be anything, but since the counters will +% receive different prototypes based on the sub type, it makes sense +% to divide into sub types a la +% +% \begin{itemize} +% \item factions +% \item common markers +% \end{itemize} +% +% The faction sub types should just be the name of the faction. +% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact. Spaces should not +% matter. +% +% For common markers, there are a few names that are recognised +% specifically by the Python script. These are +% +% \begin{itemize} +% \item \texttt{common} +% \item \texttt{all} +% \item \texttt{marker} +% \item \texttt{markers} +% \end{itemize} +% +% Counters that has these sub-types will no be considered to belong +% to any faction. +% +% Note that the Python script uses the faction names to guess the +% players of the game, and uses them in several places. +% +% +% \begin{macrocode} +\def\info{% + \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}} +\def\@@info#1#2#3#4{% + \chit@dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}% + \mk@w{ \@lbchar}% + \mk@w{ \space "name": "#2",}% + \mk@w{ \space "category": "#3",}% + \mk@w{ \space "subcategory": "#4", }% + \mk@w{ \space "number": \thepage #1}% + \let\oldmk@i\mk@i% + \ifx#1,\relax\edef\mk@i{\mk@i\space\space}\fi} +\def\end@info{% + \let\mk@i\oldmk@i% + \mk@w{ \space \@rbchar,}} +% \end{macrocode} +% +% Make separate images for each counter (single sided). +% +% \begin{macrocode} +\newcommand\chitimages[2][]{% + \begingroup% + \let\chit@report\do@chit@report% + \let\natoapp@report\do@natoapp@report% + \chit@dbg{2}{chits to make images of `#2'}% + \foreach[count=\ti from 0] \t/\x in #2{% + \chit@dbg{2}{^^JRow: `\t' (`\x')} + \ifx\t\empty\else% Ignore empty rows + \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')} + % Take sub-category or default + \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi + \foreach \u/\m in \t{% + \ifx\u\empty\else% Ignore empty cells + \ifx\u\chit@blank\else% + \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% + \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined + \ifx\u\m\def\m{1}\fi% If the same as unit + \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% + %% We only make one copy of the chit, since we can duplicate + %% it in VASSAL + \info*{\u}{counter}{\x} + \begin{tikzpicture} + \chit[\u=\ti]% + \end{tikzpicture} + \end@info% + %% \foreach \n in {1,...,\m}{% Make a number of copies + %% \ifx\u\chit@blank% + %% \chit@dbg{3}{Ignoring blank chit:\u}% + %% \else% + %% \info{\u}{counter}{#2} + %% \begin{tikzpicture} + %% \chit[\u=\ti](\c,\r)% + %% \end{tikzpicture} + %% \fi% + %% }% + \fi% + \fi% + }% + \chit@dbg{2}{End of inner loop}% + \fi% + }% + \chit@dbg{2}{End of outer loop}% + \endgroup% +} +% \end{macrocode} +% +% Make separate images for each counter (double sided). The back-side +% counters must be defined by append `\texttt{ flipped}' the front +% face name +% +% \begin{macrocode} +\newcommand\doublechitimages[2][]{% + \begingroup% + \let\chit@report\do@chit@report% + \let\natoapp@report\do@natoapp@report% + \foreach[count=\ti from 0] \t/\x in #2{% + \ifx\t\empty\else% Ignore empty rows + \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')} + % Take sub-category or default + \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi + \foreach \u/\m in \t{% + \ifx\u\empty\else% Ignore empty cells + \ifx\u\chit@blank\else% + \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}% + \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined + \ifx\u\m\def\m{1}\fi% If the same as unit + \chit@dbg{2}{Next chit `\u' multiplicity `\m'}% + %% Flipped chit + \edef\s{\u\space flipped}% + %% We only make one copy of the chit, since we can duplicate + %% it in VASSAL + \info*{\u}{counter}{\x}% + \begin{tikzpicture}% + \chit[\u=\ti]% + \end{tikzpicture}% + \end@info% + \info*{\s}{counter}{\x}% + \begin{tikzpicture}% + \chit[\s=\ti]% + \end{tikzpicture}% + \end@info% + %% \foreach \n in {1,...,\m}{% Make a number of copies + %% \ifx\u\chit@blank% + %% \chit@dbg{3}{Ignoring blank chit:\u}% + %% \else% + %% \info{\u}{counter}{#2} + %% \begin{tikzpicture} + %% \chit[\u=\ti](\c,\r)% + %% \end{tikzpicture} + %% \fi% + %% }% + \fi% + \fi% + }% + \fi% + }% + \endgroup% +} +% \end{macrocode} +% +% Special for boards, we have the environment \textsf{boardimage}. +% Like \cs{info} we must specify the name and sub-category of the +% board, but the category is assumed to be \texttt{board} (though the +% optional argument can specify a different category). +% +% Within this environment some specific styles are defined that allows +% the user to specify VASSAL zones on the board. For this to work +% properly, the parent \textsf{tikzpicture} \emph{must} have the style +% \texttt{zoned}. This style will record the bounding box of the +% picture which we will need to calculate VASSAL coordinates later +% on. +% +% Other styles are \texttt{zone scope}, to be applied to +% \texttt{scope}s in the picture, and \texttt{zone path} to be applied +% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the +% picture. These will record coordinates of these elements in side +% the picture. The Python script will then define VASSAL zones based +% on these coordinates. +% +% For \texttt{zone scope} applied to a \texttt{scope}, what is +% recorded are +% +% \begin{itemize} +% \item The current coordinate transformation matrix +% \item The current translation +% \item The bounding box, within the current transformation and +% translation. +% \end{itemize} +% +% To define a zone in the board, simply enclose it in a +% +% \begin{verbatim} +% \begin{scope}[zone scope=name] +% ... +% \end{scope} +% \end{verbatim} +% +% The \meta{name} will be the name of the scope. If this contains the +% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone +% will get a hex grid with numbering attached to it. +% +% If the \meta{name} contains the sub-string \texttt{turn} (any case), +% then it is assumed to be a turn track and a rectangular grid will be +% attached. The column and row separator will be set to \texttt{T}, +% so that it won't collide with the main zone. Similar if \meta{name} +% contains \texttt{oob}, except the separator is set to \texttt{O}. +% +% If \meta{name} contains the sub-string \texttt{pool}, then it is +% assumed to be a pool of counters, and \emph{no} grid is attached. +% +% For \texttt{zone path} applied to a \texttt{path}, what is recorded +% is the path coordinates (as straight line segments) in the global +% coordinate system. +% +% Both styles take one argument --- the name of the zone. If that +% name contains the sub-string \texttt{hex} anywhere in the name, then +% the zone is assumed to contain a hex grid. Otherwise, a rectangular +% grid (of fixed size) will be applied to it. +% +% The environment \texttt{boardimage} also records the coordinate +% options currently in use (keys \texttt{hex/first row is}, +% \texttt{hex/row direction is}, and so on), as well as the current +% label option (as defined by \texttt{every hex} or \texttt{every hex +% node}). +% +% +% The information extracted is written to the +% \cs{jobname}\texttt{.json} file as a sub-object (with name given by +% the first optional argument) of the image object. In that way, we +% can later on easily get the information from our catalogue of +% images. +% +% Note, the styles \texttt{zoned}, \texttt{zone scope}, and +% \texttt{zone path} are defined in \texttt{wargame} to be dummies so +% that one can have them in the definition of the board without +% impact. +% \begin{macrocode} +\def\mk@transform{% + \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy} + \wg@pt@to@cm{\ptdx}\edef\dx{\pgfmathresult} + \wg@pt@to@cm{\ptdy}\edef\dy{\pgfmathresult} + \mk@w{ \mk@i "xx": \mxx,} + \mk@w{ \mk@i "xy": \mxy,} + \mk@w{ \mk@i "yx": \myx,} + \mk@w{ \mk@i "yy": \myy,} + \mk@w{ \mk@i "dx": \dx,} + \mk@w{ \mk@i "dy": \dy,} +} +% \end{macrocode} +% \begin{macrocode} +\def\mk@bb#1{% + \wg@get@bb{#1} + \mk@w{ \mk@i "lower left": [\llx,\lly],} + \mk@w{ \mk@i "upper right": [\urx,\ury],} + \begingroup + \wg@get@global@nchor{#1}{south west} + \mk@w{ \mk@i "global lower left": [\tmp@x,\tmp@y],} + \wg@get@global@nchor{#1}{north east} + \mk@w{ \mk@i "global upper right": [\tmp@x,\tmp@y]} + \endgroup +} +\def\mk@pos#1(#2){% + \wg@dbg{10}{^^JMarking `#2' with `#1' - start} + \coordinate[transform shape] (tmp) at (#2) {}; + \wg@get@nchor{tmp}{center} + \wg@dbg{3}{^^JMarking `#2' with `#1' - `\tmp@x',\tmp@y'} + \tikzset{zone point={#1}{\tmp@x}{\tmp@y}} +} +% \end{macrocode} +% +% For the key \texttt{zone path} to work, we need to be able to record +% the path as it moves along. To that end, we make a custom +% decoration that will do that for us, and, once the path is finished, +% write the path to our JSON file. +% +% \begin{macrocode} +\pgfdeclaredecoration{record path construction}{initial}{% + \state{initial}[width=0pt,next state=more]{ + \begingroup + \pgf@decorate@inputsegment@first + \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} + \xdef\wg@path{[\x,\y]} + \endgroup + }% + \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{% + \begingroup + \pgf@decorate@inputsegment@last + \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} + \xdef\wg@path{\wg@path,[\x,\y]} + \endgroup + } + \state{final}{% + \begingroup + \pgf@decorate@inputsegment@last + \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y} + \xdef\wg@path{\wg@path,[\x,\y]} + \endgroup + \mk@w{ \mk@i "zone path \wg@record@path@name": \@lbchar} + \mk@w{ \mk@i\space "path": [\wg@path] \@rbchar,} + } +}% +% \end{macrocode} +% +% Now we can make our environment +% +% The first thing we do is to use the \cs{info} macro to mark the +% image. Then we open our JSON file. We make a short-hand macro for +% writing to that file. The macro \cs{bd@i} records the current +% indention (which is important in JSON) +% +% \begin{macrocode} +\newenvironment{boardimage}[3][board]{% + \def\bd@n{#2} + \newcount\mk@point + \mk@point=0 + \let\oomk@i\mk@i% + \let\markpos\mk@pos% +% \end{macrocode} +% +% +% Then, to extract the label option, we make a dummy \texttt{node} +% with the styles \texttt{every hex} and \texttt{every hex node}, so +% we can extract that option. +% +% \begin{macrocode} + \info{dummy}{<>}{}% + %\tikz{}% + \tikz{\scoped[% + every hex/.try,every hex node/.try, + ]{% + \def\hex@col{0}% + \def\hex@row{0}% + \node[hex,inner sep=0,outer sep=0]{% + \message{^^JHex label: `\meaning\hex@label'}% + \global\let\mk@label\hex@label}}}% +% \end{macrocode} +% +% The next thing we do is to make an object. The first things we put +% in are the units used (``cm''), and the grid options. +% +% \begin{macrocode} + \info*{#2}{#1}{#3}% + \mk@w{ \mk@i "zones": \@lbchar}% + \edef\mk@i{\mk@i\space} + %% Everything is made into centimeters + \mk@w{ \mk@i "units": "cm",} + \hex@dbg{0}{Label: `\meaning\mk@label'} + \@ifundefined{mk@label}{}{\mk@w{ \mk@i "labels": "\mk@label",}} + %% Write out coordinate options as "coords" object + \mk@w{ \mk@i"coords": \@lbchar}% + \mk@w{ \mk@i "row": \@lbchar}% + \mk@w{ \mk@i\space "offset": \hex@coords@row@off,}% + \mk@w{ \mk@i\space "factor": \hex@coords@row@fac \@rbchar,}% + \mk@w{ \mk@i "column": \@lbchar}% + \mk@w{ \mk@i\space "offset": \hex@coords@col@off,}% + \mk@w{ \mk@i\space "factor": \hex@coords@col@fac,}% + \mk@w{ \mk@i\space "top short": "\hex@top@short@col",}% + \mk@w{ \mk@i\space "bottom short": "\hex@bot@short@col" \@rbchar}% + \mk@w{ \mk@i\@rbchar,}% +% \end{macrocode} +% +% We then monkey-patch \cs{boardframe} to also output coordinates to +% our JSON file. Note that this will probably be embedded in a +% different object. +% +% \begin{macrocode} + %% + \let\oldbo@rdframe\bo@rdframe% + \def\bo@rdframe[##1](##2)(##3){% + \oldbo@rdframe[##1](##2)(##3)% + \mk@w{ \mk@i"board frame": \@lbchar} + \mk@w{ \mk@i\space "lower left": [\llx,\lly],} + \mk@w{ \mk@i\space "upper right": [\urx,\ury],} + \mk@w{ \mk@i\space "margin": \margin,} + \mk@w{ \mk@i\space "width": \w,} + \mk@w{ \mk@i\space "height": \h \@rbchar,}}% +% \end{macrocode} +% +% Next, we make the style \texttt{zoned} to be applied to the +% \texttt{tikzpicture} environment. This records the bounding box of +% the full picture. +% +% \begin{macrocode} + \tikzset{ + zoned/.code={% Apply to whole picture + \pgfkeys{% + % This needs to be done in the picture! + /tikz/execute at end picture={% + \mk@w{ \mk@i "zoned": \@lbchar} + \mk@transform% + \mk@bb{current bounding box} + \mk@w{ \mk@i \@rbchar,} + } + } + }, +% \end{macrocode} +% +% The next style is the \texttt{zone scope}. At the start of the +% scope we record the current transformation matrix. Then we install +% a handler to extract the bounding box at the end of the scope. Note +% that we increase indention here. +% +% \begin{macrocode} + zone scope/.code={% + \mk@w{ \mk@i"zone scope ##1": \@lbchar} + \let\omk@i\mk@i + \edef\mk@i{\mk@i\space} + \mk@transform% + %\bd@w{ \@rbchar,} + \gdef\wg@export@box{##1}% + \pgfkeys{% + /tikz/local bounding box=wg export box, + /tikz/execute at end scope={ + \mk@bb{wg export box} + \let\mk@i\omk@i + \mk@w{ \mk@i\@rbchar,}}, + } % pgfkeys + }, % zone scope +% \end{macrocode} +% The next style gets the global coordinates of the current (0,0) +% point - f.ex. in a node - and outputs that +% \begin{macrocode} + zone point/.code n args={3}{ + \pgf@xa=##2 cm + \pgf@ya=##3 cm + \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}} + % \pgfpointtransformed{\pgfpoint{0pt}{0pt}} + \pgf@xa=\pgf@x + \pgf@ya=\pgf@y + \wg@pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult} + \wg@pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult} + \advance\mk@point1 + \global\mk@point=\mk@point + \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py] + \@rbchar, } + %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py} + }, + zone oob point/.code n args={3}{ + \pgf@xa=##2 cm + \pgf@ya=##3 cm + \advance\pgf@xa.1cm + \advance\pgf@ya.1cm + \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}} + % \pgfpointtransformed{\pgfpoint{0pt}{0pt}} + \pgf@xa=\pgf@x + \pgf@ya=\pgf@y + \wg@pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult} + \wg@pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult} + \advance\mk@point1 + \global\mk@point=\mk@point + \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", + "parent": "\wg@export@box", "type": "point", "coords": [\px,\py] + \@rbchar, } + %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py} + }, + zone global point/.code n args={3}{ + \advance\mk@point1 + \global\mk@point=\mk@point + \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py] + \@rbchar, } + }, +% \end{macrocode} +% +% The \texttt{zone path} style is a bit more simple, but only because +% the bulk of the work is done in a decoration. We need to be able to +% pass a name to that decoration, s we make a key for that. The user +% need not think about that though. +% +% \begin{macrocode} + /pgf/decoration/record path name/.store in=\wg@record@path@name, + zone path/.style={% + postaction={decorate,decoration={ + record path construction, + record path name=##1}} + } % zone path + }% tikzset +} +% \end{macrocode} +% +% That finishes the first part of the environment. At the end of the +% environment, we simple write the name of the picture, and close our +% JSON output. +% +% \begin{macrocode} +{% + \mk@w{ \mk@i "name": "\bd@n" }% + \let\mk@i\oomk@i% + \mk@w{ \mk@i\@rbchar}% + \end@info% +} +% \end{macrocode} +% +% Make battle markers. Mandatory argument is how many markers, +% optional is the group to add the markers to. +% +% \begin{macrocode} +\def\wg@gennumberm@rkers#1#2#3{ + \message{^^JNumbered markers: Type=`#1' Max=`#2' Category=`#3'} + \def\markers{} + \def\keys{} + \foreach \i in {1,...,#2}{% + \xdef\keys{/tikz/#1 \i/.style={/tikz/#1=\i},\keys} + \xdef\markers{\markers,#1 \i}} + {% + \nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}% +\tikzset{ + wg hidden unit/.pic={}, + wg hidden unit/.style={ + chit={ + frame={draw=none,fill=none}, + full=wg hidden unit}}} +\DeclareRobustCommand\battlemarkers[2][BattleMarkers]{% + \wg@gennumberm@rkers{battle marker}{#2}{#1}% + \message{^^JMake a hidden unit and add to Markers category} + {% + \nopagecolor% + \chitimages[Markers]{{wg hidden unit}}% + % + \info{battle-marker-icon}{icon}{}% + \tikz[scale=.7,transform shape]{\pic{battle marker=0};}% + \info{clear-battles-icon}{icon}{} + \tikz[scale=.4,transform shape]{% + \pic{eliminate icon}; + \pic[scale=.7,transform shape] at (-.3,0) {battle marker=0};}% + }% +} +% \end{macrocode} +% +% Make odds markers. Mandatory argument is a list of odds and fill colours. +% Optional is the group to add the markers to. +% +% \begin{macrocode} +\def\wg@gencolorm@rkers#1#2#3{% + \def\markers{} + \def\keys{} + \foreach \o/\f in {#2}{% + \ifx\o\f\def\f{white}\fi% + \message{^^JOdds marker `#1 \o' w/fill `\f'}% + \xdef\keys{/tikz/#1 \o/.style={/tikz/#1={\o,\f}},\keys} + \xdef\markers{\markers,#1 \o}} + {\nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}% +\DeclareRobustCommand\oddsmarkers[2][OddsMarkers]{% + \wg@gencolorm@rkers{odds marker}{#2}{#1}% + \info{odds-battles-icon}{icon}{} + \tikz[scale=.5,transform shape]{\pic{odds marker={?:?,white}}} + \info{resolve-battles-icon}{icon}{} + \tikz[scale=.3,transform shape]{% + \pic{dice}; + \pic[scale=1.2,transform shape] at (-.2,-.2) {battle marker=0};}% +} +% \end{macrocode} +% +% Make results markers. Mandatory argument is a list of results and +% fill colours. Optional is the group to add the markers to. +% +% \begin{macrocode} +\DeclareRobustCommand\resultmarkers[2][ResultMarkers]{% + \wg@gencolorm@rkers{result marker}{#2}{#1}} +% \end{macrocode} +% +% Common icons used by many modules +% +% \begin{macrocode} +\DeclareRobustCommand\commonicons[2]{% + \begingroup% + \nopagecolor% + \tikzset{icon/.style={scale=.4,transform shape}}% + % + \info{pool-icon}{icon}{} + \tikz[icon]{\pic{pool icon};} + % + \info{oob-icon}{icon}{}% + \tikz[icon]{\pic{oob icon={#1}{#2}};}% + % + \info{flip-icon}{icon}{}% + \tikz[icon]{\pic{flip icon};}% + % + \info{eliminate-icon}{icon}{}% + \tikz[icon]{\pic{eliminate icon};}% + % + \info{restore-icon}{icon}{}% + \tikz[icon]{\pic{restore icon};}% + % + \info{dice-icon}{icon}{}% + \tikz[icon,scale=.9]{\pic{dice};}% + % + \info{unit-icon}{icon}{}% + \tikz[icon,scale=.7]{% + \chit[fill=#1, + symbol={[ + scale line widths, + line width=1pt, + faction=friend, + command=land, + main=infantry, + scale=1.3](0,-.15)}]}% + \endgroup% +} +% \end{macrocode} +% +% \subsubsection{Making dice} +% \begin{Syntax} +% \cs{dice}\oarg{tikz-options}\oarg{node-options}\marg{name}\marg{name}\marg{list} +% \end{Syntax} +% \begin{enumerate} +% \item \meta{tikz-options} +% \item \meta{node-options} +% \item \meta{name} - an identifier - e.g., the same as \meta{shape}. +% \item \meta{shape} - one of \texttt{d4}, \texttt{d6}, \texttt{d8}, +% \texttt{d10}, \texttt{d12}, or \texttt{d20}. +% \item \meta{list} - list of pairs +% \meta{value}\texttt{/}\meta{printed}, where \meta{value} is the +% value, and \meta{printed} is the shown value. If \meta{printed} is +% left out, then \meta{value} is used. +% \end{enumerate} +% +% \begin{macrocode} +\def\dice{% + \@ifnextchar[{\wg@dice}{\wg@dice[]}%] +} +\def\wg@dice[#1]{% + \@ifnextchar[{\wg@@dice{#1}}{\wg@@dice{#1}[]} %] +} +\def\wg@@dice#1[#2]#3#4#5{% + \foreach \v/\p in {#5}{% + \info{#3-\v}{die-roll}{#3} + \tikz[#1]{ + \node[shape=#4,transform shape,draw=none,fill=black,opacity=.5] + at (.05,-.03){}; + \node[shape=#4,#2,transform shape]{\p};}}} +% \end{macrocode} +% +% \subsubsection{Hooks into chits, etc.} +% +% TO BE DONE: We could add hook the \texttt{hex} shape that would +% allow us to write out the settings for each of these. This would +% allow us to make data files that contain the information available +% in the \LaTeX{} code. +% +% If one then assumed that for example the upper left corner holds the +% start-up hex, then one could use that information. +% +% The code below exports the chit information to the JSON file. +% Together with the battle, odds, and result markers stuff above, this +% allows the exporter to almost automatically set up battle odds and +% result calculations. The fields exported are +% +% \begin{itemize} +% \item Left and right identifiers +% \item Upper left, upper right, lower left, and lower right +% identifiers. (some care must be taken if these contains graphics +% and not just text.) +% \item Factors +% \item NATO symbol +% \begin{itemize} +% \item Faction, command, echelon +% \item Mains +% \item Left, right, top, and bottom attributes and modifiers +% \item Below attribute +% \end{itemize} +% \end{itemize} +% +% The exporter can set up prototypes for NATO types, echelons, +% etc. The exporter can also set factors as marks on the units. +% +% \begin{macrocode} +\tikzset{ + zone turn/.store in=\zone@turn, + zone mult/.store in=\zone@mult +} +\def\@chit@rep@line#1#2{% + \@ifundefined{#2}{}{ + \edef\wg@chit@tmp{\csname #2\endcsname} + {\escapechar=`/ + \xdef\tmp{\detokenize\expandafter{\wg@chit@tmp} \@empty}} + % \message{^^J\meaning\@tmp -> \meaning\tmp} + \mk@w{ \mk@i\space "#1": "\tmp",}}} + +\def\do@chit@report{% + \chit@dbg{3}{Start of Chit Report} + \mk@w{ \mk@i "chit": \@lbchar} + \chit@dbg{3}{Report - ID} + \@ifundefined{id}{} {\mk@w{ \mk@i\space "id": "\id", }}% + \chit@dbg{3}{Report - Symbol: `\meaning\chit@symbol'} + \@ifundefined{chit@symbol}{}{\mk@w{ \mk@i\space "symbol": "true", }}% + \chit@dbg{3}{Report - Full: `\meaning\chit@full'} + \@chit@rep@line{full}{chit@full} + \chit@dbg{3}{Report - Factors: `\meaning\chit@factors'} + \@chit@rep@line{factors}{chit@factors}% + \chit@dbg{3}{Report - Left: `\meaning\chit@left'} + \@chit@rep@line{left}{chit@left}% + \chit@dbg{3}{Report - Right: : `\meaning\chit@right'} + \@chit@rep@line{right}{chit@right}% + \chit@dbg{3}{Report - Upper left: `\meaning\chit@upper@left'} + \@chit@rep@line{upper left}{chit@upper@left}% + \chit@dbg{3}{Report - Lower left: `\meaning\chit@lower@left'} + \@chit@rep@line{lower left}{chit@lower@left}% + \chit@dbg{3}{Report - Upper right: `\meaning\chit@upper@right} + \@chit@rep@line{upper right}{chit@upper@right}% + \chit@dbg{3}{Report - Lower right: `\meaning\chit@lower@right'} + \@chit@rep@line{lower right}{chit@lower@right}% + \chit@dbg{3}{Report - End comma} + \mk@w{ \mk@i\space "end": 0} + \@ifundefined{chit@symbol}{ + \mk@w{ \mk@i \@rbchar } + }{ + \mk@w{ \mk@i \@rbchar, }% NATOAPP6c will follow + }% + \chit@dbg{3}{End of Chit Report} +} +% \end{macrocode} +% Report out NATO App6 symbol settings +% \begin{macrocode} +\def\do@natoapp@report{% + \mk@w{ \mk@i "natoapp6c": \@lbchar} + \@chit@rep@line{id}{\id} + \@chit@rep@line{faction}{natoapp@fac} + \@chit@rep@line{command}{natoapp@cmd} + \@chit@rep@line{echelon}{natoapp@ech} + \@chit@rep@line{main}{natoapp@main} + \@chit@rep@line{left}{natoapp@left} + \@chit@rep@line{right}{natoapp@right} + \@chit@rep@line{upper}{natoapp@upper} + \@chit@rep@line{lower}{natoapp@lower} + \@chit@rep@line{below}{natoapp@below} + \mk@w{ \mk@i\space "end": 0} + \mk@w{ \mk@i \@rbchar} +} +% \end{macrocode} +% +% +% \iffalse +% +%\fi diff --git a/macros/latex/contrib/wargame/source/util/icons.dtx b/macros/latex/contrib/wargame/source/util/icons.dtx new file mode 100644 index 0000000000..14e2dc6d56 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/icons.dtx @@ -0,0 +1,118 @@ +% \iffalse +% -------------------------------------------------------------------- +% <*utils> +% \fi +% +% \subsubsection{VASSAL icons} +% +% +% Some icons that may be useful in VASSAL. We put them here so they +% may be used in manuals and the like too. +% +% +% First, the line style +% \begin{macrocode} +\tikzset{ + trash can line/.style={scale line widths,scale rounded corners, + line width=.5mm,->}, +} +% \end{macrocode} +% +% Then, the body and lid of a trash can. +% +% \begin{macrocode} +\tikzset{ + trash can body/.pic={% + \path[fill=black,scale line widths,scale rounded corners, + rounded corners=.05cm] + (-.3,.2) --++(.6,0) --++(-.1,-.7) --++(-.4,0) --cycle; + \path[fill=white] + (-.025,-.4) arc(180:360:.025) --++( 0,.5) arc(0:180:.025) --cycle; + \path[fill=white] + (-.125,-.4) arc(180:360:.025) --++(-.07,.5) arc(0:180:.025) --cycle; + \path[fill=white] + ( .075,-.4) arc(180:360:.025) --++( .07,.5) arc(0:180:.025) --cycle; + }, + trash can lid/.pic={% + \path[fill=black,scale line widths,scale rounded corners, + rounded corners=.05cm] + (-.35,.23)--++(.7,0)--++(-.07,.07)--++(-.56,0)--cycle; + \path[fill=black] + (-.15,.3) --++(.05,0) --++(0,.05) --++(.2,0) --++(0,-.05) + --++(.05,0) --++(0,.05) arc(0:90:.05) --++(-.2,0) arc(90:180:.05) + --cycle; + }, +} +% \end{macrocode} +% +% Then, a closed and open trash can +% +% \begin{macrocode} +\tikzset{ + trash can/.pic={ + \pic{trash can body}; + \pic{trash can lid}; + }, + trash can open/.pic={ + \pic{trash can body}; + \pic[rotate=-30] at (0,.1) {trash can lid}; + }, +} +% \end{macrocode} +% +% Now we can use that to generate some useful icons. +% +% \begin{macrocode} +\tikzset{ + eliminate icon/.pic={ + \pic{trash can open}; + \draw[trash can line,color=red!50!black] + (-.5,.2) to[looseness=1.5] (-.1,.23); + }, + restore icon/.pic={ + \pic{trash can open}; + \draw[trash can line,<-,color=green!50!black] + (-.5,.2) to[looseness=1.5] (-.1,.23); + }, + pool icon/.pic={ + \pic{trash can}; + }, +} +% \end{macrocode} +% +% These icons does not use the trash can picture. +% +% \begin{macrocode} +\tikzset{ + flip icon/.pic={ + \draw[scale line widths,scale rounded corners, + line width=1mm,->,color=blue!50!black] + (-.5,-.5) arc(180:0:.5);% (.5,-.5); + }, + pics/oob icon/.style n args={2}{code={% + \begin{scope}[box/.style args={##1,##2,##3,##4}{ + minimum width=##1cm, + minimum height=##2cm, + fill=##3, + anchor=##4, + draw=gray!50!black, + scale line widths, + line width=.5pt, + transform shape}, + under/.style={ + below=.05cm of ##1} + ] + \node[box={.5,.2,#1,north west,fill=#1}] (r1) at (.05,.45) {}; + \node[under=r1.south west,box={.3,.25,#1,north west}] (r2) {}; + \node[under=r2.south west,box={.2,.3, #1,north west}] (r3) {}; + \node[box={.2,.4,#2,north east}] (l1) at (-.05,.45) {}; + \end{scope} + } + } +} +% \end{macrocode} +% +% \iffalse +% -------------------------------------------------------------------- +% +% \fi diff --git a/macros/latex/contrib/wargame/source/util/misc.dtx b/macros/latex/contrib/wargame/source/util/misc.dtx new file mode 100644 index 0000000000..eee931a164 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/misc.dtx @@ -0,0 +1,145 @@ +% \iffalse +% -------------------------------------------------------------------- +%<*utils> +% \fi +% +% \subsubsection{Miscellaneous macros} +% \begin{Macro}{\wargamelogo} +% +% This will produce the logo for this package. +% +% +% \begin{center} +% \tikz{\wargamelogo} +% \end{center} +% +% \begin{macrocode} +\tikzset{ + wargame logo text/.style={ + font=\sffamily\bfseries\fontsize{12}{14}\selectfont, + scale=2.8, + inner sep=0, + text width=1.8cm, + transform shape, + align=center}, + wargame logo text content/.store in=\wg@logo@text@content, + wargame logo text content={{\huge\LaTeX} wargame}, + wargame logo chit/.style={ + chit={symbol={[ + faction=friendly, + command=land, + echelon=division, + main=infantry]}, + factors={chit/2 factors={4,3}}, + left={chit/identifier=III}, + right={chit/small identifier={10\textsuperscript{th}}}, + color=white, + fill=red!50!black + } + }, + wargame logo/.style={ + transform shape, + every hex/.style={fill=gray!5!white,draw=gray!75!black}, + hex/first row is=0, + hex/first column is=0, + hex/short top columns=none, + hex/short bottom columns=none, + hex/row direction is=normal, + hex/column direction is=normal + } +} +\newcommand\wargamelogo[1][]{% + \begin{scope}[wargame logo,#1] + \node[hex={fill=gray!30!white}] (logo center) at (hex cs:c=0,r=0) {}; + \node[hex={terrain=light woods}](logo light woods) at (hex cs:c=0,r=1) {}; + \node[hex={terrain=city}] (logo city) at (hex cs:c=0,r=-1){}; + \node[hex={terrain=woods}] (logo woods) at (hex cs:c=-1,r=0){}; + \node[hex={terrain=mountains}] (logo mountains) at (hex cs:c=-1,r=1){}; + \node[hex={terrain=beach}] (logo beach) at (hex cs:c=1,r=1) {}; + \node[hex={terrain=swamp}] (logo swamp) at (hex cs:c=1,r=0) {}; + \node[wargame logo chit] (logo chit) at (hex cs:) {}; + \node[wargame logo text] (logo text) {\wg@logo@text@content}; + \end{scope}} +% \end{macrocode} +% +% \end{Macro} +% \begin{Macro}{\wg@dbg} +% Debugging support. The counter \cs{wargamedbglvl} sets the debug +% level. The package code then uses \cs{wg@dbg} to print out +% debugging messages. This macro takes two arguments --- the first +% is the \emph{least} debug level at which the message is printed, and +% the second is the message it self. +% +% \begin{macrocode} +\newcount\wargamedbglvl\wargamedbglvl=0 +\def\wg@dbg#1#2{% + \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi} +% \end{macrocode} +% \end{Macro} +% +% \begin{Macro}{\wg@addto@macro} +% +% The macro \cs{wg@addto@macro}\marg{macro}\marg{other} adds the +% definition of the macro \meta{other} to the macro \meta{macro}. +% This uses the \cs{toks} trick of storing the \emph{tokens} of the +% definition of a \meta{macro} and \meta{other} into \spec{@} and +% expanding that token into the definition of \meta{macro}. +% Effectively, this means that the top-level definition of +% \meta{macro} and \meta{other} are expanded (i.e., macros used in +% the definition of either macro is \emph{not} expanded) and then +% that becomes the new definition of \meta{macro}. +% +% We will use this macro to do \emph{shallow} definitions of macros +% to contain keys and such. +% +% \begin{macrocode} +\long\def\wg@addto@macro#1#2{% + \begingroup + \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}% + \xdef#1{\the\toks@}% + \endgroup} +% \end{macrocode} +% \end{Macro} +% +% +% \begin{Macro}{\wg@sub@nchor} +% Get anchor from sub node. We cannot use \cs{pgfpointanchor} since +% that returns the anchor coordinates in the global coordinate +% system. +% +% \begin{macrocode} +\def\wg@sub@nchor#1#2{% + \wg@dbg{3}{^^JGet `#2' in `#1'}% + \@ifundefined{pgf@sh@ns@#1}{% + \pgf@x=0cm\pgf@y=0cm}{% + \pgf@process{% + \csname pgf@sh@ma@#1\endcsname% MW + \csname pgf@sh@np@#1\endcsname% + \pgf@sh@reanchor{\csname pgf@sh@ns@#1\endcsname}{#2}}}% + \wg@dbg{10}{-> \the\pgf@x,\the\pgf@y}% +} +% \end{macrocode} +% \end{Macro} +% +% +% Scratch dimensions +% +% \begin{macrocode} +\newdimen\wg@tmpa +\newdimen\wg@tmpb +\newdimen\wg@tmpc +\newdimen\wg@tmpd +% \end{macrocode} +% +% +% Macro to easy restore a saved path +% +% \begin{macrocode} +\def\settosave#1{ + \pgfsyssoftpath@setcurrentpath{#1}} +% \end{macrocode} +% +% \iffalse +% -------------------------------------------------------------------- +% +% \fi diff --git a/macros/latex/contrib/wargame/source/util/randomid.dtx b/macros/latex/contrib/wargame/source/util/randomid.dtx new file mode 100644 index 0000000000..be986c8857 --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/randomid.dtx @@ -0,0 +1,22 @@ +% \iffalse +% -------------------------------------------------------------------- +% <*utils> +% \fi +% +% \subsubsection{Random IDs} +% +% This macro sets the macro \cs{wg@uuid} to some random hex number. +% +% \begin{macrocode} +\def\wg@r@ndom@id{% + \def\wg@uuid{} + \foreach \i in {1,...,8}{% + \pgfmathparse{Hex(random(0,15))} + \xdef\wg@uuid{\wg@uuid\pgfmathresult}}} +% \end{macrocode} +% +% +% \iffalse +% -------------------------------------------------------------------- +% +% \fi diff --git a/macros/latex/contrib/wargame/source/util/tikz.dtx b/macros/latex/contrib/wargame/source/util/tikz.dtx new file mode 100644 index 0000000000..fa31a7b58c --- /dev/null +++ b/macros/latex/contrib/wargame/source/util/tikz.dtx @@ -0,0 +1,196 @@ +% \iffalse +% -------------------------------------------------------------------- +% <*utils> +% \fi +% +% \subsubsection{Other Tikz utilities} +% +% \begin{TikzKey}{tikz/reverseclip} +% +% A reverse clipping path. This is used to cut out stuff outside of +% path defined. +% +% \begin{macrocode} +\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) -- + (current bounding box.south east) -- + (current bounding box.south west) -- + (current bounding box.north west) -- + (current bounding box.north east)}] +% \end{macrocode} +% \end{TikzKey} +% +% \begin{TikzKey}{tikz/clip even odd rule} +% A reverse clipping path +% +% \begin{macrocode} +\tikzset{ + clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey +} +% \end{macrocode} +% \end{TikzKey} +% +% +% \begin{TikzKey}{tikz/invclip} +% +% Inverse clipping. This should be an option \emph{after} the path to +% do the inverse clipping by. This works by adding a \emph{large} +% (page) path to the current path, and then use that as clipping. +% +% \begin{macrocode} +\tikzset{ + invclip/.style={ + clip,insert path= + [clip even odd rule]{ + [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen) + } + }, +} +% \end{macrocode} +% \end{TikzKey} +% +% \begin{TikzKey}{save clip} +% +% An option for use with sub-elements of NATO App 6(c) or chit +% nodes. This will save the current path as a clipping path for the +% next paths to be drawn in the sub-element +% +% \begin{macrocode} +\newif\ifwg@s@ve\wg@s@vefalse +\tikzset{ + save clip/.is choice, + save clip/true/.code={\global\wg@s@vetrue}, + save clip/false/.code={\global\wg@s@vefalse}, + save clip/.default={true}, + save clip/.initial={false}, +} +% \end{macrocode} +% \end{TikzKey} +% +% \begin{TikzKey}{scale line widths} +% +% Scales any line width specified in the node options. +% +% Use like +% +% \begin{verbatim} +% \tikzset{ +% some/.style={ +% scale line widths, +% line width=1pt} +% } +% \end{verbatim} +% +% Note that the order is important. +% +% \begin{macrocode} +% Save pgf rounded corners macro +% \let\wg@pgfsetcornersarced\pgfsetcornersarced +\def\wg@setcornersarched#1{% + \ifx|#1|\else% + \edef\pgf@corner@arc{{#1}{#1}}% + \pgf@arccornerstrue% + \ifdim#1=0pt% + \pgf@arccornersfalse% + \fi\fi} +\newdimen\wg@lw@scaled\wg@lw@scaled=1pt +\def\wg@getscale{% + \pgfgettransformentries{% + \wg@jaca}{% + \wg@jacb}{% + \wg@jacc}{% + \wg@jacd}{% + \wg@tmp}{% + \wg@tmp}% + \pgfmathsetmacro{\wg@jac}{sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))}% + \wg@dbg{4}{Scale is \wg@jac} + \xdef\wg@scale{\wg@jac}} +\def\wg@scaled#1{% + \wg@getscale% + \wg@dbg{4}{Scaling #1 by \wg@scale} + \pgfmathsetmacro{\wg@tmp}{\wg@scale*#1}% + \xdef\wg@tmp{\wg@tmp}% + \xdef\wg@lw@scale{\wg@tmp}% + \wg@dbg{4}{Scaled #1 -> \wg@tmp}} +%% \message{^^JRounded corners: \meaning\pgfsetcornersarced} +\tikzset{ + %% Get current scale and store in \wg@scale + get scale/.code={\wg@getscale}, + scale line widths/.style={% + /utils/exec=\def\tikz@semiaddlinewidth##1{% + \wg@scaled{##1} + \wg@lw@scaled=\wg@tmp pt + \tikz@addoption{\pgfsetlinewidth{\wg@lw@scaled}}% + \wg@dbg{4}{Added scaled option \wg@tmp} + \pgfmathsetlength\pgflinewidth{\wg@tmp pt} + \wg@dbg{4}{Did set line width \wg@tmp pt} + } + }, + scale rounded corners/.style={% + /utils/exec=\def\pgfsetcornersarced##1{% + \pgf@process{##1}% + \pgf@xa=\pgf@x% + \wg@scaled{\the\pgf@xa}% + % \tikz@addoption{\wg@setcornersarched{\wg@tmp pt}}% + \wg@dbg{4}{Scaled rounded corners: \the\pgf@xa -> \wg@tmp}% + \wg@setcornersarched{\wg@tmp pt}% + } + }, + relative line width/.style={% + /utils/exec=\def\tikz@semiaddlinewidth##1{% + \wg@dbg{4}{Relative line width #1 times ##1}% + \pgfmathsetmacro{\wg@lv}{#1*##1}% + \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}% + \pgfmathsetlength\pgflinewidth{\wg@lw pt}}} +} +% \end{macrocode} +% \end{TikzKey} +% +% \begin{TikzKey}{sub pic actions} +% +% This is key that propagates actions to sub pictures of pictures. +% The normal \texttt{pic actions} cannot be used as it causes an +% infinite loop. +% +% \begin{macrocode} +\tikzset{ + sub pic actions/.code={% + \tikz@picmode% + \edef\opts{% + \iftikz@mode@draw draw,\else draw=none,\fi + \iftikz@mode@fill fill\else fill=none\fi} + \wg@dbg{5}{^^JSub Mode: \meaning\tikz@picmode \meaning\opts} + \pgfset{/tikz/.cd} + \pgfkeysalsofrom{\opts} + }} +% \end{macrocode} +% \end{TikzKey} +% +% \begin{TikzKey}{wg/debug show} +% +% Show debugging information +% +% \begin{macrocode} +\tikzset{ + wg/debug show/.code={% + \extractcolorspec{pgfstrokecolor}{\wg@tmp@fg} + \def\wg@tmp@bg{none} + \@ifundefinedcolor{pgffillcolor}{}{ + \extractcolorspec{pgffillcolor}{\wg@tmp@bg}} + \begingroup + \tikz@mode + \wargamedbglvl=#1 + \wg@dbg{3}{Drawing with w/stroke `\wg@tmp@fg' + (\tikz@strokecolor,\iftikz@mode@draw\else not\space\fi drawing) + and fill `\wg@tmp@bg' (\tikz@fillcolor,\iftikz@mode@fill\else + not\space\fi filling)} + \endgroup + } +} +% \end{macrocode} +% \end{TikzKey} +% +% +% \iffalse +% -------------------------------------------------------------------- +% +% \fi diff --git a/macros/latex/contrib/wargame/source/utils/wgexport.py b/macros/latex/contrib/wargame/source/utils/wgexport.py index 9ad370cfcd..a09a7e4257 100755 --- a/macros/latex/contrib/wargame/source/utils/wgexport.py +++ b/macros/latex/contrib/wargame/source/utils/wgexport.py @@ -1,1885 +1,6192 @@ #!/usr/bin/env python +# Script collected from other scripts +# +# ../vassal/vassal.py +# latexexporter.py +# main.py # -from pprint import pprint - # ==================================================================== -def add_pws(opw=None,upw=None): - '''Add password options to the poppler commands - - Parameters - ---------- - opw : str - Owner password - upw : str - User password - - Returns - ------- - opts : list - List of arguments - ''' - args = [] - if upw is not None: - args.extend(['-upw',upw]) - - if opw is not None: - args.extend(['-opw',opw]) +# From ../vassal/vassal.py +# Script collected from other scripts +# +# ../common/singleton.py +# ../common/verbose.py +# ../common/verboseguard.py +# base.py +# element.py +# globalkey.py +# gameelements.py +# mapelements.py +# globalproperty.py +# turn.py +# documentation.py +# player.py +# chessclock.py +# widget.py +# grid.py +# zone.py +# board.py +# map.py +# chart.py +# command.py +# trait.py +# withtraits.py +# traits/dynamicproperty.py +# traits/globalproperty.py +# traits/prototype.py +# traits/place.py +# traits/report.py +# traits/calculatedproperty.py +# traits/restrictcommand.py +# traits/label.py +# traits/layer.py +# traits/globalcommand.py +# traits/globalhotkey.py +# traits/nostack.py +# traits/deselect.py +# traits/restrictaccess.py +# traits/rotate.py +# traits/stack.py +# traits/mark.py +# traits/mask.py +# traits/trail.py +# traits/delete.py +# traits/sendto.py +# traits/moved.py +# traits/skel.py +# traits/submenu.py +# traits/basic.py +# traits/trigger.py +# traits/nonrect.py +# traits/click.py +# game.py +# buildfile.py +# moduledata.py +# save.py +# vsav.py +# vmod.py +# exporter.py +# +# ==================================================================== +# From ../common/singleton.py +# ==================================================================== +class Singleton(type): + '''Meta base class for singletons''' + _instances = {} + def __call__(cls, *args, **kwargs): + '''Create the singleton object or returned existing + + Parameters + ---------- + args : tuple + Arguments + kwargs : dict + Keyword arguments + ''' + if cls not in cls._instances: + cls._instances[cls] = \ + super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] - return args +# +# EOF +# +# ==================================================================== +# From ../common/verbose.py +# -------------------------------------------------------------------- + +class Verbose(metaclass=Singleton): + def __init__(self,verbose=False): + '''Singleton for writing message to screen, contigent on setting + + Parameters + ---------- + verbose : bool + Whether to show messages or not + ''' + self._indent = '' + self._verbose = verbose + + def setVerbose(self,verbose): + '''Set whether to print or not + + Parameters + ---------- + verbose : bool + Whether to show messages or not + ''' + self._verbose = verbose + + @property + def verbose(self): + '''Test if this is verbose''' + return self._verbose + + def message(self,*args,**kwargs): + '''Write messsage if verbose + + Parameters + ---------- + args : tuple + Arguments + kwargs : dict + Keyword arguments + ''' + if not self._verbose: return + if not kwargs.pop('noindent', False): + print(self._indent,end='') + print(*args,**kwargs) + + def incr(self): + '''Increment indention''' + self._indent += ' ' + + def decr(self): + '''Decrement indention''' + if len(self._indent) > 0: + self._indent = self._indent[:-1] +# +# EOF +# +# ==================================================================== +# From ../common/verboseguard.py # -------------------------------------------------------------------- -def create_proc(args): - '''Create a process - Parameters - ---------- - args : list - List of commmand line arguments +class VerboseGuard: + def __init__(self,*args,**kwargs): + '''A guard pattern that increases verbose indention - Returns - ------- - proc : subprocess.Process - Process handle - ''' - from os import environ - from subprocess import Popen, PIPE, TimeoutExpired + This is a context manager. The arguments passed are used for + an initial message, before increasinig indention. - return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE) - -# -------------------------------------------------------------------- -def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None): - '''Get dictionary of PDF information + Parameters + ---------- + args : tuple + Arguments + kwargs : dict + Keyword arguments + ''' + Verbose().message(*args,**kwargs) - Parameters - ---------- - pdfname : str - File name - opw : str - Owner password - upw : str - User password - timeout : int - Timeout in seconds or None + def __bool_(self): + '''Test if verbose''' + return Verbose().verbose - Returns - ------- - info : dict - PDF information - ''' - from subprocess import TimeoutExpired - - args = ['pdfinfo', pdfname] - args.extend(add_pws(opw=opw,upw=upw)) + def __enter__(self): + '''Enter context''' + Verbose().incr() + return self - proc = create_proc(args) + def __exit__(self,*args): + '''Exit context''' + Verbose().decr() - try: - out, err = proc.communicate(timeout=timeout) - except Exception as e: - proc.kill() - proc.communicate() - raise RuntimeError(f'Failed to get PDF info: {e}') - - d = {} - for field in out.decode("utf8", "ignore").split("\n"): - sf = field.split(":") - key, value = sf[0], ":".join(sf[1:]) - if key != "": - d[key] = (int(value.strip()) if key == 'Pages' else value.strip()) + def __call__(self,*args,**kwargs): + '''Write a message at current indention level + + Parameters + ---------- + args : tuple + Arguments + kwargs : dict + Keyword arguments + ''' + Verbose().message(*args,**kwargs) - if "Pages" not in d: - raise ValueError +# +# EOF +# +# ==================================================================== +# From base.py +# ==================================================================== +# Key encoding +SHIFT = 65 +CTRL = 130 +ALT = 520 +CTRL_SHIFT = CTRL+SHIFT +ALT_SHIFT = ALT+SHIFT +NONE = '\ue004' +NONE_MOD = 0 +def key(let,mod=CTRL): - return d - -# -------------------------------------------------------------------- -def get_images_info(infofile): - '''Read in image information from file + '''Encode a key sequence Parameters ---------- - infofile : str - file name - - Returns - ------- - images : list - List of image information + let : str + Key code (Letter) + mod : int + Modifier mask ''' - #from csv import DictReader - from json import load - - with open(infofile,'r') as file: - info = load(file) - #reader = DictReader(file,fieldnames=['page','name','type','sub type']) - #return [l for l in reader] - - return info + return f'{ord(let)},{mod}' +# +def hexcolor(s): + if isinstance(s,str): + s = s.replace('0x','') + if len(s) == 3: + r, g, b = [int(si,16)/16 for si in s] + elif len(s) == 6: + r = int(s[0:2],16) / 256 + g = int(s[2:4],16) / 256 + b = int(s[4:6],16) / 256 + else: + raise RuntimeError('3 or 6 hexadecimal digits for color string') + elif isinstance(s,int): + r = ((s >> 16) & 0xFF) / 256 + g = ((s >> 8) & 0xFF) / 256 + b = ((s >> 0) & 0xFF) / 256 + else: + raise RuntimeError('Hex colour must be string or integer') + + return rgb(int(r*256),int(g*256),int(b*256)) +# Colour encoding +def rgb(r,g,b): + '''Encode RGB colour -# -------------------------------------------------------------------- -def convert_page(pdfname,page,opw=None,upw=None,timeout=None): - '''Convert a single PDF page to an image - Parameters ---------- - pdfname : str - File name - page : int - Page number - opw : str - Owner password - upw : str - User password - timeout : int - Timeout in seconds or None - + r : int + Red channel + g : int + Green channel + b : int + Blue channel + Returns ------- - img : bytes - The bytes of the image + colour : str + RGB colour as a string ''' - args = ['pdftocairo', - '-singlefile', - '-f', str(page), - '-l', str(page), - '-png'] - args.extend(add_pws(opw=opw,upw=upw)) - args.append(pdfname) - args.append('-') - - proc = create_proc(args) - - try: - out, err = proc.communicate(timeout=timeout) - except Exception as e: - proc.kill() - proc.communicate() - raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}') - - from io import BytesIO - from PIL import Image - - img = out #Image.open(BytesIO(out)) - - return img - -# -------------------------------------------------------------------- -def ignore_entry(entry): - return entry['category'] in ['<>','<>'] - -# -------------------------------------------------------------------- -def convert_pages(pdfname,infoname, - opw=None,upw=None,timeout=None): - '''Convert pages in a PDF to images + return ','.join([str(r),str(g),str(b)]) - This creates a list of dictionaries with the image name and data - (as bytes) +def rgba(r,g,b,a): + '''Encode RGBA colour Parameters ---------- - pdfname : str - File name of PDF to read images from - infoname : str - File name of CSV to read image info from - opw : str - Owner password - upw : str - User password - timeout : int - Timeout in seconds or None + r : int + Red channel + g : int + Green channel + b : int + Blue channel + a : int + Alpha channel Returns ------- - imgs : list - List of images as dicts, info and the bytes of the images + colour : str + RGBA colour as a string ''' + return ','.join([str(r),str(g),str(b),str(a)]) - oargs = {'opw':opw,'upw':upw,'timeout':timeout} - docinfo = get_pdf_info(pdfname,**oargs) - info = get_images_info(infoname) - - if len(info)-1 != docinfo['Pages']: - raise RuntimeError(f'Number of pages in {pdfname} ' - f'({docinfo["Pages"]}) not {len(info)-1}') +def dumpTree(node,ind=''): + '''Dump the tree of nodes - for i in info: - if ignore_entry(i): continue - i['img'] = convert_page(pdfname,i['number'],**oargs) + Parameters + ---------- + node : xml.dom.Node + Node to dump + ind : str + Current indent + ''' + print(f'{ind}{node}') + for c in node.childNodes: + dumpTree(c,ind+' ') - return info +# +# EOF +# +# ==================================================================== +# From element.py # ==================================================================== -def remove_and_get(zipfname,*filenames): - '''Open zip file, and make a copy of it without the files listed - in entries. Return the data of files listed in entry.''' - from tempfile import mkdtemp - from zipfile import ZipFile - from shutil import move, rmtree - from os import path +class Element: + BUILD = 'VASSAL.build.' + MODULE = BUILD + 'module.' + WIDGET = BUILD + 'widget.' + MAP = MODULE + 'map.' + PICKER = MAP + 'boardPicker.' + BOARD = PICKER + 'board.' + def __init__(self,parent,tag,node=None,**kwargs): + '''Create a new element + + Parameters + ---------- + parent : Element + Parent element to add this element to + tag : str + Element tag + node : xml.dom.Node + If not None, then read attributes from that. Otherwise + set elements according to kwargs + kwargs : dict + Attribute keys and values. Only used if node is None + ''' + if parent is not None: + self._root = parent._root + self._node = (node if node is not None else + parent.addNode(tag,**kwargs)) + else: + self._root = None + self._node = None + + # ---------------------------------------------------------------- + # Attributes + def __contains__(self,key): + '''Check if element has attribute key''' + return self.hasAttribute(key) + + def __getitem__(self,key): + '''Get attribute key value''' + return self.getAttribute(key) - tempdir = mkdtemp() - ret = {} - try: - tempname = path.join(tempdir, 'new.zip') - with ZipFile(zipfname, 'r') as zipread: + def __setitem__(self,key,value): + '''Set attribute key value''' + self.setAttribute(key,value) - with ZipFile(tempname, 'w') as zipwrite: + def hasAttribute(self,k): + '''Check if element has attribute ''' + return self._node.hasAttribute(k) + + def getAttribute(self,k): + '''Get attribute key value''' + return self._node.getAttribute(k) + + def setAttribute(self,k,v): + '''Set attribute key value''' + self._node.setAttribute(k,str(v).lower() + if isinstance(v,bool) else str(v)) + + def setAttributes(self,**kwargs): + '''Set attributes to dictionary key and value''' + for k,v in kwargs.items(): + self.setAttribute(k,v) + + def getAttributes(self): + '''Get attributes as dict''' + return self._node.attributes + + # ---------------------------------------------------------------- + # Plain nodes + def getChildren(self): + '''Get child nodes (xml.dom.Node)''' + return self._node.childNodes + + # ---------------------------------------------------------------- + # Getters + # + # First generics + def getAsDict(self,tag='',key=None,enable=True): + '''Get elements with a specific tag as a dictionary + where the key is given by attribute key''' + cont = self._node.getElementsByTagName(tag) + if not enable or key is None: + return cont + + return {e.getAttribute(key): e for e in cont} + + def getAsOne(self,tag='',single=True): + '''Get elements with a specific tag, as a list. + If single is true, then assume we only have one such + child element, or fail.''' + cont = self._node.getElementsByTagName(tag) + if single and len(cont) != 1: + return None + return cont + + def getElementsByKey(self,cls,key='',asdict=True): + '''Get elments of a specific class as a dictionary, + where the key is set by the key attribute.''' + cont = self.getAsDict(cls.TAG,key,asdict) + if cont is None: return None + + if not asdict: return [cls(self,node=n) for n in cont] + + return {k : cls(self,node=n) for k, n in cont.items()} + + def getAllElements(self,cls,single=True): + '''Get elements with a specific tag, as a list. If single is + true, then assume we only have one such child element, or + fail. + + ''' + cont = self.getAsOne(cls.TAG,single=single) + if cont is None: return None + return [cls(self,node=n) for n in cont] + + def getSpecificElements(self,cls,key,*names,asdict=True): + '''Get all elements of specific class and that has the + attribute key, and the attribute value is in names + + ''' + cont = self.getAsOne(cls.TAG,single=False) + cand = [cls(self,node=n) for n in cont + if n.getAttribute(key) in names] + if asdict: + return {c[key] : c for c in cand} + return cand + + def getParent(self,cls,checkTag=True): + if self._node.parentNode is None: + return None + if checkTag and self._node.parentNode.tagName != cls.TAG: + return None + return cls(self,node=self._node.parentNode) + + def getParentOfClass(self,cls): + '''Searches back until we find the parent with the right + class, or none + ''' + t = {c.TAG: c for c in cls} + p = self._node.parentNode + while p is not None: + c = t.get(p.tagName,None) + if c is not None: return c(self,node=p) + p = p.parentNode + return None + + # ---------------------------------------------------------------- + # Adders + def addNode(self,tag,**attr): + '''Add a note to this element + + Parameters + ---------- + tag : str + Node tag name + attr : dict + Attributes to set + ''' + e = self._root.createElement(tag) + if self._node: self._node.appendChild(e) + + for k, v in attr.items(): + e.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v)) + + return e + + def addText(self,text): + '''Add a text child node to an element''' + t = self._root.createTextNode(text) + self._node.appendChild(t) + return t + + def getText(self): + '''Get contained text node content''' + if self._node.firstChild is None or \ + self._node.firstChild.nodeType != self._node.firstChild.TEXT_NODE: + return '' + return self._node.firstChild.nodeValue + - for item in zipread.infolist(): - data = zipread.read(item.filename) + def add(self,cls,**kwargs): + '''Add an element and return wrapped in cls object''' + return cls(self,node=None,**kwargs) - if item.filename not in filenames: - zipwrite.writestr(item, data) - else: - ret[item.filename] = data - - move(tempname, zipfname) - finally: - rmtree(tempdir) + def append(self,elem): + '''Append and element''' + if self._node.appendChild(elem._node): + return elem + return False - return ret + # ---------------------------------------------------------------- + def remove(self,elem): + '''Remove an element''' + try: + self._node.removeChild(elem._node) + except: + return None + return elem + # ---------------------------------------------------------------- + def insertBefore(self,toadd,ref): + '''Insert an element before another element''' + try: + self._node.insertBefore(toadd._node,ref._node) + except: + return None + return toadd # -------------------------------------------------------------------- -def add_back(zipfname,files): - '''Open a zip file for appending and add data in files +class DummyElement(Element): + def __init__(self,parent,node=None,**kwargs): + '''A dummy element we can use to select elements of different + classes - Files is a dictionary of filenames mapping to file content, - as returned by remove_and_get - ''' - from zipfile import ZipFile - - with ZipFile(zipfname, 'a') as zipadd: - for filename,data in files.items(): - zipadd.writestr(filename,data) + ''' + super(DummyElement,self).__init__(parent,'Dummy',node=node) -# ==================================================================== -# Contants used for tag names -BUILD = 'VASSAL.build.' -MODULE = BUILD + 'module.' -WIDGET = BUILD + 'widget.' -MAP = MODULE + 'map.' -PICKER = MAP + 'boardPicker.' +# +# EOF +# # ==================================================================== -def get_asdict(elem,tag,key,enable=True): - cont = elem.getElementsByTagName(tag) - if not enable: - return cont - - return {e.getAttribute(key): e for e in cont} +# From globalkey.py + +# -------------------------------------------------------------------- +class GlobalKey(Element): + SELECTED = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS' + def __init__(self, + parent, + tag, + node = None, + name = '', + buttonHotkey = '', + hotkey = '', + buttonText = '', + canDisable = False, + deckCount = '-1', + filter = '', + propertyGate = '', + reportFormat = '', + reportSingle = False, + singleMap = True, + target = SELECTED, + tooltip = '', + icon = ''): + ''' + Parameters + ---------- + - tag The XML tag to use + - parent Parent node + - node Optionally existing node + - name Name of key + - buttonHotkey Key in "global" scope + - hotkey Key to send to targeted pieces + - buttonText Text on button + - canDisable If true, disabled when propertyGate is true + - deckCount Number of decks (-1 is all) + - filter Which units to target + - propertyGate When true, disable + - reportFormat Chat message + - reportSingle Also show single piece reports + - singleMap Only originating map if True + - target Preselection filter (default selected pieces) + - tooltip Hover-over message + - icon Image to use as icon + + Default targets are selected units + ''' + nme = (name if len(name) > 0 else + tooltip if len(tooltip) > 0 else '') + tip = (tooltip if len(tooltip) > 0 else + name if len(name) > 0 else '') + super(GlobalKey,self).\ + __init__(parent, + tag, + node = node, + name = nme, + buttonHotkey = buttonHotkey, # This hot key + hotkey = hotkey, # Target hot key + buttonText = buttonText, + canDisable = canDisable, + deckCount = deckCount, + filter = filter, + propertyGate = propertyGate, + reportFormat = reportFormat, + reportSingle = reportSingle, + singleMap = singleMap, + target = target, + tooltip = tip, + icon = icon) +# +# EOF +# +# ==================================================================== +# From gameelements.py + +# -------------------------------------------------------------------- +class GameElementService: + def getGame(self): + return self.getParent(Game) + +# -------------------------------------------------------------------- +class GameElement(Element,GameElementService): + def __init__(self,game,tag,node=None,**kwargs): + super(GameElement,self).__init__(game,tag,node=node,**kwargs) + +# -------------------------------------------------------------------- +class Notes(GameElement): + TAG = Element.MODULE+'NotesWindow' + def __init__(self,elem,node=None, + name = 'Notes', + buttonText = '', + hotkey = key('N',ALT), + icon = '/images/notes.gif', + tooltip = 'Show notes window'): + super(Notes,self).__init__(elem,self.TAG,node=node, + name = name, + buttonText = buttonText, + hotkey = hotkey, + icon = icon, + tooltip = tooltip) + def encode(self): + return ['NOTES\t\\','PNOTES'] + +# -------------------------------------------------------------------- +class PredefinedSetup(GameElement): + TAG = Element.MODULE+'PredefinedSetup' + def __init__(self,elem,node=None, + name = '', + file = '', + useFile = False, + isMenu = False, + description = ''): + useFile = ((useFile or not isMenu) and + (file is not None and len(file) > 0)) + if file is None: file = '' + super(PredefinedSetup,self).__init__(elem,self.TAG,node=node, + name = name, + file = file, + useFile = useFile, + isMenu = isMenu, + description = description) + def addPredefinedSetup(self,**kwargs): + '''Add a `PredefinedSetup` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PredefinedSetup + The added element + ''' + return self.add(PredefinedSetup,**kwargs) + def getPredefinedSetups(self,asdict=True): + '''Get all PredefinedSetup element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `PredefinedSetup` elements. If `False`, return a list of all PredefinedSetup` children. + Returns + ------- + children : dict or list + Dictionary or list of `PredefinedSetup` children + ''' + return self.getElementsByKey(PredefinedSetup,'name',asdict) + + + + + +# -------------------------------------------------------------------- +class GlobalTranslatableMessages(GameElement): + TAG=Element.MODULE+'properties.GlobalTranslatableMessages' + def __init__(self,elem,node=None): + '''Translations + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + ''' + super(GlobalTranslatableMessages,self).\ + __init__(elem,self.TAG,node=node) + +# -------------------------------------------------------------------- +class Language(GameElement): + TAG = 'VASSAL.i18n.Language' + def __init__(self,elem,node=None,**kwargs): + super(Languate,self).__init__(sele,self.TAG,node=none,**kwargs) + +# -------------------------------------------------------------------- +class Chatter(GameElement): + TAG=Element.MODULE+'Chatter' + def __init__(self,elem,node=None,**kwargs): + '''Chat + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + kwargs : dict + Attributes + ''' + super(Chatter,self).__init__(elem,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class KeyNamer(GameElement): + TAG=Element.MODULE+'KeyNamer' + def __init__(self,elem,node=None,**kwargs): + '''Key namer (or help menu) + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + kwargs : dict + Attributes + ''' + super(KeyNamer,self).__init__(elem,self.TAG,node=node,**kwargs) + # -------------------------------------------------------------------- -def get_asone(elem,tag,enable=True): - cont = elem.getElementsByTagName(tag) - if enable and len(cont) != 1: - return None - return cont +# +# +# +# +# +# +# +# +# +class GlobalOptions(GameElement): + NEVER = 'Never' + ALWAYS = 'Always' + PROMPT = 'Use Preferences Setting' + TAG = Element.MODULE+'GlobalOptions' + def __init__(self,doc,node=None, + autoReport = PROMPT, + centerOnMove = PROMPT, + chatterHTMLSupport = ALWAYS, + hotKeysOnClosedWindows = NEVER, + inventoryForAll = ALWAYS, + nonOwnerUnmaskable = PROMPT, + playerIdFormat = "$playerName$", + promptString = "Opponents can unmask pieces", + sendToLocationMoveTrails = NEVER, + storeLeadingZeroIntegersAsStrings = False, + description = 'Global options', + dragThreshold = 10): + '''Set global options on the module + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + + autoReport : str='always' + centerOnMove : str Option + chatterHTMLSupport : str='never' + hotKeysOnClosedWindows : str='never' + inventoryForAll : str='always' + nonOwnerUnmaskable : str='never' + playerIdFormat : str='$PlayerName$' + promptString : str=? + sendToLocationMoveTrails : bool=false + storeLeadingZeroIntegersAsStrings : bool=False + ''' + super(GlobalOptions,self).\ + __init__(doc,self.TAG,node=node, + autoReport = autoReport, + centerOnMove = centerOnMove, + chatterHTMLSupport = chatterHTMLSupport, + hotKeysOnClosedWindows = hotKeysOnClosedWindows, + inventoryForAll = inventoryForAll, + nonOwnerUnmaskable = nonOwnerUnmaskable, + playerIdFormat = playerIdFormat, + promptString = promptString, + sendToLocationMoveTrails = sendToLocationMoveTrails, + storeLeadingZeroIntegersAsStrings = storeLeadingZeroIntegersAsStrings, + dragThreshold = dragThreshold, + description = description) + + def addOption(self,**kwargs): + '''Add a `Option` element to this + + Options known + - stepIcon - image file name (/images/StepForward16.gif) + - stepHotKey - key + - undoIcon - image file name (/images/Undo16.gif) + - undoHotKey - key + - serverControlsIcon - image file name (/images/connect.gif) + - serverControlsHotKey - key + - debugControlsIcon - image file name + - debugControlsHotKey - key + + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Option + The added element + ''' + return self.add(Option,**kwargs) + def getOptions(self): + return self.getElementsByKey(Option,'name') + + def addPreference(self,cls,**kwargs): + return self.add(cls,**kwargs) + + def addIntPreference(self,**kwargs): + return self.add(IntPreference,**kwargs) + + def addFloatPreference(self,**kwargs): + return self.add(FloatPreference,**kwargs) -# -------------------------------------------------------------------- -def get_doc(string): - from xml.dom.minidom import parseString + def addBoolPreference(self,**kwargs): + return self.add(BoolPreference,**kwargs) + + def addStrPreference(self,**kwargs): + return self.add(StrPreference,**kwargs) + + def addTextPreference(self,**kwargs): + return self.add(TextPreference,**kwargs) + + def addEnumPreference(self,**kwargs): + return self.add(EnumPreference,**kwargs) + + def getIntPreferences(self): + return self.getElementsByKey(IntPreference,'name') - return parseString(string) + def getFloatPreferences(self): + return self.getElementsByKey(FloatPreference,'name') -# -------------------------------------------------------------------- -def get_game(doc): - return doc.getElementsByTagName('VASSAL.build.GameModule')[0] + def getBoolPreferences(self): + return self.getElementsByKey(BoolPreference,'name') -# -------------------------------------------------------------------- -def get_globalproperties(doc,one=True): - return get_asone(doc,MODULE+'properties.GlobalProperties',one) + def getStrPreferences(self): + return self.getElementsByKey(StrPreference,'name') -# -------------------------------------------------------------------- -def get_globalproperty(doc,asdict=True): - '''A little confusing - this returns a dictionary''' - return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict) + def getTextPreferences(self): + return self.getElementsByKey(TextPreference,'name') -# -------------------------------------------------------------------- -def get_turntrack(doc,asdict=True): - return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict) + def getEnumPreferences(self): + return self.getElementsByKey(EnumPreference,'name') -# -------------------------------------------------------------------- -def get_turncounter(doc,asdict=True): - return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict) + def getPreferences(self): + retd = {} + for cls in [IntPreference, + FloatPreference, + BoolPreference, + StrPreference, + TextPreference, + EnumPreference]: + retd.update(self.getElementsByKey(cls,'name')) -# -------------------------------------------------------------------- -def get_turnlist(doc,asdict=True): - return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict) + return retd + # -------------------------------------------------------------------- -def get_turnhotkey(doc,asdict=True): - return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict) - +class Option(Element): + TAG = 'option' + def __init__(self,doc,node=None,name='',value=''): + super(Option,self).__init__(doc,tag=self.TAG,node=node,name=name) + self.addText(value) + + def getGlobalOptions(self): + return self.getParent(GlobalOptions) + +# -------------------------------------------------------------------- +class Preference(Element): + PREFS = 'VASSAL.preferences.' + def __init__(self, + doc, + tag, + node = None, + name = '', + default = '', + desc = '', + tab = '', + **kwargs): + '''Add a preference + + Parameters + ---------- + name : str + Name of property + default : str + Default value + desc : str + Description + tab : str + Preference tab to put in to + ''' + super(Preference,self).__init__(doc, + tag = tag, + node = node, + name = name, + default = default, + desc = desc, + tab = tab) + + def getGlobalOptions(self): + return self.getParent(GlobalOptions) + # -------------------------------------------------------------------- -def get_documentation(doc,one=True): - return get_asone(doc,MODULE+'Documentation',one) +class IntPreference(Preference): + TAG = Preference.PREFS+'IntegerPreference' + def __init__(self, + doc, + node = None, + name = '', + default = 0, + desc = '', + tab = ''): + super(IntPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = str(default), + desc = desc, + tab = tab) +# -------------------------------------------------------------------- +class FloatPreference(Preference): + TAG = Preference.PREFS+'DoublePreference' + def __init__(self, + doc, + node = None, + name = '', + default = 0., + desc = '', + tab = ''): + super(FloatPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = str(default), + desc = desc, + tab = tab) +# -------------------------------------------------------------------- +class BoolPreference(Preference): + TAG = Preference.PREFS+'BooleanPreference' + def __init__(self, + doc, + node = None, + name = '', + default = False, + desc = '', + tab = ''): + super(BoolPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = ('true' if default + else 'false'), + desc = desc, + tab = tab) +# -------------------------------------------------------------------- +class StrPreference(Preference): + TAG = Preference.PREFS+'StringPreference' + def __init__(self, + doc, + node = None, + name = '', + default = '', + desc = '', + tab = ''): + super(StrPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = default, + desc = desc, + tab = tab) +# -------------------------------------------------------------------- +class TextPreference(Preference): + TAG = Preference.PREFS+'TextPreference' + def __init__(self, + doc, + node = None, + name = '', + default = '', + desc = '', + tab = ''): + super(TextPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = (default + .replace('\n',' ')), + desc = desc, + tab = tab) +# -------------------------------------------------------------------- +class EnumPreference(Preference): + TAG = Preference.PREFS+'EnumPreference' + def __init__(self, + doc, + node = None, + name = '', + values = [], + default = '', + desc = '', + tab = ''): + ce = lambda v : str(v).replace(',',r'\,') + sl = [ce(v) for v in values] + df = ce(v) + assert df in sl, \ + f'Default value "{default}" not in list {":".join(values)}' + super(EnumPreference,self).__init__(doc, + tag = self.TAG, + node = node, + name = name, + default = df, + desc = desc, + tab = tab, + list = sl) + # -------------------------------------------------------------------- -def get_helpfiles(doc,key='fileName',asdict=True): - return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict) +# CurrentMap == "Board" +class Inventory(GameElement): + TAG = Element.MODULE+'Inventory' + def __init__(self,doc,node=None, + canDisable = False, + centerOnPiece = True, + disabledIcon = '', + drawPieces = True, + foldersOnly = False, + forwardKeystroke = True, + groupBy = '', + hotkey = key('I',ALT), + icon = '/images/inventory.gif', + include = '{}', + launchFunction = 'functionHide', + leafFormat = '$PieceName$', + name = '', + nonLeafFormat = '$PropertyValue$', + pieceZoom = '0.33', + pieceZoom2 = '0.5', + pieceZoom3 = '0.6', + propertyGate = '', + refreshHotkey = key('I',ALT_SHIFT), + showMenu = True, + sides = '', + sortFormat = '$PieceName$', + sortPieces = True, + sorting = 'alpha', + text = '', + tooltip = 'Show inventory of all pieces', + zoomOn = False): + super(Inventory,self).__init__(doc,self.TAG,node=node, + canDisable = canDisable, + centerOnPiece = centerOnPiece, + disabledIcon = disabledIcon, + drawPieces = drawPieces, + foldersOnly = foldersOnly, + forwardKeystroke = forwardKeystroke, + groupBy = groupBy, + hotkey = hotkey, + icon = icon, + include = include, + launchFunction = launchFunction, + leafFormat = leafFormat, + name = name, + nonLeafFormat = nonLeafFormat, + pieceZoom = pieceZoom, + pieceZoom2 = pieceZoom2, + pieceZoom3 = pieceZoom3, + propertyGate = propertyGate, + refreshHotkey = refreshHotkey, + showMenu = showMenu, + sides = sides, + sortFormat = sortFormat, + sortPieces = sortPieces, + sorting = sorting, + text = text, + tooltip = tooltip, + zoomOn = zoomOn) + + + -# -------------------------------------------------------------------- -def get_tutorials(doc,key='name',asdict=True): - return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict) + + -# -------------------------------------------------------------------- -def get_inventories(doc,key='name',asdict=False): - return get_asdict(doc,MODULE+'Inventory',key,asdict) -# -------------------------------------------------------------------- -def get_maps(doc,asdict=True): - return get_asdict(doc,MODULE+'Map','mapName',asdict) -# -------------------------------------------------------------------- -def get_widgetmap(doc,asdict=True): - return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict) -# -------------------------------------------------------------------- -def get_chartwindows(doc,asdict=True): - return get_asdict(doc,MODULE+'ChartWindow','name',asdict) -# -------------------------------------------------------------------- -def get_map(doc,name): - maps = get_maps(doc,True) - return maps.get(name, None) if maps else None # -------------------------------------------------------------------- -def get_boards(doc,asdict=True): - return get_asdict(doc,PICKER+'Board','name',asdict) +class Prototypes(GameElement): + TAG = Element.MODULE+'PrototypesContainer' + def __init__(self,game,node=None,**kwargs): + super(Prototypes,self).\ + __init__(game,self.TAG,node=node,**kwargs) -# -------------------------------------------------------------------- -def get_zoned(doc,one=True): - return get_asone(doc,PICKER+'board.ZonedGrid',one) + def addPrototype(self,**kwargs): + '''Add a `Prototype` element to this -# -------------------------------------------------------------------- -def get_zone(doc,asdict=True): - return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict) + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Prototype + The added element + ''' + return self.add(Prototype,**kwargs) + def getPrototypes(self,asdict=True): + '''Get all Prototype element(s) from this -# -------------------------------------------------------------------- -def get_hexgrid(doc,one=True): - return get_asone(doc,PICKER+'board.HexGrid',one) + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Prototype` elements. If `False`, return a list of all Prototype` children. + Returns + ------- + children : dict or list + Dictionary or list of `Prototype` children + ''' + return self.getElementsByKey(Prototype,'name',asdict=asdict) + + -# -------------------------------------------------------------------- -def get_squaregrid(doc,one=True): - return get_asone(doc,PICKER+'board.SquareGrid',one) # -------------------------------------------------------------------- -def get_regiongrid(doc,one=True): - return get_asone(doc,PICKER+'board.RegionGrid',one) - +class DiceButton(GameElement): + TAG=Element.MODULE+'DiceButton' + def __init__(self,elem,node=None, + addToTotal = 0, + canDisable = False, + hotkey = key('6',ALT), + icon = '/images/die.gif', + keepCount = 1, + keepDice = False, + keepOption = '>', + lockAdd = False, + lockDice = False, + lockPlus = False, + lockSides = False, + nDice = 1, + nSides = 6, + name = '1d6', + plus = 0, + prompt = False, + propertyGate = '', + reportFormat = '** $name$ = $result$ *** <$PlayerName$>;', + reportTotal = False, + sortDice = False, + text = '1d6', + tooltip = 'Roll a 1d6'): + super(DiceButton,self).\ + __init__(elem,self.TAG,node=node, + addToTotal = addToTotal, + canDisable = canDisable, + hotkey = hotkey, + icon = icon, + keepCount = keepCount, + keepDice = keepDice, + keepOption = keepOption, + lockAdd = lockAdd, + lockDice = lockDice, + lockPlus = lockPlus, + lockSides = lockSides, + nDice = nDice, + nSides = nSides, + name = name, + plus = plus, + prompt = prompt, + propertyGate = propertyGate, + reportFormat = reportFormat, + reportTotal = reportTotal, + sortDice = sortDice, + text = text, + tooltip = tooltip) + +# -------------------------------------------------------------------- +class GameMassKey(GlobalKey,GameElementService): + TAG = Element.MODULE+'GlobalKeyCommand' + def __init__(self,map,node=None, + name = '', + buttonHotkey = '', + hotkey = '', + buttonText = '', + canDisable = False, + deckCount = '-1', + filter = '', + propertyGate = '', + reportFormat = '', + reportSingle = False, + singleMap = True, + target = GlobalKey.SELECTED, + tooltip = '', + icon = ''): + '''Default targets are selected units''' + super(GameMassKey,self).\ + __init__(map, + self.TAG, + node = node, + name = name, + buttonHotkey = buttonHotkey, # This hot key + hotkey = hotkey, # Target hot key + buttonText = buttonText, + canDisable = canDisable, + deckCount = deckCount, + filter = filter, + propertyGate = propertyGate, + reportFormat = reportFormat, + reportSingle = reportSingle, + singleMap = singleMap, + target = target, + tooltip = tooltip, + icon = icon) + # -------------------------------------------------------------------- -def get_hexnumbering(doc,one=True): - return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one) +class StartupMassKey(GlobalKey,GameElementService): + TAG = Element.MODULE+'StartupGlobalKeyCommand' + FIRST_LAUNCH = 'firstLaunchOfSession' + EVERY_LAUNCH = 'everyLaunchOfSession' + START_GAME = 'startOfGameOnly' + def __init__(self, + map, + node = None, + name = '', + buttonHotkey = '', + hotkey = '', + buttonText = '', + canDisable = False, + deckCount = '-1', + filter = '', + propertyGate = '', + reportFormat = '', + reportSingle = False, + singleMap = True, + target = GlobalKey.SELECTED, + tooltip = '', + icon = '', + whenToApply = EVERY_LAUNCH): + '''Default targets are selected units''' + super(StartupMassKey,self).\ + __init__(map, + self.TAG, + node = node, + name = name, + buttonHotkey = buttonHotkey, # This hot key + hotkey = hotkey, # Target hot key + buttonText = buttonText, + canDisable = canDisable, + deckCount = deckCount, + filter = filter, + propertyGate = propertyGate, + reportFormat = reportFormat, + reportSingle = reportSingle, + singleMap = singleMap, + target = target, + tooltip = tooltip, + icon = icon) + if node is None: + self['whenToApply'] = whenToApply + +# -------------------------------------------------------------------- +class Menu(GameElement): + TAG = Element.MODULE+'ToolbarMenu' + def __init__(self, + game, + node = None, + canDisable = False, + description = '', + disabledIcon = '', + hotkey = '', + icon = '', + menuItems = [],# Button texts + propertyGate = '', + text = '',# Menu name + tooltip = ''): + if len(description) <= 0 and len(tooltip) > 0: + description = tooltip + if len(tooltip) <= 0 and len(description) > 0: + tooltip = description + super(Menu,self).\ + __init__(game, + self.TAG, + node = node, + canDisable = canDisable, + description = description, + disabledIcon = disabledIcon, + hotkey = hotkey, + icon = icon, + menuItems = ','.join(menuItems), + propertyGate = propertyGate, + text = text, + tooltip = tooltip) + # -------------------------------------------------------------------- -def get_regions(doc,asdict=True): - return get_asdict(doc,PICKER+'board.Region', 'name', asdict) - +class SymbolicDice(GameElement): + TAG = Element.MODULE+'SpecialDiceButton' + def __init__(self, + game, + node = None, + canDisable = False, + disabledIcon = '', + hotkey = key('6',ALT), + name = "Dice", # GP prefix + text = '', # Text on button + icon = '/images/die.gif', # Icon on button + format = '{name+": "+result1}', # Report + tooltip = 'Die roll', # Help + propertyGate = '', # Property to disable when T + resultButton = False, # Result on button? + resultChatter = True, # Result in Chatter? + resultWindow = False, # Result window? + backgroundColor = rgb(0xdd,0xdd,0xdd), # Window background + windowTitleResultFormat = "$name$", # Window title + windowX = '67', # Window size + windowY = '65'): + super(SymbolicDice,self).\ + __init__(game, + self.TAG, + node = node, + canDisable = canDisable, + disabledIcon = disabledIcon, + hotkey = hotkey, + name = name, + text = text, + icon = icon, + format = format, + tooltip = tooltip, + propertyGate = propertyGate, + resultButton = resultButton, + resultChatter = resultChatter, + resultWindow = resultWindow, + backgroundColor = backgroundColor, + windowTitleResultFormat = windowTitleResultFormat, + windowX = windowX, + windowY = windowY) + + def addDie(self,**kwargs): + return self.add(SpecialDie,**kwargs) + + def getSymbolicDice(self): + return self.getParent(SymbolicDice) + + # -------------------------------------------------------------------- -def get_squarenumbering(doc,one=True): - return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one) +class SpecialDie(GameElement): + TAG = Element.MODULE+'SpecialDie' + def __init__(self, + symbolic, # Symblic dice + node = None, + name = '', # Name of dice (no GP) + report = '{name+": "+result}', + faces = None): + super(SpecialDie,self).\ + __init__(symbolic, + self.TAG, + node = node, + name = name, + report = report) + if node is not None or faces is None: + return + if isinstance(faces,list): + faces = {i+1: f for i,f in enumerate(faces)} + for v,f in faces: + self.addFace(text = str(v), value = v, icon = f) + + def addFace(self,**kwargs): + self.add(DieFace,**kwargs) + + def getSymbolicDice(self): + return self.getParent(SymbolicDice) + -# -------------------------------------------------------------------- -def get_pieces(doc,asdict=True): - return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict) + + +# -------------------------------------------------------------------- +class DieFace(GameElement): + TAG = Element.MODULE+'SpecialDieFace' + def __init__(self, + special, # Special dice + node, # existing node + icon = '', # graphical representation + text = '', # Text representation + value = 0): # Value representation + super(DieFace,self).\ + __init__(special, + self.TAG, + node = node, + icon = icon, + text = text, + value = value) + + def getSpecialDie(self): + return self.getParent(SpecialDie) +# +# EOF +# +# ==================================================================== +# From mapelements.py + +# -------------------------------------------------------------------- +class MapElementService: + def getMap(self): + '''Get map - either a Map or WidgetMap''' + return self.getParentOfClass([WidgetMap,Map]) + # if self._parent is None: + # return None + # + # if 'WidgetMap' in self._parent.tagName: + # return self.getParent(WidgetMap) + # + # return self.getParent(Map) + def getGame(self): + m = self.getMap() + if m is not None: return m.getGame() + return None # -------------------------------------------------------------------- -def get_prototypecontainer(doc,one=True): - return get_asone(doc,MODULE+'PrototypesContainer',one) +class MapElement(Element,MapElementService): + def __init__(self,map,tag,node=None,**kwargs): + super(MapElement,self).__init__(map,tag,node=node,**kwargs) -# -------------------------------------------------------------------- -def get_prototypes(doc,asdict=True): - return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict) # -------------------------------------------------------------------- -def get_masskey(map,asdict=True): - return get_asdict(map,MAP+'MassKeyCommand','name', asdict) - +class StackMetrics(MapElement): + TAG=Element.MAP+'StackMetrics' + def __init__(self,map,node=None, + bottom = key('(',0), + down = key('%',0), + top = key('&',0), + up = key("'",0), + disabled = False, + exSepX = 6, # Expanded (after double click) + exSepY = 18, # Expanded (after double click) + unexSepX = 8, # Compact + unexSepY = 16): # Compact + super(StackMetrics,self).__init__(map,self.TAG,node=node, + bottom = bottom, + disabled = disabled, + down = down, + exSepX = exSepX, + exSepY = exSepY, + top = top, + unexSepX = unexSepX, + unexSepY = unexSepY, + up = up) + +# -------------------------------------------------------------------- +class ImageSaver(MapElement): + TAG=Element.MAP+'ImageSaver' + def __init__(self,map,node=None, + buttonText = '', + canDisable = False, + hotkey = '', + icon = '/images/camera.gif', + propertyGate = '', + tooltip = 'Save map as PNG image'): + super(ImageSaver,self).__init__(map,self.TAG,node=node, + buttonText = buttonText, + canDisable = canDisable, + hotkey = hotkey, + icon = icon, + propertyGate = propertyGate, + tooltip = tooltip) +# -------------------------------------------------------------------- +class TextSaver(MapElement): + TAG=Element.MAP+'TextSaver' + def __init__(self,map,node=None, + buttonText = '', + canDisable = False, + hotkey = '', + icon = '/images/camera.gif', + propertyGate = '', + tooltip = 'Save map as text'): + super(TextSaver,self).__init__(map,self.TAG,node=node, + buttonText = buttonText, + canDisable = canDisable, + hotkey = hotkey, + icon = icon, + propertyGate = propertyGate, + tooltip = tooltip) + + +# -------------------------------------------------------------------- +class ForwardToChatter(MapElement): + TAG=Element.MAP+'ForwardToChatter' + def __init__(self,map,node=None,**kwargs): + super(ForwardToChatter,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class MenuDisplayer(MapElement): + TAG=Element.MAP+'MenuDisplayer' + def __init__(self,map,node=None,**kwargs): + super(MenuDisplayer,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class MapCenterer(MapElement): + TAG=Element.MAP+'MapCenterer' + def __init__(self,map,node=None,**kwargs): + super(MapCenterer,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class StackExpander(MapElement): + TAG=Element.MAP+'StackExpander' + def __init__(self,map,node=None,**kwargs): + super(StackExpander,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class PieceMover(MapElement): + TAG=Element.MAP+'PieceMover' + def __init__(self,map,node=None,**kwargs): + super(PieceMover,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class SelectionHighlighters(MapElement): + TAG=Element.MAP+'SelectionHighlighters' + def __init__(self,map,node=None,**kwargs): + super(SelectionHighlighters,self).\ + __init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class KeyBufferer(MapElement): + TAG=Element.MAP+'KeyBufferer' + def __init__(self,map,node=None,**kwargs): + super(KeyBufferer,self).__init__(map,self.TAG,node=node,**kwargs) + +# -------------------------------------------------------------------- +class HighlightLastMoved(MapElement): + TAG=Element.MAP+'HighlightLastMoved' + def __init__(self,map,node=None, + color = rgb(255,0,0), + enabled = True, + thickness = 2): + super(HighlightLastMoved,self).__init__(map,self.TAG,node=node, + color = color, + enabled = enabled, + thickness = thickness) + +# -------------------------------------------------------------------- +class CounterDetailViewer(MapElement): + TAG=Element.MAP+'CounterDetailViewer' + TOP_LAYER = 'from top-most layer only' + ALL_LAYERS = 'from all layers' + INC_LAYERS = 'from listed layers only' + EXC_LAYERS = 'from layers other than those listed' + FILTER = 'by using a property filter' + def __init__(self,map,node=None, + borderWidth = 0, + centerAll = False, + centerText = False, + combineCounterSummary = False, + counterReportFormat = '', + delay = 700, + description = '', + display = TOP_LAYER, + emptyHexReportForma = '$LocationName$', + enableHTML = True, + extraTextPadding = 0, + fgColor = rgb(0,0,0), + fontSize = 9, + graphicsZoom = 1.0, + hotkey = key('\n',0), + layerList = '', + minDisplayPieces = 2, + propertyFilter = '', + showDeck = False, + showDeckDepth = 1, + showDeckMasked = False, + showMoveSelectde = False, + showNoStack = False, + showNonMovable = False, + showOverlap = False, + showgraph = True, + showgraphsingle = False, + showtext = False, + showtextsingle = False, + stretchWidthSummary = False, + summaryReportFormat = '$LocationName$', + unrotatePieces = False, + version = 3, + verticalOffset = 0, + verticalTopText = 5, + zoomlevel = 1.0): # showTerrain attributes + super(CounterDetailViewer,self)\ + .__init__(map,self.TAG,node=node, + borderWidth = borderWidth, + centerAll = centerAll, + centerText = centerText, + combineCounterSummary = combineCounterSummary, + counterReportFormat = counterReportFormat, + delay = delay, + description = description, + display = display, + emptyHexReportForma = emptyHexReportForma, + enableHTML = enableHTML, + extraTextPadding = extraTextPadding, + fgColor = fgColor, + fontSize = fontSize, + graphicsZoom = graphicsZoom, + hotkey = hotkey, + layerList = layerList, + minDisplayPieces = minDisplayPieces, + propertyFilter = propertyFilter, + showDeck = showDeck, + showDeckDepth = showDeckDepth, + showDeckMasked = showDeckMasked, + showMoveSelectde = showMoveSelectde, + showNoStack = showNoStack, + showNonMovable = showNonMovable, + showOverlap = showOverlap, + showgraph = showgraph, + showgraphsingle = showgraphsingle, + showtext = showtext, + showtextsingle = showtextsingle, + stretchWidthSummary = stretchWidthSummary, + summaryReportFormat = summaryReportFormat, + unrotatePieces = unrotatePieces, + version = version, + verticalOffset = verticalOffset, + verticalTopText = verticalTopText, + zoomlevel = zoomlevel) + +# -------------------------------------------------------------------- +class GlobalMap(MapElement): + TAG=Element.MAP+'GlobalMap' + def __init__(self,map,node=None, + buttonText = '', + color = rgb(255,0,0), + hotkey = key('O',CTRL_SHIFT), + icon = '/images/overview.gif', + scale = 0.2, + tooltip = 'Show/Hide overview window'): + super(GlobalMap,self).\ + __init__(map,self.TAG,node=node, + buttonText = buttonText, + color = color, + hotkey = hotkey, + icon = icon, + scale = scale, + tooltip = 'Show/Hide overview window') + +# -------------------------------------------------------------------- +class Zoomer(MapElement): + TAG = Element.MAP+'Zoomer' + def __init__(self,map,node=None, + inButtonText = '', + inIconName = '/images/zoomIn.gif', + inTooltip = 'Zoom in', + outButtonText = '', + outIconName = '/images/zoomOut.gif', + outTooltip = 'Zoom out', + pickButtonText = '', + pickIconName = '/images/zoom.png', + pickTooltip = 'Select Zoom', + zoomInKey = key('=',CTRL_SHIFT), + zoomLevels = [0.2,0.25,0.333,0.4,0.5, + 0.555,0.625,0.75,1.0,1.25,1.6], + zoomOutKey = key('-'), + zoomPickKey = key('='), + zoomStart = 3): + '''Zoom start is counting from the back (with default zoom levels, + and zoom start, the default zoom is 1''' + lvls = ','.join([str(z) for z in zoomLevels]) + super(Zoomer,self).\ + __init__(map,self.TAG,node=node, + inButtonText = inButtonText, + inIconName = inIconName, + inTooltip = inTooltip, + outButtonText = outButtonText, + outIconName = outIconName, + outTooltip = outTooltip, + pickButtonText = pickButtonText, + pickIconName = pickIconName, + pickTooltip = pickTooltip, + zoomInKey = zoomInKey, + zoomLevels = lvls, + zoomOutKey = zoomOutKey, + zoomPickKey = zoomPickKey, + zoomStart = zoomStart) + +# -------------------------------------------------------------------- +class HidePiecesButton(MapElement): + TAG=Element.MAP+'HidePiecesButton' + def __init__(self,map,node=None, + buttonText = '', + hiddenIcon = '/images/globe_selected.gif', + hotkey = key('O'), + showingIcon = '/images/globe_unselected.gif', + tooltip = 'Hide all pieces on this map'): + super(HidePiecesButton,self).\ + __init__(map,self.TAG,node=node, + buttonText = buttonText, + hiddenIcon = hiddenIcon, + hotkey = hotkey, + showingIcon = showingIcon, + tooltip = tooltip) + # -------------------------------------------------------------------- -def get_masskey(map,asdict=True): - return get_asdict(map,MAP+'MassKeyCommand','name', asdict) - +class MassKey(GlobalKey,MapElementService): + TAG = Element.MAP+'MassKeyCommand' + def __init__(self,map,node=None, + name = '', + buttonHotkey = '', + hotkey = '', + buttonText = '', + canDisable = False, + deckCount = '-1', + filter = '', + propertyGate = '', + reportFormat = '', + reportSingle = False, + singleMap = True, + target = GlobalKey.SELECTED, + tooltip = '', + icon = ''): + '''Default targets are selected units''' + super(MassKey,self).\ + __init__(map,self.TAG,node=node, + name = name, + buttonHotkey = buttonHotkey, # This hot key + hotkey = hotkey, # Target hot key + buttonText = buttonText, + canDisable = canDisable, + deckCount = deckCount, + filter = filter, + propertyGate = propertyGate, + reportFormat = reportFormat, + reportSingle = reportSingle, + singleMap = singleMap, + target = target, + tooltip = tooltip, + icon = icon) + + +# -------------------------------------------------------------------- +class Flare(MapElement): + TAG=Element.MAP+'Flare' + def __init__(self,map,node=None, + circleColor = rgb(255,0,0), + circleScale = True, + circleSize = 100, + flareKey = 'keyAlt', + flareName = 'Map Flare', + flarePulses = 6, + flarePulsesPerSec = 3, + reportFormat = ''): + super(Flare,self).__init__(map,self.TAG,node=node, + circleColor = circleColor, + circleScale = circleScale, + circleSize = circleSize, + flareKey = flareKey, + flareName = flareName, + flarePulses = flarePulses, + flarePulsesPerSec = flarePulsesPerSec, + reportFormat = '') + +# -------------------------------------------------------------------- +class AtStart(MapElement): + TAG = Element.MODULE+'map.SetupStack' + def __init__(self,map,node=None, + name = '', + location = '', + useGridLocation = True, + owningBoard = '', + x = 0, + y = 0): + '''Pieces are existing PieceSlot elements''' + super(AtStart,self).\ + __init__(map,self.TAG,node=node, + name = name, + location = location, + owningBoard = owningBoard, + useGridLocation = useGridLocation, + x = x, + y = y) + + def addPieces(self,*pieces): + '''Add a `Pieces` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Pieces + The added element + ''' + # copy pieces here + copies = [] + for p in pieces: + c = self.addPiece(p) + if c is not None: + copies.append(c) + return copies + + def addPiece(self,piece): + '''Add a `Piece` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Piece + The added element + ''' + if not isinstance(piece,PieceSlot): + print(f'Trying to add {type(piece)} to AtStart') + return None + + p = piece.clone(self) + # self._node.appendChild(p._node) + return p + + def getPieces(self,asdict=True): + '''Get all Piece element(s) from this -# -------------------------------------------------------------------- -def get_piece_parts(code): - '''Takes the code part of a pieceslot (the text node) and decodes - it into traits. + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Piece` + elements. If `False`, return a list of all Piece` + children. - One can use this to modify the pieces. One could for example - remove a trait by calling this function, and remove the element - corresponding to the trait from the returned list. Then one must - collect up the definitions and states separately and pass each of - them to `enc_parts', which gives us two strings that can be passed - to `add_piece' (or `add_proto') function to get the new text node - content of the piece slot or prototype definition. For example + Returns + ------- + children : dict or list + Dictionary or list of `Piece` children - piece = get_pieceslots(root,pieces)['foo'] - code = piece.childNodes[0].nodeValue - traits = get_piece_parts(code) - ... # Modify the list - - # Separate out definitions and states - defs = [enc_def(e['def']) for e in traits] - states = [enc_def(e['state']) for e in traits] + ''' + return self.getElementsWithKey(PieceSlot,'entryName',asdict) - # Encode definitions and states as a single string, and then form body - def = enc_parts(*defs) - state = enc_parts(*states) - body = add_piece(def,state) +# +# EOF +# +# ==================================================================== +# From globalproperty.py - # Set new definition - piece.childNodes[0].nodeValue = body +# -------------------------------------------------------------------- +class GlobalProperties(Element): + TAG = Element.MODULE+'properties.GlobalProperties' + def __init__(self,elem,node=None,**named): + super(GlobalProperties,self).__init__(elem,self.TAG,node=node) + + for n, p in named: + self.addProperty(n, **p) + + def getGame(self): + return self.getParent(Game) + def addProperty(self,**kwargs): + '''Add a `Property` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Property + The added element + ''' + return GlobalProperty(self,node=None,**kwargs) + def getProperties(self): + return getElementsByKey(GlobalProperty,'name') - See also dicts_to_piece - - Parameters - ---------- - code : str - Text node content of or tags - - Returns - ------- - traits : list of dict - All the traits in a list. Each element of the list is a - dictionary with the keys 'def' and 'status'. - The key 'def' points to a list of the trait attributes used to - define the trait type. The first element of the 'def' list is - the trait identifier. The rest are the arguments for that - trait type. Note, all fields are encoded as strings. +# -------------------------------------------------------------------- +class GlobalProperty(Element): + TAG = Element.MODULE+'properties.GlobalProperty' + def __init__(self,elem,node=None, + name = '', + initialValue = '', + isNumeric = False, + min = "null", + max = "null", + wrap = False, + description = ""): + super(GlobalProperty,self).__init__(elem,self.TAG, + node = node, + name = name, + initialValue = initialValue, + isNumeric = isNumeric, + min = min, + max = max, + wrap = wrap, + description = description) + + def getGlobalProperties(self): + return self.getParent(GlobalProperties) - The key 'status' encodes the status of the trait. The format - of this depends on the trait.Note, all fields are encoded as - strings. - ''' - cmd,iden,typ,sta = code.split('/') - defs = typ.split(r' ') - stas = sta.split(r' ') - defs = [d.rstrip("\\").split(';') for d in defs] - stas = [s.rstrip("\\").split(';') for s in stas] - ret = [{'def': d, 'state': s} for d,s in zip(defs,stas)] - return ret +# +# EOF +# +# ==================================================================== +# From turn.py + +# -------------------------------------------------------------------- +class TurnLevel(Element): + def __init__(self,elem,tag,node=None,**kwargs): + super(TurnLevel,self).__init__(elem,tag,node=node,**kwargs) + + def addLevel(self,counter=None,phases=None): + '''Add a `Level` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Level + The added element + ''' + if counter is None and phases is None: + return self + + t = TurnCounter if counter is not None else TurnList + o = counter if counter is not None else phases + + subcounter = o.pop('counter',None) + subphases = o.pop('phases',None) + + s = t(self,node=None,**o) + + return s.addLevel(subcounter, subphases) + + def getUp(self): + return self.getParent(TurnLevel) + def addCounter(self,**kwargs): + '''Add a `Counter` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Counter + The added element + ''' + return self.add(self,TurnCounter,**kwargs) + def addList(self,**kwargs): + '''Add a `List` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : List + The added element + ''' + return self.add(self,TurnList,**kwargs) + def getCounter(self): + return self.getAllElements(TurnCounter) + def getList(self): + return self.getAllElements(TurnList) + +# -------------------------------------------------------------------- +class TurnTrack(TurnLevel): + TAG = Element.MODULE+'turn.TurnTracker' + def __init__(self,elem,node=None, + name = '', + buttonText = 'Turn', + hotkey = '', + icon = '', + length = -1, + lengthStyle = 'Maximum', + nexthotkey = key('T',ALT), + plusButtonSize = 22, + prevhotkey = key('T',ALT_SHIFT), + reportFormat = 'Turn updated from $oldTurn$ to $newTurn$', + turnButtonHeight = 22, + fwdOnly = True, + turnFormat = None, + counter = None, + phases = None): + levels = (counter if counter is not None else + phases if phases is not None else None) + if levels is not None: + lvl = 1 + lvls = [f'$level{lvl}$'] + sub = levels + while True: + sub = sub.get('counter',sub.get('phases',None)) + if sub is None: + break + lvl += 1 + lvls.append(f'$level{lvl}$') + + turnFormat = ' '.join(lvls) + + if turnFormat is None: + turnFormat = '$level1$ $level2$ $level3$ $level4$' + + super(TurnTrack,self).__init__(elem, self.TAG, + node = node, + name = name, + buttonText = buttonText, + hotkey = hotkey, + icon = icon, + length = length, + lengthStyle = lengthStyle, + nexthotkey = nexthotkey, + plusButtonSize = plusButtonSize, + prevhotkey = prevhotkey, + reportFormat = reportFormat, + turnButtonHeight = turnButtonHeight, + turnFormat = turnFormat) + + self.addLevel(counter=counter, phases=phases) + + def getGame(self): + return self.getParent(Game) + def getLists(self,asdict=True): + '''Get all List element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `List` + elements. If `False`, return a list of all List` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `List` children + + ''' + return self.getElementsByKey(TurnList,'property',asdict=asdict) + def getCounters(self,asdict=True): + '''Get all Counter element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Counter` + elements. If `False`, return a list of all Counter` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `Counter` children + + ''' + return self.getElementsByKey(TurnCounter,'property',asdict=asdict) + def addHotkey(self,**kwargs): + '''Add a `Hotkey` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Hotkey + The added element + ''' + return self.add(TurnGlobalHotkey,**kwargs) + def getHotkeys(self,asdict): + return self.getElementsByKey(TurnGlobalHotkey,'name',asdict=asdict) + def encode(self): + ret = f'TURN{self["name"]}\t' + + return [] + +# -------------------------------------------------------------------- +class TurnCounter(TurnLevel): + TAG = Element.MODULE+"turn.CounterTurnLevel" + def __init__(self,elem,node=None, + property = '', + start = 1, + incr = 1, + loop = False, + loopLimit = -1, + turnFormat = "$value$"): + super(TurnCounter,self).__init__(elem,self.TAG,node=node, + property = property, + start = start, + incr = incr, + loop = loop, + loopLimit = loopLimit, + turnFormat = turnFormat) + +# -------------------------------------------------------------------- +class TurnList(TurnLevel): + TAG = Element.MODULE+"turn.ListTurnLevel" + def __init__(self,elem,node=None, + property = '', + names = [], + configFirst = False, + configList = False, + turnFormat = '$value$'): + super(TurnList,self).\ + __init__(elem,self.TAG,node=node, + property = property, + list = ','.join([str(p) for p in names]), + configFirst = configFirst, + configList = configList, + turnFormat = turnFormat) + # -------------------------------------------------------------------- -def dicts_to_piece(traits): - # Separate out definitions and states - defs = [enc_def(e['def']) for e in traits] - states = [enc_def(e['state']) for e in traits] +class TurnGlobalHotkey(Element): + TAG = Element.MODULE+'turn.TurnGlobalHotkey' + def __init__(self,elem, + node = None, + hotkey = '', + match = '{true}', + reportFormat = '', + name = ''): + '''Global key activated by turn change + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + hotkey : str + What to send (global command) + match : str + When to send + reportFormat : str + What to what + name : str + A free form name + ''' + super(TurnGlobalHotkey,self).__init__(elem,self.TAG, + node = node, + hotkey = hotkey, + match = match, + reportFormat = reportFormat, + name = name) + + def getTurnTrack(self): + '''Get the turn track''' + return self.getParent(TurnTrack) - # Encode definitions and states as a single string, and then form body - df = enc_parts(*defs) - state = enc_parts(*states) - body = add_piece(df,state) +# +# EOF +# +# ==================================================================== +# From documentation.py - return body - - # ==================================================================== -# Key encoding -SHIFT = 65 -CTRL = 130 -ALT = 520 -CTRL_SHIFT = CTRL+SHIFT -ALT_SHIFT = ALT+SHIFT -NONE = '\ue004' -NONE_MOD = 0 -def key(let,mod=CTRL): - - '''Encode a key sequence +def createKeyHelp(*args,**kwargs): + '''Creates a help file with key-bindings - Parameters - ---------- - let : str - Key code (Letter) - mod : int - Modifier mask + See Documentation.createKeyHelp ''' - return f'{ord(let)},{mod}' -# Colour encoding -def rgb(r,g,b): - return ','.join([str(r),str(g),str(b)]) + return Documentation.createKeyHelp(*args,**kwargs) + +# -------------------------------------------------------------------- +class Documentation(GameElement): + TAG=Element.MODULE+'Documentation' + def __init__(self,doc,node=None,**kwargs): + '''Documentation (or help menu) + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + kwargs : dict + Attributes + ''' + super(Documentation,self).__init__(doc,self.TAG,node=node,**kwargs) + + def addAboutScreen(self,**kwargs): + '''Add a `AboutScreen` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : AboutScreen + The added element + ''' + return self.add(AboutScreen,**kwargs) + def addHelpFile(self,**kwargs): + '''Add a `HelpFile` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : HelpFile + The added element + ''' + return self.add(HelpFile,**kwargs) + def addBrowserHelpFile(self,**kwargs): + '''Add a `BrowserHelpFile` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : BrowserHelpFile + The added element + ''' + return self.add(HelpBrowserFile,**kwargs) + def addBrowserPDFFile(self,**kwargs): + '''Add a `BrowserPDFFile` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : BrowserPDFFile + The added element + ''' + return self.add(BrowserPDFFile,**kwargs) + def addTutorial(self,**kwargs): + '''Add a `Tutorial` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Tutorial + The added element + ''' + return self.add(Tutorial,**kwargs) + def getAboutScreens(self): + return self.getElementsByKey(AboutScreen,'title') + def getHelpFiles(self): + return self.getElementsByKey(HelpFile,'title') + def getBrowserHelpFiles(self): + return self.getElementsByKey(BrowserHelpFile,'title') + def getBrowserPDFFiles(self): + return self.getElementsByKey(BrowserPDFFile,'title') + def getTutorials(self): + return self.getElementsByKey(Tutorial,'name') + + @classmethod + def createKeyHelp(cls,keys,title='',version=''): + '''Creates a help file with key-bindings + + Parameters + ---------- + keys : list of list of str + List of key-binding documentation + title : str + Title of help file + version : str + Version number + + Returns + ------- + txt : str + File content + ''' + txt = f''' + + +

{title} (Version {version}) Key bindings

+ + ''' + + for key, where, description in keys: + txt += (f'' + f'' + f'') + + txt += ''' +
KeyWhereEffect
{key}{where}{description}
+ + ''' + + return txt + + +# -------------------------------------------------------------------- +class AboutScreen(Element): + TAG = Element.MODULE+'documentation.AboutScreen' + def __init__(self,doc,node=None,title='',fileName=""): + '''Create an about screen element that shows image + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + title : str + Entry title + fileName : str + Internal file name + ''' + super(AboutScreen,self).__init__(doc, + self.TAG, + node = node, + fileName = fileName, + title = title) + def getDocumentation(self): + '''Get Parent element''' + return self.getParent(Documentation) + +# -------------------------------------------------------------------- +class BrowserPDFFile(Element): + TAG = Element.MODULE+'documentation.BrowserPDFFile' + def __init__(self,doc,node=None,title='',pdfFile=''): + '''Create help menu item that opens an embedded PDF + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + title : str + Entry title + pdfFile : str + Internal file name + ''' + super(BrowserPDFFile,self).__init__(doc,self.TAG, + node = node, + pdfFile = pdfFile, + title = title) + def getDocumentation(self): + '''Get Parent element''' + return self.getParent(Documentation) + +# -------------------------------------------------------------------- +class HelpFile(Element): + TAG = Element.MODULE+'documentation.HelpFile' + ARCHIVE = 'archive' + def __init__(self,doc,node=None, + title='', + fileName='', + fileType=ARCHIVE): + '''Create a help menu entry that opens an embeddded file + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + title : str + Entry title + fileName : str + Internal file name + fileType : str + How to find the file + ''' + super(HelpFile,self).__init__(doc,self.TAG,node=node, + fileName = fileName, + fileType = fileType, + title = title) + + def getDocumentation(self): + '''Get Parent element''' + return self.getParent(Documentation) + +# -------------------------------------------------------------------- +class BrowserHelpFile(Element): + TAG = Element.MODULE+'documentation.BrowserHelpFile' + def __init__(self,doc,node=None, + title='', + startingPage='index.html'): + '''Create a help menu entry that opens an embeddded HTML + page (with possible sub-pages) file + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + title : str + Entry title + startingPage : str + which file to start at + ''' + super(BrowserHelpFile,self).__init__(doc,self.TAG,node=node, + startingPage=startingPage, + title=title) + + def getDocumentation(self): + '''Get Parent element''' + return self.getParent(Documentation) + +# -------------------------------------------------------------------- +class Tutorial(Element): + TAG = Element.MODULE+'documentation.Tutorial' + def __init__(self,doc,node=None, + name = 'Tutorial', + logfile = 'tutorial.vlog', + promptMessage = 'Load the tutorial?', + welcomeMessage = 'Press "Step forward" (PnDn) to step through the tutorial', + launchOnStartup = True): + '''Add a help menu item that loads the tutorial + + Also adds the start-up option to run the tutorial + + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + name : str + Name of entry + logfile : str + Internal file name + promptMessage : str + What to show + launchOnStartup : bool + By default, launch tutorial first time running module + ''' + super(Tutorial,self).__init__(doc,self.TAG,node=node, + name = name, + logfile = logfile, + promptMessage = promptMessage, + welcomeMessage = welcomeMessage, + launchOnStartup = launchOnStartup) + + def getDocumentation(self): + '''Get Parent element''' + return self.getParent(Documentation) -def rgba(r,g,b,a): - return ','.join([str(r),str(g),str(b),str(a)]) +# +# EOF +# +# ==================================================================== +# From player.py # -------------------------------------------------------------------- -def set_node_attr(node,**attr): - for k,v in attr.items(): - node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v)) - -# -------------------------------------------------------------------- -def add_node(root,parent,tag,**attr): - e = root.createElement(tag) - if parent: parent.appendChild(e) - set_node_attr(e,**attr) - - return e - - -# -------------------------------------------------------------------- -def add_text(root,parent,text): - t = root.createTextNode(text) - parent.appendChild(t) - return t - -# -------------------------------------------------------------------- -def add_game(root,doc, - name, - version, - ModuleOther1 = "", - ModuleOther2 = "", - VassalVersion = "3.6.7", - description = "", - nextPieceSlotId = 0): - return add_node(root,doc,BUILD+'GameModule', - name = name, - version = version, - ModuleOther1 = ModuleOther1, - ModuleOther2 = ModuleOther2, - VassalVersion = VassalVersion, - description = description, - nextPieceSlotId = nextPieceSlotId) - -# -------------------------------------------------------------------- -def add_basiccommandencoder(root,doc): - return add_node(root,doc,MODULE+'BasicCommandEncoder') - -# -------------------------------------------------------------------- -def add_globalproperties(root,elem,*props,**named): - gp = add_node(root,elem,MODULE+'properties.GlobalProperties') - # Add elements where each is a dict with _at least_ - # "{'name':NAME, 'initialValue':VALUE}" - for p in props: - add_globalproperty(root,gp,**p) - # Add elements where each is a named dict with _at least_ - # "{'initialValue':VALUE}" - for n, p in named: - add_globalproperty(root,gp,name=n,**p) - - return gp - - -# -------------------------------------------------------------------- -def add_globalproperty(root,elem,name,initialValue, - isNumeric = False, - min = "null", - max = "null", - wrap = False, - description = ""): - return add_node(root,elem,MODULE+'properties.GlobalProperty', - name = name, - initialValue = initialValue, - isNumeric = isNumeric, - min = min, - max = max, - wrap = wrap, - description = description) - -# -------------------------------------------------------------------- -def add_turntrack(root, elem, name, - buttonText = 'Turn', - hotkey = '', - icon = '', - length = -1, - lengthStyle = 'Maximum', - nexthotkey = key('T',ALT), - plusButtonSize = 22, - prevhotkey = key('T',ALT_SHIFT), - reportFormat = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$', - turnButtonHeight = 22, - turnFormat = None, - counter = None, - phases = None): - - levels = (counter if counter is not None else - phases if phases is not None else None) - if levels is not None: - lvl = 1 - lvls = [f'$level{lvl}$'] - sub = levels - while True: - sub = sub.get('counter',sub.get('phases',None)) - if sub is None: - break - lvl += 1 - lvls.append(f'$level{lvl}$') - - turnFormat = ' '.join(lvls) - - if turnFormat is None: - turnFormat = '$level1$ $level2$ $level3$ $level4$' - - t = add_node(root, elem, MODULE+'turn.TurnTracker', - name = name, - buttonText = buttonText, - hotkey = hotkey, - icon = icon, - length = length, - lengthStyle = lengthStyle, - nexthotkey = nexthotkey, - plusButtonSize = plusButtonSize, - prevhotkey = prevhotkey, - reportFormat = reportFormat, - turnButtonHeight = turnButtonHeight, - turnFormat = turnFormat) - - add_turnlevel(root, t, counter=counter, phases=phases) - - return t - -# -------------------------------------------------------------------- -def add_turnlevel(root,parent,counter=None,phases=None): - f,s = ((add_turncounter,counter) if counter is not None else - (add_turnlist,phases) if phases is not None else (None,None)) - if f is None: return parent - - subcounter = s.pop('counter',None) - subphases = s.pop('phases',None) - - sub = f(root,parent,**s) - - return add_turnlevel(root,sub,counter=subcounter,phases=subphases) - -# -------------------------------------------------------------------- -def add_turncounter(root,elem,property, - incr = 1, - loop = False, - loopLimit = -1, - turnFormat = "$value$"): - c = add_node(root,elem,MODULE+"turn.CounterTurnLevel", - property = property, - incr = incr, - loop = loop, - loopLimit = loopLimit, - turnFormat = turnFormat) - return c - +class PlayerRoster(GameElement): + TAG = Element.MODULE+'PlayerRoster' + def __init__(self,doc,node=None,buttonKeyStroke='', + buttonText='Retire', + buttonToolTip='Switch sides, become observer, or release faction'): + '''Add a player roster to the module + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + buttonText : str + Text on button + buttonTooltip : str + Tool tip to show when hovering over button + ''' + super(PlayerRoster,self).__init__(doc,self.TAG,node=node, + buttonKeyStroke = buttonKeyStroke, + buttonText = buttonText, + buttonToolTip = buttonToolTip) + def addSide(self,name): + '''Add a `Side` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Side + The added element + ''' + return self.add(PlayerSide,name=name) + def getSides(self): + '''Get all sides''' + return self.getAllElements(PlayerSide,False) + def encode(self): + '''Encode for save''' + return ['PLAYER\ta\ta\t'] + +# -------------------------------------------------------------------- +class PlayerSide(Element): + TAG = 'entry' + def __init__(self,doc,node=None,name=''): + '''Adds a side to the player roster + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + name : str + Name of side + ''' + super(PlayerSide,self).__init__(doc,self.TAG,node=node) + if node is None: + self.addText(name) + + def getPlayerRoster(self): + '''Get Parent element''' + return self.getParent(PlayerRoster) + + +# +# EOF +# +# ==================================================================== +# From chessclock.py + +# ==================================================================== +class ChessClock(Element): + TAG=Element.MODULE+'chessclockcontrol.ChessClock' + def __init__(self, + doc, + node = None, + icon = '', + description = '', + side = '', + tooltip = 'Individual clock control', + buttonText = '', + startHotkey = '', + stopHotkey = '', + tickingBackgroundColor = rgb(255,255,0), + tickingFontColor = rgb(0,0,0), + tockingFontColor = rgb(51,51,51)): + '''Individual clock for a side + + When the clock is running, the background colour may be + changed, and the colour of the numbers alternate between + `tickingFontColor` and `tockingFontColor`. + + Parameters + ---------- + doc : Element + Parent element + node : xml.dom.Element + Read from this node + icon : str + File name of button icon + description : str + Note on this clock + side : str + Name of side this clock belongs to + tooltop : str + Hover help text + buttonText : str + Text on button + startHotkey : str (key code) + Key or command to start timer + stopHotkey : str (key code) + Key or command to stop timer + tickingBackgroundColor : str (color) + Background color of time display when clock is running + tickingFontColor : str (color) + First color of numbers in display when clock is running. + tockingFontColor : str (color) + Second color of numbers in display when clock is running. + ''' + super(ChessClock,self).__init__(#ChessClock + doc, + self.TAG, + node = node, + icon = icon, + description = description, + side = side, + tooltip = tooltip, + buttonText = buttonText, + startHotkey = startHotkey, + stopHotkey = stopHotkey, + tickingBackgroundColor = tickingBackgroundColor, + tickingFontColor = tickingFontColor, + tockingFontColor = tockingFontColor) + + def getControl(self): + '''Get Parent element''' + return self.getParent(ChessClockControl) + +# ==================================================================== +class ChessClockControl(GameElement): + TAG=Element.MODULE+'ChessClockControl' + ALWAYS = 'Always' + AUTO = 'Auto' + NEVER = 'Never' + def __init__(self, + doc, + node = None, + name = 'Chess clock', + description = '', + buttonIcon = 'chess_clock.png', + buttonText = '', + buttonTooltip = 'Show/stop/hide chess clocks', + showHotkey = key('U',ALT), + pauseHotkey = key('U',CTRL_SHIFT), + nextHotkey = key('U'), + startOpponentKey = '', + showTenths = AUTO, + showSeconds = AUTO, + showHours = AUTO, + showDays = AUTO, + allowReset = False, + addClocks = True): + '''A set of chess clocs + + Parameters + ---------- + doc : Element + Parent + node : xml.dom.Element + Node to read state from + name : str + Name of clock control + description : str + Note on the chess clocks control + buttonIcon : str + Icon file name for button (chess_clock.png) + buttonText : str + Text on button + buttonTooltip : str + Hower help + showHotkey : str (key code) + Show or hide interface hot key + nextHotkey : str (key code) + Start the next clock hot key + pauseHotkey : str (key code) + Pause all clocks hot key + startOpponentKey : str (key code) + Start opponens clock + showTenths : one of AUTO, ALWAYS, NEVER + Whether to show tenths of seconds + showSeconds : one of AUTO, ALWAYS, NEVER + Whether to show seconds in clock + showHours : one of AUTO, ALWAYS, NEVER + Whether to show hours in clock + showDays : one of AUTO, ALWAYS, NEVER + Whether to show days in clock + allowReset : boolean + If true, allow manual reset of all clocks + ''' + super(ChessClockControl,self).__init__(# ChessclockControl + doc, + self.TAG, + node = node, + name = name, + description = description, + buttonIcon = buttonIcon, + buttonText = buttonText, + buttonTooltip = buttonTooltip, + showHotkey = showHotkey, + pauseHotkey = pauseHotkey, + nextHotkey = nextHotkey, + startOpponentKey = startOpponentKey, + showTenths = showTenths, + showSeconds = showSeconds, + showHours = showHours, + showDays = showDays, + allowReset = allowReset) + print(node,addClocks) + if node is not None or not addClocks: + return + + print('--- Will add clocks') + game = self.getGame() + roster = game.getPlayerRoster()[0] + sides = roster.getSides() + for side in sides: + name = side.getText() + self.addClock(side = name, + tooltip = f'Clock for {name}', + buttonText = name, + startHotkey = key('U'), + stopHotkey = key('U')) + + def addClock(self,**kwargs): + '''Add a clock element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : AboutScreen + The added element + ''' + return self.add(ChessClock,**kwargs) + def getClocks(self,asdict=True): + '''Return dictionary of clocs''' + return self.getElementsByKey(ChessClock,'side',asdict) + + +# +# EOF +# +# ==================================================================== +# From widget.py + +# -------------------------------------------------------------------- +class WidgetElement: + def __init__(self): + pass + + def addTabs(self,**kwargs): + '''Add a `Tabs` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Tabs + The added element + ''' + return self.add(TabWidget,**kwargs) + def addCombo(self,**kwargs): + '''Add a drop-down menu to this + + Parameters + ---------- + Dictionary of attribute key-value pairs + Returns + ------- + element : Combo + The added element + ''' + return self.add(ComboWidget,**kwargs) + def addPanel(self,**kwargs): + '''Add a `Panel` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Panel + The added element + ''' + return self.add(PanelWidget,**kwargs) + def addList(self,**kwargs): + '''Add a `List` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : List + The added element + ''' + return self.add(ListWidget,**kwargs) + def addMapWidget(self,**kwargs): + '''Add a `MapWidget` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : MapWidget + The added element + ''' + return self.add(MapWidget,**kwargs) + def addChart(self,**kwargs): + '''Add a `Chart` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Chart + The added element + ''' + return self.add(Chart,**kwargs) + def addPieceSlot(self,**kwargs): + '''Add a `PieceSlot` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PieceSlot + The added element + ''' + return self.add(PieceSlot,**kwargs) + def addPiece(self,piece): + '''Add a `Piece` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Piece + The added element + ''' + if not isinstance(piece,PieceSlot): + print(f'Trying to add {type(piece)} to ListWidget') + return None + + p = piece.clone(self) + return p + def getTabs(self,asdict=True): + '''Get all Tab element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Tab` elements. If `False`, return a list of all Tab` children. + Returns + ------- + children : dict or list + Dictionary or list of `Tab` children + ''' + return self.getElementsByKey(TabWidget,'entryName',asdict) + def getCombos(self,asdict=True): + '''Get all Combo element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Tab` elements. If `False`, return a list of all Tab` children. + Returns + ------- + children : dict or list + Dictionary or list of `Tab` children + ''' + return self.getElementsByKey(ComboWidget,'entryName',asdict) + def getLists(self,asdict=True): + '''Get all List element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `List` elements. If `False`, return a list of all List` children. + Returns + ------- + children : dict or list + Dictionary or list of `List` children + ''' + return self.getElementsByKey(ListWidget,'entryName',asdict) + def getPanels(self,asdict=True): + '''Get all Panel element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Panel` elements. If `False`, return a list of all Panel` children. + Returns + ------- + children : dict or list + Dictionary or list of `Panel` children + ''' + return self.getElementsByKey(PanelWidget,'entryName',asdict) + def getMapWidgets(self,asdict=True): + '''Get all MapWidget element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `MapWidget` elements. If `False`, return a list of all MapWidget` children. + Returns + ------- + children : dict or list + Dictionary or list of `MapWidget` children + ''' + return self.getElementsByKey(MapWidget,'entryName',asdict) + def getCharts(self,asdict=True): + '''Get all Chart element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Chart` elements. If `False`, return a list of all Chart` children. + Returns + ------- + children : dict or list + Dictionary or list of `Chart` children + ''' + return self.getElementsByKey(Chart,'chartName',asdict) + def getPieceSlots(self,asdict=True): + '''Get all PieceSlot element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `PieceSlot` elements. If `False`, return a list of all PieceSlot` children. + Returns + ------- + children : dict or list + Dictionary or list of `PieceSlot` children + ''' + return self.getElementsByKey(PieceSlot,'entryName',asdict) + +# -------------------------------------------------------------------- +class PieceWindow(GameElement,WidgetElement): + TAG=Element.MODULE+'PieceWindow' + def __init__(self,elem,node=None, + name = '', + defaultWidth = 0, + hidden = False, + hotkey = key('C',ALT), + scale = 1., + text = '', + tooltip = 'Show/hide piece window', + icon = '/images/counter.gif'): + super(PieceWindow,self).__init__(elem,self.TAG,node=node, + name = name, + defaultWidth = defaultWidth, + hidden = hidden, + hotkey = hotkey, + scale = scale, + text = text, + tooltip = tooltip, + icon = icon) + +# -------------------------------------------------------------------- +class ComboWidget(Element,WidgetElement): + TAG=Element.WIDGET+'BoxWidget' + def __init__(self,elem,node=None,entryName='',width=0,height=0): + super(ComboWidget,self).__init__(elem, + self.TAG, + node = node, + entryName = entryName, + width = width, + height = height) + # -------------------------------------------------------------------- -def add_turnlist(root,elem,property,names, - configFirst = False, - configList = False, - turnFormat = '$value$'): - c = add_node(root,elem,MODULE+"turn.ListTurnLevel", - property = property, - list = ','.join([str(p) for p in names]), - configFirst = configFirst, - configList = configList, - turnFormat = turnFormat) - return c - +class TabWidget(Element,WidgetElement): + TAG=Element.WIDGET+'TabWidget' + def __init__(self,elem,node=None,entryName=''): + super(TabWidget,self).__init__(elem, + self.TAG, + node = node, + entryName = entryName) + +# -------------------------------------------------------------------- +class ListWidget(Element,WidgetElement): + TAG=Element.WIDGET+'ListWidget' + def __init__(self,elem,node = None, + entryName = '', + height = 0, + width = 300, + scale = 1., + divider = 150): + super(ListWidget,self).__init__(elem,self.TAG,node=node, + entryName = entryName, + height = height, + width = width, + scale = scale, + divider = divider) + +# -------------------------------------------------------------------- +class PanelWidget(Element,WidgetElement): + TAG=Element.WIDGET+'PanelWidget' + def __init__(self,elem,node=None, + entryName = '', + fixed = False, + nColumns = 1, + vert = False): + super(PanelWidget,self).__init__(elem,self.TAG,node=node, + entryName = entryName, + fixed = fixed, + nColumns = nColumns, + vert = vert) + +# -------------------------------------------------------------------- +class MapWidget(Element): + TAG=Element.WIDGET+'MapWidget' + def __init__(self,elem,node=None,entryName=''): + super(MapWidget,self).__init__(elem,self.TAG, + node = node, + entryName = entryName) + + def addWidgetMap(self,**kwargs): + '''Add a `WidgetMap` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : WidgetMap + The added element + ''' + return self.add(WidgetMap,**kwargs) + def getWidgetMaps(self,asdict=True): + '''Get all WidgetMap element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `WidgetMap` elements. If `False`, return a list of all WidgetMap` children. + Returns + ------- + children : dict or list + Dictionary or list of `WidgetMap` children + ''' + return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict) + +# +# EOF +# +# ==================================================================== +# From grid.py + +# -------------------------------------------------------------------- +HEX_WIDTH = 88.50779626676963 +HEX_HEIGHT = 102.2 +RECT_WIDTH = 80 +RECT_HEIGHT = 80 # -------------------------------------------------------------------- -def add_turnhotkey(root,elem, - hotkey, - match = '{phase=="first"}', - reportFormat = '{PlayerName}', - name = ''): - return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey', - hotkey = hotkey, - match = match, - reportFormat = reportFormat, - name = name) +class BaseGrid(Element): + def __init__(self,zone,tag,node=None, + color = rgb(0,0,0), + cornersLegal = False, + dotsVisible = False, + dx = HEX_WIDTH, # Meaning seems reversed! + dy = HEX_HEIGHT, + edgesLegal = False, + sideways = False, + snapTo = True, + visible = True, + x0 = 0, + y0 = 32): + super(BaseGrid,self).__init__(zone,tag,node=node, + color = color, + cornersLegal = cornersLegal, + dotsVisible = dotsVisible, + dx = dx, + dy = dy, + edgesLegal = edgesLegal, + sideways = sideways, + snapTo = snapTo, + visible = visible, + x0 = x0, + y0 = y0) + def getZone(self): + z = self.getParent(Zone) + return z + def getZonedGrid(self): + z = self.getZone() + if z is not None: + return z.getZonedGrid() + return None + def getBoard(self): + z = self.getZonedGrid() + if z is not None: + return z.getBoard() + return self.getParent(Board) + def getPicker(self): + z = self.getBoard() + if z is not None: + return z.getPicker() + return None + def getMap(self): + b = self.getPicker() + if b is not None: + return b.getMap() + return None + def getNumbering(self): + pass + def getLocation(self,loc): + numbering = self.getNumbering() + if numbering is None or len(numbering) < 1: + return None + + return numbering[0].getLocation(loc) + +# -------------------------------------------------------------------- +class BaseNumbering(Element): + def __init__(self,grid,tag,node=None, + color = rgb(255,0,0), + first = 'H', + fontSize = 24, + hDescend = False, + hDrawOff = 0, + hLeading = 1, + hOff = 0, + hType = 'A', + locationFormat = '$gridLocation$', + rotateText = 0, + sep = '', + stagger = True, + vDescend = False, + vDrawOff = 32, + vLeading = 0, + vOff = 0, + vType = 'N', + visible = True): + super(BaseNumbering,self).__init__(grid,tag,node=node, + color = color, + first = first, + fontSize = fontSize, + hDescend = hDescend, + hDrawOff = hDrawOff, + hLeading = hLeading, + hOff = hOff, + hType = hType, + locationFormat = locationFormat, + rotateText = rotateText, + sep = sep, + stagger = stagger, + vDescend = vDescend, + vDrawOff = vDrawOff, + vLeading = vLeading, + vOff = vOff, + vType = vType, + visible = visible) + def getGrid(self): return getParent(BaseGrid) + + def _getMatcher(self,tpe,leading): + if tpe == 'A': + return \ + '-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|' + \ + 'J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|' + \ + 'U+|V+|W+|X+|Y+|Z+)' + + return f'-?[0-9]{{{int(leading)+1},}}' + + def _getIndex(self,name,tpe): + if tpe == 'A': + negative = name.startswith('-') + if negative: + name = name[1:] + + value = 0 + for num,let in enumerate(name): + if not let.isupper(): + continue + if num < len(name) - 1: + value += 26 + else: + value += ord(let)-ord('A') + + if negative: + value *= -1 + + return value + + return int(name) + + def _getCenter(self,col,row): + '''Convert col and row index to picture coordinates''' + print('Dummy GetCenter') + pass + + def getLocation(self,loc): + '''Get picture coordinates from grid location''' + from re import match + + first = self['first'] + vType = self['vType'] + hType = self['hType'] + vOff = int(self['vOff']) + hOff = int(self['hOff']) + colPat = self._getMatcher(hType,self['hLeading']) + rowPat = self._getMatcher(vType,self['vLeading']) + patts = ((colPat,rowPat) if first == 'H' else (rowPat,colPat)) + colGrp = 1 if first == 'H' else 2 + rowGrp = 2 if first == 'H' else 1 + patt = ''.join([f'({p})' for p in patts]) + matched = match(patt,loc) + if not matched: + return None + + rowStr = matched[rowGrp] + colStr = matched[colGrp] + rowNum = self._getIndex(rowStr,vType) + colNum = self._getIndex(colStr,hType) + + return self._getCenter(colNum-hOff, rowNum-vOff); + + +# -------------------------------------------------------------------- +class HexGrid(BaseGrid): + TAG = Element.BOARD+'HexGrid' + def __init__(self,zone,node=None,**kwargs): + super(HexGrid,self).__init__(zone,self.TAG,node=node,**kwargs) + + def addNumbering(self,**kwargs): + '''Add a `Numbering` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Numbering + The added element + ''' + return self.add(HexNumbering,**kwargs) + def getNumbering(self): + return self.getAllElements(HexNumbering) + def getDeltaX(self): + return float(self['dx']) + def getDeltaY(self): + return float(self['dy']) + def getXOffset(self): + return int(self['x0']) + def getYOffset(self): + return int(self['y0']) + def getMaxRows(self): + from math import floor + height = self.getZone().getHeight() + return floor(height / self.getDeltaX() + .5) + def getMaxCols(self): + from math import floor + width = self.getZone().getWidth() + return floor(width / self.getDeltaY() + .5) + +# -------------------------------------------------------------------- +class SquareGrid(BaseGrid): + TAG = Element.BOARD+'SquareGrid' + def __init__(self,zone,node=None, + dx = RECT_WIDTH, + dy = RECT_HEIGHT, + edgesLegal = False, + x0 = 0, + y0 = int(0.4*RECT_HEIGHT), + **kwargs): + super(SquareGrid,self).__init__(zone,self.TAG,node=node, + dx = dx, + dy = dy, + edgesLegal = edgesLegal, + x0 = 0, + y0 = int(0.4*RECT_HEIGHT), + **kwargs) + def addNumbering(self,**kwargs): + '''Add a `Numbering` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Numbering + The added element + ''' + return self.add(SquareNumbering,**kwargs) + def getNumbering(self): + return self.getAllElements(SquareNumbering) + def getDeltaX(self): + return float(self['dx']) + def getDeltaY(self): + return float(self['dy']) + def getXOffset(self): + return int(self['x0']) + def getYOffset(self): + return int(self['y0']) + def getMaxRows(self): + from math import floor + height = self.getZone().getHeight() + return floor(height / self.getDeltaY() + .5) + def getMaxCols(self): + from math import floor + width = self.getZone().getWidth() + return floor(width / self.getDeltaX() + .5) + +# -------------------------------------------------------------------- +class HexNumbering(BaseNumbering): + TAG = Element.BOARD+'mapgrid.HexGridNumbering' + def __init__(self,grid,node=None,**kwargs): + super(HexNumbering,self).__init__(grid,self.TAG,node=node,**kwargs) + def getGrid(self): + g = self.getParent(HexGrid) + return g + + def _getCenter(self,col,row): + '''Convert col and row index to picture coordinates''' + from math import floor + + stagger = self['stagger'] == 'true' + sideways = self.getGrid()['sideways'] == 'true' + hDesc = self['hDescend'] == 'true' + vDesc = self['vDescend'] == 'true' + xOff = self.getGrid().getXOffset() + yOff = self.getGrid().getYOffset() + hexW = self.getGrid().getDeltaX() + hexH = self.getGrid().getDeltaY() + zxOff = self.getGrid().getZone().getXOffset() + zyOff = self.getGrid().getZone().getYOffset() + maxRows = self.getGrid().getMaxRows() + maxCols = self.getGrid().getMaxCols() + # print(f' Col: {col}') + # print(f' Row: {row}') + # print(f' Stagger: {stagger}') + # print(f' Sideways: {sideways}') + # print(f' hDesc: {hDesc}') + # print(f' vDesc: {vDesc}') + # print(f' maxRows: {maxRows}') + # print(f' maxCols: {maxCols}') + + if sideways: + maxRows, maxCols = maxCols, maxRows + + if stagger: + if sideways: + if col % 2 != 0: + row += 1 if hDesc else -1 + else: + if col % 2 != 0: + row += 1 if vDesc else -1 + + if hDesc: + col = maxCols - col + if vDesc: + row = maxRows - row + + x = col * hexW + xOff + y = row * hexH + yOff + (hexH/2 if col % 2 != 0 else 0) + + x = int(floor(x + .5)) + y = int(floor(y + .5)) + if sideways: + # print(f'Swap coordinates because {sideways}') + x, y = y, x + + return x,y + +# -------------------------------------------------------------------- +class SquareNumbering(BaseNumbering): + TAG = Element.BOARD+'mapgrid.SquareGridNumbering' + def __init__(self,grid,node=None,hType='N',**kwargs): + super(SquareNumbering,self).__init__(grid,self.TAG,node=node, + hType=hType,**kwargs) + def getGrid(self): + return self.getParent(SquareGrid) + + def getCenter(self,col,row): + hDesc = self['hDescend'] == 'true' + vDesc = self['vDescend'] == 'true' + xOff = self.getGrid().getXOffset() + yOff = self.getGrid().getYOffset() + squareW = self.getGrid().getDeltaX() + squareH = self.getGrid().getDeltaY() + maxRows = self.getGrid().getMaxRows() + maxCols = self.getGrid().getMaxCols() + + if vDesc: row = maxRows - row + if hDesc: col = maxCols - col + + x = col * squareW + xOff + y = row * squareH + yOff + + return x,y + + +# -------------------------------------------------------------------- +class RegionGrid(Element): + TAG = Element.BOARD+'RegionGrid' + def __init__(self,zone,node=None,snapto=True,fontsize=9,visible=True): + super(RegionGrid,self).__init__(zone,self.TAG,node=node, + snapto = snapto, + fontsize = fontsize, + visible = visible) + + def getZone(self): + return self.getParent(Zone) + def getZoneGrid(self): + z = self.getZone() + if z is not None: + return z.getBoard() + return None + def getBoard(self): + z = self.getZonedGrid() + if z is not None: + return z.getBoard() + return self.getParent(Board) + def getMap(self): + b = self.getBoard() + if b is not None: + return b.getMap() + return None + def getRegions(self): + return self.getElementsByKey(Region,'name') + def checkName(self,name): + '''Get unique name''' + poss = len([e for e in self.getRegions() + if e == name or e.startswith(name+'_')]) + if poss == 0: + return name + + return name + f'_{poss}' + def addRegion(self,**kwargs): + '''Add a `Region` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Region + The added element + ''' + return self.add(Region,**kwargs) + def getLocation(self,loc): + for r in self.getRegions().values(): + if loc == r['name']: + return int(r['originx']),int(r['originy']) - - + return None + # -------------------------------------------------------------------- -def add_translatable(root,elem): - return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages') +class Region(Element): + TAG = Element.BOARD+'Region' + def __init__(self,grid,node=None, + name = '', + originx = 0, + originy = 0, + alsoPiece = True, + piece = None, + prefix = ''): + fullName = name + ("@"+prefix if len(prefix) else "") + realName = grid.checkName(fullName) if node is None else fullName + super(Region,self).__init__(grid, + self.TAG, + node = node, + name = realName, + originx = originx, + originy = originy) + + if node is None and alsoPiece: + m = self.getMap() + b = self.getBoard() + if m is not None and b is not None: + if piece is None: + g = m.getGame() + pieces = g.getSpecificPieces(name,asdict=False) + piece = pieces[0] if len(pieces) > 0 else None + + if piece is not None: + # bname = m['mapName'] + bname = b['name'] + #print(f'Adding at-start name={name} location={realName} ' + # f'owning board={bname}') + a = m.addAtStart(name = name, + location = realName, + useGridLocation = True, + owningBoard = bname, + x = 0, + y = 0) + p = a.addPiece(piece) + if p is None: + print(f'EEE Failed to add piece {name} ({piece}) to add-start {a}') + #if p is not None: + # print(f'Added piece {name} in region') + #else: + # print(f'Could not find piece {name}') + + def getGrid(self): + return self.getParent(RegionGrid) + def getZone(self): + g = self.getGrid() + if g is not None: + return g.getZone() + return None + def getZonedGrid(self): + z = self.getZone() + if z is not None: + return z.getZonedGrid() + return None + def getBoard(self): + z = self.getZonedGrid() + if z is not None: + return z.getBoard() + return self.getParent(Board) + def getPicker(self): + z = self.getBoard() + if z is not None: + return z.getPicker() + return None + def getMap(self): + b = self.getPicker() + if b is not None: + return b.getMap() + return None + +# +# EOF +# +# ==================================================================== +# From zone.py + +# -------------------------------------------------------------------- +class ZonedGrid(Element): + TAG=Element.BOARD+'ZonedGrid' + def __init__(self,board,node=None): + super(ZonedGrid,self).__init__(board,self.TAG,node=node) + + def getBoard(self): + b = self.getParent(Board) + # print(f'Get Board of Zoned: {b}') + return b + def getPicker(self): + z = self.getBoard() + if z is not None: + return z.getPicker() + return None + def getMap(self): + z = self.getPicker() + if z is not None: + return z.getMap() + return None + def addHighlighter(self,**kwargs): + '''Add a `Highlighter` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Highlighter + The added element + ''' + return self.add(ZonedGridHighlighter,**kwargs) + def getHighlighters(self,single=True): + '''Get all or a sole `ZonedGridHighlighter` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Highligter` child, otherwise fail. + If `False` return all `Highligter` children in this element + Returns + ------- + children : list + List of `Highligter` children (even if `single=True`) + ''' + return self.getAllElements(ZonedGridHighliger,single=single) + def addZone(self,**kwargs): + '''Add a `Zone` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Zone + The added element + ''' + return self.add(Zone,**kwargs) + def getZones(self,asdict=True): + '''Get all Zone element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Zone` elements. If `False`, return a list of all Zone` children. + Returns + ------- + children : dict or list + Dictionary or list of `Zone` children + ''' + return self.getElementsByKey(Zone,'name',asdict=asdict) + +# -------------------------------------------------------------------- +class ZonedGridHighlighter(Element): + TAG=Element.BOARD+'mapgrid.ZonedGridHighlighter' + def __init__(self,zoned,node=None): + super(ZonedGridHighlighter,self).__init__(zoned,self.TAG,node=node) + def getZonedGrid(self): return self.getParent(ZonedGrid) + + +# -------------------------------------------------------------------- +class Zone(Element): + TAG = Element.BOARD+'mapgrid.Zone' + def __init__(self,zoned,node=None, + name = "", + highlightProperty = "", + locationFormat = "$gridLocation$", + path = "0,0;976,0;976,976;0,976", + useHighlight = False, + useParentGrid = False): + super(Zone,self).\ + __init__(zoned,self.TAG,node=node, + name = name, + highlightProperty = highlightProperty, + locationFormat = locationFormat, + path = path, + useHighlight = useHighlight, + useParentGrid = useParentGrid) + + def getZonedGrid(self): + z = self.getParent(ZonedGrid) + # print(f'Get Zoned of Zone {self["name"]}: {z}') + return z + + def getBoard(self): + z = self.getZonedGrid() + if z is not None: + return z.getBoard() + return None + def getPicker(self): + z = self.getBoard() + if z is not None: + return z.getPicker() + return None + def getMap(self): + z = self.getPicker() + if z is not None: + return z.getMap() + return None + def addHexGrid(self,**kwargs): + '''Add a `HexGrid` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : HexGrid + The added element + ''' + return self.add(HexGrid,**kwargs) + def addSquareGrid(self,**kwargs): + '''Add a `SquareGrid` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : SquareGrid + The added element + ''' + return self.add(SquareGrid,**kwargs) + def addRegionGrid(self,**kwargs): + '''Add a `RegionGrid` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : RegionGrid + The added element + ''' + return self.add(RegionGrid,**kwargs) + def getHexGrids(self,single=True): + '''Get all or a sole `HexGrid` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `HexGrid` child, otherwise fail. + If `False` return all `HexGrid` children in this element + Returns + ------- + children : list + List of `HexGrid` children (even if `single=True`) + ''' + return self.getAllElements(HexGrid,single=single) + def getSquareGrids(self,single=True): + '''Get all or a sole `SquareGrid` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `SquareGrid` child, otherwise fail. + If `False` return all `SquareGrid` children in this element + Returns + ------- + children : list + List of `SquareGrid` children (even if `single=True`) + ''' + return self.getAllElements(SquareGrid,single=single) + def getRegionGrids(self,single=True): + '''Get all or a sole `RegionGrid` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `RegionGrid` child, otherwise fail. + If `False` return all `RegionGrid` children in this element + Returns + ------- + children : list + List of `RegionGrid` children (even if `single=True`) + ''' + return self.getAllElements(RegionGrid,single=single) + def getGrids(self,single=True): + '''Get all or a sole `Grid` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Grid` child, otherwise fail. + If `False` return all `Grid` children in this element + Returns + ------- + children : list + List of `Grid` children (even if `single=True`) + ''' + g = self.getHexGrids(single=single) + if g is not None: return g + + g = self.getSquareGrids(single=single) + if g is not None: return g + + g = self.getRegionGrids(single=single) + if g is not None: return g + + return g + + def getPath(self): + p = self['path'].split(';') + r = [] + for pp in p: + c = pp.split(',') + r.append([int(c[0]),int(c[1])]) + return r + def getBB(self): + from functools import reduce + path = self.getPath() + llx = reduce(lambda old,point:min(point[0],old),path,100000000000) + lly = reduce(lambda old,point:min(point[1],old),path,100000000000) + urx = reduce(lambda old,point:max(point[0],old),path,-1) + ury = reduce(lambda old,point:max(point[1],old),path,-1) + return llx,lly,urx,ury + def getWidth(self): + llx,_,urx,_ = self.getBB() + return urx-llx + def getHeight(self): + _,lly,_,ury = self.getBB() + return ury-lly + def getXOffset(self): + return self.getBB()[0] + def getYOffset(self): + return self.getBB()[1] + +# +# EOF +# +# ==================================================================== +# From board.py + +# -------------------------------------------------------------------- +class BoardPicker(MapElement): + TAG = Element.MAP+'BoardPicker' + def __init__(self,doc,node=None, + addColumnText = 'Add column', + addRowText = 'Add row', + boardPrompt = 'Select board', + slotHeight = 125, + slotScale = 0.2, + slotWidth = 350, + title = 'Choose Boards'): + super(BoardPicker,self).__init__(doc,self.TAG,node=node, + addColumnText = addColumnText, + addRowText = addRowText, + boardPrompt = boardPrompt, + slotHeight = slotHeight, + slotScale = slotScale, + slotWidth = slotWidth, + title = title) + + def addSetup(self,**kwargs): + '''Add a `Setup` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Setup + The added element + ''' + if 'mapName' not in kwargs: + m = self.getMap() + kwargs['mapName'] = m.getAttribute('mapName') + + return self.add(Setup,**kwargs) + def getSetups(self,single=False): + '''Get all or a sole `Setup` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Setup` child, otherwise fail. + If `False` return all `Setup` children in this element + Returns + ------- + children : list + List of `Setup` children (even if `single=True`) + ''' + return self.getAllElements(Setup,single=single) + def addBoard(self,**kwargs): + '''Add a `Board` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Board + The added element + ''' + return self.add(Board,**kwargs) + def getBoards(self,asdict=True): + '''Get all Board element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` elements. If `False`, return a list of all Board` children. + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + ''' + return self.getElementsByKey(Board,'name',asdict=asdict) + def encode(self): + setups = self.getSetups() + if setups is not None and len(setups)>0: + return [setups[0]._node.childNodes[0].nodeValue] + + ret = [] + for bn in self.getBoards().keys(): + ret.append(bn+'BoardPicker\t'+bn+'\t0\t0') + + return ret + +# -------------------------------------------------------------------- +class Setup(Element): + TAG = 'setup' + def __init__(self,picker,node=None, + mapName = '', + maxColumns = 1, + boardNames = []): + super(Setup,self).__init__(picker,self.TAG,node=node) + col = 0 + row = 0 + lst = [f'{mapName}BoardPicker'] + for bn in boardNames: + lst.extend([bn,str(col),str(row)]) + col += 1 + if col >= maxColumns: + col = 0 + row += 1 + + txt = r' '.join(lst) + self.addText(txt) + def getPicker(self): return self.getParent(BoardPicker) + # -------------------------------------------------------------------- -def add_language(root,elem): - return add_node(root,elem,'VASSAL.i18n.Language') +class Board(Element): + TAG = Element.PICKER+'Board' + def __init__(self,picker,node=None, + name = '', + image = '', + reversible = False, + color = rgb(255,255,255), + width = 0, + height = 0): + super(Board,self).__init__(picker,self.TAG,node=node, + image = image, + name = name, + reversible = reversible, + color = color, + width = width, + height = height) + + def getPicker(self): return self.getParent(BoardPicker) + def getMap(self): + z = self.getPicker() + if z is not None: + return z.getMap() + return None + def addZonedGrid(self,**kwargs): + '''Add a `ZonedGrid` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : ZonedGrid + The added element + ''' + return self.add(ZonedGrid,**kwargs) + def getZonedGrids(self,single=True): + '''Get all or a sole `ZonedGrid` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `ZonedGrid` child, otherwise fail. + If `False` return all `ZonedGrid` children in this element + Returns + ------- + children : list + List of `ZonedGrid` children (even if `single=True`) + ''' + return self.getAllElements(ZonedGrid,single=single) + def getZones(self,asdict=True): + '''Get all Zone element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Zone` elements. If `False`, return a list of all Zone` children. + Returns + ------- + children : dict or list + Dictionary or list of `Zone` children + ''' + zoned = self.getZonedGrids(single=True) + if zoned is None: return None + + return zoned[0].getZones(asdict=asdict) + + def getWidth(self): + # print(f'Getting width of {self}: {self["width"]}') + if 'width' in self and int(self['width']) != 0: + return int(self['width']) + return 0 + + def getHeight(self): + # print(f'Getting height of {self}: {self["height"]}') + if 'height' in self and int(self['height']) != 0: + return int(self['height']) + return 0 + +# +# EOF +# +# ==================================================================== +# From map.py + +# -------------------------------------------------------------------- +class BaseMap(Element): + def __init__(self,doc,tag,node=None, + mapName = '', + allowMultiple = 'false', + backgroundcolor = rgb(255,255,255), + buttonName = '', + changeFormat = '$message$', + color = rgb(0,0,0), # Selected pieces + createFormat = '$pieceName$ created in $location$ *', + edgeHeight = '0', + edgeWidth = '0', + hideKey = '', + hotkey = key('M',ALT), + icon = '/images/map.gif', + launch = 'false', + markMoved = 'Always', + markUnmovedHotkey = '', + markUnmovedIcon = '/images/unmoved.gif', + markUnmovedReport = '', + markUnmovedText = '', + markUnmovedTooltip = 'Mark all pieces on this map as not moved', + moveKey = '', + moveToFormat = '$pieceName$ moves $previousLocation$ → $location$ *', + moveWithinFormat = '$pieceName$ moves $previousLocation$ → $location$ *', + showKey = '', + thickness = '3'): + super(BaseMap,self).__init__(doc,tag,node=node, + allowMultiple = allowMultiple, + backgroundcolor = backgroundcolor, + buttonName = buttonName, + changeFormat = changeFormat, + color = color, + createFormat = createFormat, + edgeHeight = edgeHeight, + edgeWidth = edgeWidth, + hideKey = hideKey, + hotkey = hotkey, + icon = icon, + launch = launch, + mapName = mapName, + markMoved = markMoved, + markUnmovedHotkey = markUnmovedHotkey, + markUnmovedIcon = markUnmovedIcon, + markUnmovedReport = markUnmovedReport, + markUnmovedText = markUnmovedText, + markUnmovedTooltip = markUnmovedTooltip, + moveKey = moveKey, + moveToFormat = moveToFormat, + moveWithinFormat = moveWithinFormat, + showKey = showKey, + thickness = thickness) + + def getGame(self): + '''Get the game''' + return self.getParentOfClass([Game]) + def addPicker(self,**kwargs): + '''Add a `Picker` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Picker + The added element + ''' + return self.add(BoardPicker,**kwargs) + def getBoardPicker(self,single=True): + '''Get all or a sole `BoardPicker` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `BoardPicker` child, otherwise fail. + If `False` return all `BoardPicker` children in this element + Returns + ------- + children : list + List of `BoardPicker` children (even if `single=True`) + ''' + return self.getAllElements(BoardPicker,single) + def getPicker(self,single=True): + '''Get all or a sole `BoardPicker` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `BoardPicker` child, otherwise fail. + If `False` return all `BoardPicker` children in this element + Returns + ------- + children : list + List of `BoardPicker` children (even if `single=True`) + ''' + return self.getAllElements(BoardPicker,single) + def getStackMetrics(self,single=True): + '''Get all or a sole `StackMetric` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `StackMetric` child, otherwise fail. + If `False` return all `StackMetric` children in this element + Returns + ------- + children : list + List of `StackMetric` children (even if `single=True`) + ''' + return self.getAllElements(StackMetrics,single) + def getImageSaver(self,single=True): + '''Get all or a sole `ImageSaver` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `ImageSaver` child, otherwise fail. + If `False` return all `ImageSaver` children in this element + Returns + ------- + children : list + List of `ImageSaver` children (even if `single=True`) + ''' + return self.getAllElements(ImageSaver,single) + def getTextSaver(self,single=True): + '''Get all or a sole `TextSaver` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `TextSaver` child, otherwise fail. + If `False` return all `TextSaver` children in this element + Returns + ------- + children : list + List of `TextSaver` children (even if `single=True`) + ''' + return self.getAllElements(TextSaver,single) + def getForwardToChatter(self,single=True): + '''Get all or a sole `ForwardToChatter` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `ForwardToChatter` child, otherwise fail. + If `False` return all `ForwardToChatter` children in this element + Returns + ------- + children : list + List of `ForwardToChatter` children (even if `single=True`) + ''' + return self.getAllElements(ForwardToChatter,single) + def getMenuDisplayer(self,single=True): + '''Get all or a sole `MenuDi` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `MenuDi` child, otherwise fail. + If `False` return all `MenuDi` children in this element + Returns + ------- + children : list + List of `MenuDi` children (even if `single=True`) + ''' + return self.getAllElements(MenuDisplayer,single) + def getMapCenterer(self,single=True): + '''Get all or a sole `MapCenterer` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `MapCenterer` child, otherwise fail. + If `False` return all `MapCenterer` children in this element + Returns + ------- + children : list + List of `MapCenterer` children (even if `single=True`) + ''' + return self.getAllElements(MapCenterer,single) + def getStackExpander(self,single=True): + '''Get all or a sole `StackExpander` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `StackExpander` child, otherwise fail. + If `False` return all `StackExpander` children in this element + Returns + ------- + children : list + List of `StackExpander` children (even if `single=True`) + ''' + return self.getAllElements(StackExpander,single) + def getPieceMover(self,single=True): + '''Get all or a sole `PieceMover` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `PieceMover` child, otherwise fail. + If `False` return all `PieceMover` children in this element + Returns + ------- + children : list + List of `PieceMover` children (even if `single=True`) + ''' + return self.getAllElements(PieceMover,single) + def getSelectionHighlighters(self,single=True): + '''Get all or a sole `SelectionHighlighter` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `SelectionHighlighter` child, otherwise fail. + If `False` return all `SelectionHighlighter` children in this element + Returns + ------- + children : list + List of `SelectionHighlighter` children (even if `single=True`) + ''' + return self.getAllElements(SelectionHighlighters,single) + def getKeyBufferer(self,single=True): + return self.getAllElements(KeyBufferer,single) + def getHighlightLastMoved(self,single=True): + '''Get all or a sole `HighlightLa` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `HighlightLa` child, otherwise fail. + If `False` return all `HighlightLa` children in this element + Returns + ------- + children : list + List of `HighlightLa` children (even if `single=True`) + ''' + return self.getAllElements(HighlightLastMoved,single) + def getCounterDetailViewer(self,single=True): + '''Get all or a sole `CounterDetailViewer` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `CounterDetailViewer` child, otherwise fail. + If `False` return all `CounterDetailViewer` children in this element + Returns + ------- + children : list + List of `CounterDetailViewer` children (even if `single=True`) + ''' + return self.getAllElements(CounterDetailViewer,single) + def getGlobalMap(self,single=True): + '''Get all or a sole `GlobalMap` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `GlobalMap` child, otherwise fail. + If `False` return all `GlobalMap` children in this element + Returns + ------- + children : list + List of `GlobalMap` children (even if `single=True`) + ''' + return self.getAllElements(GlobalMap,single) + def getZoomer(self,single=True): + '''Get all or a sole `Zoomer` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Zoomer` child, otherwise fail. + If `False` return all `Zoomer` children in this element + Returns + ------- + children : list + List of `Zoomer` children (even if `single=True`) + ''' + return self.getAllElements(Zoomer,single) + def getHidePiecesButton(self,single=True): + '''Get all or a sole `HidePiece` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `HidePiece` child, otherwise fail. + If `False` return all `HidePiece` children in this element + Returns + ------- + children : list + List of `HidePiece` children (even if `single=True`) + ''' + return self.getAllElements(HidePiecesButton,single) + def getMassKeys(self,asdict=True): + '''Get all MassKey element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `MassKey` elements. If `False`, return a list of all MassKey` children. + Returns + ------- + children : dict or list + Dictionary or list of `MassKey` children + ''' + return self.getElementsByKey(MassKey,'name',asdict) + def getFlare(self,single=True): + '''Get all or a sole `Flare` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Flare` child, otherwise fail. + If `False` return all `Flare` children in this element + Returns + ------- + children : list + List of `Flare` children (even if `single=True`) + ''' + return self.getAllElements(Flare,single) + def getAtStarts(self,single=True): + '''Get all or a sole `AtStart` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `AtStart` child, otherwise fail. + If `False` return all `AtStart` children in this element + Returns + ------- + children : list + List of `AtStart` children (even if `single=True`) + ''' + return self.getAllElements(AtStart,single) + def getBoards(self,asdict=True): + '''Get all Board element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` elements. If `False`, return a list of all Board` children. + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + ''' + picker = self.getPicker() + if picker is None: return None + return picker[0].getBoards(asdict=asdict) + def addBoardPicker(self,**kwargs): + '''Add a `BoardPicker` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : BoardPicker + The added element + ''' + return self.add(BoardPicker,**kwargs) + def addStackMetrics(self,**kwargs): + '''Add a `StackMetrics` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : StackMetrics + The added element + ''' + return self.add(StackMetrics,**kwargs) + def addImageSaver(self,**kwargs): + '''Add a `ImageSaver` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : ImageSaver + The added element + ''' + return self.add(ImageSaver,**kwargs) + def addTextSaver(self,**kwargs): + '''Add a `TextSaver` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : TextSaver + The added element + ''' + return self.add(TextSaver,**kwargs) + def addForwardToChatter(self,**kwargs): + '''Add a `ForwardToChatter` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : ForwardToChatter + The added element + ''' + return self.add(ForwardToChatter,**kwargs) + def addMenuDisplayer(self,**kwargs): + '''Add a `MenuDisplayer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : MenuDisplayer + The added element + ''' + return self.add(MenuDisplayer,**kwargs) + def addMapCenterer(self,**kwargs): + '''Add a `MapCenterer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : MapCenterer + The added element + ''' + return self.add(MapCenterer,**kwargs) + def addStackExpander(self,**kwargs): + '''Add a `StackExpander` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : StackExpander + The added element + ''' + return self.add(StackExpander,**kwargs) + def addPieceMover(self,**kwargs): + '''Add a `PieceMover` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PieceMover + The added element + ''' + return self.add(PieceMover,**kwargs) + def addSelectionHighlighters(self,**kwargs): + '''Add a `SelectionHighlighters` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : SelectionHighlighters + The added element + ''' + return self.add(SelectionHighlighters,**kwargs) + def addKeyBufferer(self,**kwargs): + '''Add a `KeyBufferer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : KeyBufferer + The added element + ''' + return self.add(KeyBufferer,**kwargs) + def addHighlightLastMoved(self,**kwargs): + '''Add a `HighlightLastMoved` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : HighlightLastMoved + The added element + ''' + return self.add(HighlightLastMoved,**kwargs) + def addCounterDetailViewer(self,**kwargs): + '''Add a `CounterDetailViewer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : CounterDetailViewer + The added element + ''' + return self.add(CounterDetailViewer,**kwargs) + def addGlobalMap(self,**kwargs): + '''Add a `GlobalMap` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : GlobalMap + The added element + ''' + return self.add(GlobalMap,**kwargs) + def addZoomer(self,**kwargs): + '''Add a `Zoomer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Zoomer + The added element + ''' + return self.add(Zoomer,**kwargs) + def addHidePiecesButton(self,**kwargs): + '''Add a `HidePiecesButton` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : HidePiecesButton + The added element + ''' + return self.add(HidePiecesButton,**kwargs) + def addMassKey(self,**kwargs): + '''Add a `MassKey` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : MassKey + The added element + ''' + return self.add(MassKey,**kwargs) + def addStartupMassKey(self,**kwargs): + '''Add a `StartupMassKey` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : StartupMassKey + The added element + ''' + return self.add(MassKey,**kwargs) + def addFlare(self,**kwargs): + '''Add a `Flare` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Flare + The added element + ''' + return self.add(Flare,**kwargs) + def addAtStart(self,**kwargs): + '''Add a `AtStart` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : AtStart + The added element + ''' + return self.add(AtStart,**kwargs) + +# -------------------------------------------------------------------- +class Map(BaseMap): + TAG = Element.MODULE+'Map' + def __init__(self,doc,node=None, + mapName = '', + allowMultiple = 'false', + backgroundcolor = rgb(255,255,255), + buttonName = '', + changeFormat = '$message$', + color = rgb(0,0,0), + createFormat = '$pieceName$ created in $location$ *', + edgeHeight = '0', + edgeWidth = '0', + hideKey = '', + hotkey = key('M',ALT), + icon = '/images/map.gif', + launch = 'false', + markMoved = 'Always', + markUnmovedHotkey = '', + markUnmovedIcon = '/images/unmoved.gif', + markUnmovedReport = '', + markUnmovedText = '', + markUnmovedTooltip = 'Mark all pieces on this map as not moved', + moveKey = '', + moveToFormat = '$pieceName$ moves $previousLocation$ → $location$ *', + moveWithinFormat = '$pieceName$ moves $previousLocation$ → $location$ *', + showKey = '', + thickness = '3'): + super(Map,self).__init__(doc,self.TAG,node=node, + allowMultiple = allowMultiple, + backgroundcolor = backgroundcolor, + buttonName = buttonName, + changeFormat = changeFormat, + color = color, + createFormat = createFormat, + edgeHeight = edgeHeight, + edgeWidth = edgeWidth, + hideKey = hideKey, + hotkey = hotkey, + icon = icon, + launch = launch, + mapName = mapName, + markMoved = markMoved, + markUnmovedHotkey = markUnmovedHotkey, + markUnmovedIcon = markUnmovedIcon, + markUnmovedReport = markUnmovedReport, + markUnmovedText = markUnmovedText, + markUnmovedTooltip = markUnmovedTooltip, + moveKey = moveKey, + moveToFormat = moveToFormat, + moveWithinFormat = moveWithinFormat, + showKey = showKey, + thickness = thickness) + + def getGame(self): + return self.getParent(Game) + # -------------------------------------------------------------------- -def add_chatter(root,elem): - return add_node(root,elem,MODULE+'Chatter') +class WidgetMap(BaseMap): + TAG = Element.WIDGET+'WidgetMap' + def __init__(self,doc,node=None,**attr): + super(WidgetMap,self).__init__(doc,self.TAG,node=node,**attr) + + def getGame(self): + return self.getParentOfClass([Game]) + def getMapWidget(self): + return self.getParent(MapWidget) + + +# +# EOF +# +# ==================================================================== +# From chart.py # -------------------------------------------------------------------- -def add_keynamer(root,elem): - return add_node(root,elem,MODULE+'KeyNamer') +class ChartWindow(GameElement,WidgetElement): + TAG=Element.MODULE+'ChartWindow' + def __init__(self,elem,node=None, + name = '', + hotkey = key('A',ALT), + description = '', + text = '', + tooltip = 'Show/hide Charts', + icon = '/images/chart.gif'): + super(ChartWindow,self).__init__(elem,self.TAG,node=node, + name = name, + hotkey = hotkey, + description = description, + text = text, + tooltip = tooltip, + icon = icon) + + def addChart(self,**kwargs): + '''Add a `Chart` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Chart + The added element + ''' + return self.add(Chart,**kwargs) + def getCharts(self,asdict=True): + '''Get all Chart element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Chart` elements. If `False`, return a list of all Chart` children. + Returns + ------- + children : dict or list + Dictionary or list of `Chart` children + ''' + return self.getElementsById(Chart,'chartName',asdict=asdict) + + # -------------------------------------------------------------------- -def add_documentation(root,doc): - return add_node(root,doc,MODULE+'Documentation') +class Chart(Element): + TAG=Element.WIDGET+'Chart' + def __init__(self,elem,node=None, + chartName = '', + fileName = '', + description = ''): + super(Chart,self).__init__(elem,self.TAG,node=node, + chartName = chartName, + description = description, + fileName = fileName) + +# +# EOF +# +# ==================================================================== +# From command.py # -------------------------------------------------------------------- -def add_about(root,doc,title,fileName=""): - return add_node(root,doc,MODULE+'documentation.AboutScreen', - fileName = fileName, - title = title) +class Command: + def __init__(self,what,iden,tpe,state): + self.cmd = '/'.join([what,iden,tpe,state]) + # -------------------------------------------------------------------- -def add_pdffile(root,doc,title,pdfFile): - return add_node(root,doc,MODULE+'documentation.BrowserPDFFile', - pdfFile = pdfFile, title=title) +class AddCommand(Command): + ID = '+' + def __init__(self,iden,tpe,state): + super(AddCommand,self).__init__(self.ID,iden,tpe,state) + + +# +# EOF +# +# ==================================================================== +# From trait.py +# ==================================================================== +class Trait: + known_traits = [] + def __init__(self): + '''Base class for trait capture. Unlike the Element classes, + this actually holds state that isn't reflected elsewhere in + the DOM. This means that the data here is local to the + object. So when we do + + piece = foo.getPieceSlots()[0] + traits = p.getTraits() + for trait in traits: + if trait.ID == 'piece': + trait["gpid"] = newPid + trait["lpid"] = newPid + + we do not actually change anything in the DOM. To do that, we + must add back _all_ the traits as + + piece.setTraits(traits) + + We can add traits to a piece, like + + piece.addTrait(MarkTrait('Hello','World')) + + But it is not particularly efficient. Better to do + (continuing from above) + + traits.append(MarkTrait('Hello','World;) + piece.setTraits(traits) + + ''' + self._type = None + self._state = None + + def setType(self,**kwargs): + '''Set types. Dictionary of names and values. Dictonary keys + defines how we access the fields, which is internal here. + What matters is the order of the values. + + ''' + self._type = list(kwargs.values()) + self._tnames = list(kwargs.keys()) + def setState(self,**kwargs): + '''Set states. Dictionary of names and values. Dictonary keys + defines how we access the fields, which is internal here. + What matters is the order of the values. + ''' + self._state = list(kwargs.values()) + self._snames = list(kwargs.keys()) + + + def __getitem__(self,key): + '''Look up item in either type or state''' + try: + return self._type[self._tnames.index(key)] + except: + pass + return self._state[self._snames.index(key)] + + def __setitem__(self,key,value): + '''Set item in either type or state''' + try: + self._type[self._tnames.index(key)] = value + return + except: + pass + self._state[self._snames.index(key)] = value + + def encode(self,term=False): + ''' + returns type and state encoded''' + t = self.encodeFields(self.ID,*self._type,term=term) + s = self.encodeFields(*self._state,term=term) + return t,s + + @classmethod + def findTrait(cls,traits,ID,key=None,value=None,verbose=False): + for trait in traits: + if trait.ID != ID: + continue + if verbose: + print(f' {trait.ID}') + if key is None or value is None: + if verbose: + print(f' Return {trait.ID}') + return trait + if verbose: + print(f' Check {key}={value}: {trait[key]}') + if trait[key] == value: + return trait + if verbose: + print(f' Trait of type {ID} with {key}={value} not found') + return None + + @classmethod + def take(cls,iden,t,s): + '''If the first part of the string t matches the ID, then take it. + + t and s are lists of strings. + ''' + if iden != cls.ID: return None + + ret = cls() + ret._type = t + ret._state = s + ret.check() # Check if we're reasonable, or raise + #print(f'Took {iden} {cls}\n' + # f' {ret._tnames}\n' + # f' {ret._snames}') + return ret + + def check(self): + '''Implement if trait should check that all is OK when cloning''' + pass + + @classmethod + def encodeFields(cls,*args,term=False): + return ';'.join([str(e).lower() if isinstance(e,bool) else str(e) + for e in args])+(';' if term else '') + + @classmethod + def decodeFields(cls,s): + from re import split + return split(r'(? 0, \ + # 'No name specified for ChangePropertyTriat' + super(ChangePropertyTrait,self).__init__() + self._constraints = self.encodeConstraints(numeric,wrap,min,max) + self._commands = self.encodeCommands(commands) + + def encodeConstraints(self,numeric,wrap,min,max): + isnum = f'{numeric}'.lower() + iswrap = f'{wrap}'.lower() + return f'{isnum},{min},{max},{iswrap}' + + def decodeConstraints(self,constraints): + f = Trait.decodeKeys(constraints) + return f[0]=='true',f[3]=='true',int(f[1]),int(f[2]) + + def encodeCommands(self,commands): + cmds = [] + for cmd in commands: + # print(cmd) + com = cmd[0] + ':' + cmd[1].replace(',',r'\,') + ':' + cmd[2] + if cmd[2] == self.DIRECT: + com += f'\,'+cmd[3].replace(':',r'\:') + cmds.append(com) + return ','.join(cmds) + + def decodeCommands(self,commands): + cmds = Trait.decodeKeys(commands) + ret = [] + for cmd in cmds: + parts = Trait.decodeKeys(cmd,':') + # print('parts',parts) + if parts[-1][0] == self.DIRECT: + parts = parts[:-1]+Trait.decodeKeys(parts[-1],',') + ret.append(parts) + # print(commands,parts) + return ret + + def getCommands(self): + return self.decodeCommands(self['commands']) + def setCommands(self,commands): + self['commands'] = self.encodeCommands(commands) + + def check(self): + assert len(self['name']) > 0,\ + f'No name given for ChangePropertyTrait' + + # -------------------------------------------------------------------- -# CurrentMap == "Board" -def add_inventory(root,doc, - canDisable = False, - centerOnPiece = True, - disabledIcon = '', - drawPieces = True, - foldersOnly = False, - forwardKeystroke = True, - groupBy = '', - hotkey = key('I',ALT), - icon = '/images/inventory.gif', - include = '{}', - launchFunction = 'functionHide', - leafFormat = '$PieceName$', - name = '', - nonLeafFormat = '$PropertyValue$', - pieceZoom = '0.33', - pieceZoom2 = '0.5', - pieceZoom3 = '0.6', - propertyGate = '', - refreshHotkey = key('I',ALT_SHIFT), - showMenu = True, - sides = '', - sortFormat = '$PieceName$', - sortPieces = True, - sorting = 'alpha', - text = '', - tooltip = 'Show inventory of all pieces', - zoomOn = False): - return add_node(root,doc,MODULE+'Inventory', - canDisable = canDisable, - centerOnPiece = centerOnPiece, - disabledIcon = disabledIcon, - drawPieces = drawPieces, - foldersOnly = foldersOnly, - forwardKeystroke = forwardKeystroke, - groupBy = groupBy, - hotkey = hotkey, - icon = icon, - include = include, - launchFunction = launchFunction, - leafFormat = leafFormat, - name = name, - nonLeafFormat = nonLeafFormat, - pieceZoom = pieceZoom, - pieceZoom2 = pieceZoom2, - pieceZoom3 = pieceZoom3, - propertyGate = propertyGate, - refreshHotkey = refreshHotkey, - showMenu = showMenu, - sides = sides, - sortFormat = sortFormat, - sortPieces = sortPieces, - sorting = sorting, - text = text, - tooltip = tooltip, - zoomOn = zoomOn) +class DynamicPropertyTrait(ChangePropertyTrait): + ID = 'PROP' + def __init__(self, + *commands, + name = '', + value = 0, + numeric = False, + min = 0, + max = 100, + wrap = False, + description = ''): + '''Commands are + + - menu + - key + - Type (only 'P' for now) + - Expression + ''' + super(DynamicPropertyTrait,self).__init__(*commands, + numeric = numeric, + min = min, + max = max, + wrap = wrap) + # print(commands,'Name',name) + self.setType(name = name, + constraints = self._constraints, + commands = self._commands, + description = description) + self.setState(value=value) + - +Trait.known_traits.append(DynamicPropertyTrait) + +# +# EOF +# +# ==================================================================== +# From traits/globalproperty.py + +# -------------------------------------------------------------------- +class GlobalPropertyTrait(ChangePropertyTrait): + # The real value of CURRENT_ZONE causes problems when copying the + # trait, since it contains slashes. Maybe a solition is to make + # it a raw string with escaped slashes? No, that's already done + # below when setting the type. However, the default in the Java + # code is the CURRENT_ZONE real value, so setting this to the + # empty string should make it be that value. + ID = 'setprop' + CURRENT_ZONE = 'Current Zone/Current Map/Module' + NAMED_ZONE = 'Named Zone' + NAMED_MAP = 'Named Map' + DIRECT = 'P' + def __init__(self, + *commands, + name = '', + numeric = False, + min = 0, + max = 100, + wrap = False, + description = '', + level = CURRENT_ZONE, + search = ''): + '''Commands are + + - menu + - key + - Type (only 'P' for now) + - Expression + ''' + super(GlobalPropertyTrait,self).__init__(*commands, + numeric = numeric, + min = min, + max = max, + wrap = wrap) + self.setType(name = name, + constraints = self._constraints, + commands = self._commands, + description = description, + level = level.replace('/',r'\/'), + search = search) + self.setState() + +Trait.known_traits.append(GlobalPropertyTrait) + +# +# EOF +# +# ==================================================================== +# From traits/prototype.py # -------------------------------------------------------------------- -def add_map(root,doc, - mapName, - allowMultiple = 'false', - backgroundcolor = rgb(255,255,255), - buttonName = '', - changeFormat = '$message$', - color = rgb(0,0,0), - createFormat = '$pieceName$ created in $location$ *', - edgeHeight = '0', - edgeWidth = '0', - hideKey = '', - hotkey = key('M',ALT), - icon = '/images/map.gif', - launch = 'false', - markMoved = 'Always', - markUnmovedHotkey = '', - markUnmovedIcon = '/images/unmoved.gif', - markUnmovedReport = '', - markUnmovedText = '', - markUnmovedTooltip = 'Mark all pieces on this map as not moved', - moveKey = '', - moveToFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *', - moveWithinFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *', - showKey = '', - thickness = '3', - cls = MODULE+'Map'): - return add_node(root,doc,cls, - allowMultiple = allowMultiple, - backgroundcolor = backgroundcolor, - buttonName = buttonName, - changeFormat = changeFormat, - color = color, - createFormat = createFormat, - edgeHeight = edgeHeight, - edgeWidth = edgeWidth, - hideKey = hideKey, - hotkey = hotkey, - icon = icon, - launch = launch, - mapName = mapName, - markMoved = markMoved, - markUnmovedHotkey = markUnmovedHotkey, - markUnmovedIcon = markUnmovedIcon, - markUnmovedReport = markUnmovedReport, - markUnmovedText = markUnmovedText, - markUnmovedTooltip = markUnmovedTooltip, - moveKey = moveKey, - moveToFormat = moveToFormat, - moveWithinFormat = moveWithinFormat, - showKey = showKey, - thickness = thickness) - -# -------------------------------------------------------------------- -def add_widgetmap(root,doc, mapName,**attr): - return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr) - -# -------------------------------------------------------------------- -def add_boardpicker(root,doc, - addColumnText = 'Add column', - addRowText = 'Add row', - boardPrompt = 'Select board', - slotHeight = 125, - slotScale = 0.2, - slotWidth = 350, - title = 'Choose Boards'): - return add_node(root,doc,MAP+'BoardPicker', - addColumnText = addColumnText, - addRowText = addRowText, - boardPrompt = boardPrompt, - slotHeight = slotHeight, - slotScale = slotScale, - slotWidth = slotWidth, - title = title) - -# -------------------------------------------------------------------- -def add_setup(root,picker,mapName,maxColumns,boardNames): - '''Add given boards in row-first up to maxColumns per row''' - s = add_node(root,picker,'setup') - col = 0 - row = 0 - lst = [f'{mapName}BoardPicker'] - for bn in boardNames: - lst.extend([bn,str(col),str(row)]) - col += 1 - if col >= maxColumns: - col = 0 - row += 1 - txt = r' '.join(lst) - add_text(root,s,txt) - -# -------------------------------------------------------------------- -def add_board(root,picker,name, - image = '', - reversible = False, - color = rgb(255,255,255), - width = 0, - height = 0): - return add_node(root,picker,PICKER+'Board', - image = image, - name = name, - reversible = reversible, - color = color, - width = width, - height = height) - -# -------------------------------------------------------------------- -def add_zoned(root,board): - return add_node(root,board,PICKER+'board.ZonedGrid') - -# -------------------------------------------------------------------- -def add_zonehighlighter(root,zoned): - return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter') - -# -------------------------------------------------------------------- -def add_zone(root,zoned, - name = "", - highlightProperty = "", - locationFormat = "$gridLocation$", - path = "0,0;976,0;976,976;0,976", - useHighlight = False, - useParentGrid = False): - return add_node(root,zoned,PICKER+'board.mapgrid.Zone', - name = name, - highlightProperty = highlightProperty, - locationFormat = locationFormat, - path = path, - useHighlight = useHighlight, - useParentGrid = useParentGrid) +class PrototypeTrait(Trait): + ID = 'prototype' + def __init__(self,name=''): + '''Create a prototype trait (VASSAL.counter.UsePrototype)''' + super(PrototypeTrait,self).__init__() + self.setType(name = name) + self.setState(ignored = '') + + +Trait.known_traits.append(PrototypeTrait) + +# +# EOF +# +# ==================================================================== +# From traits/place.py + +# -------------------------------------------------------------------- +class PlaceTrait(Trait): + ID = 'placemark' + STACK_TOP = 0 + STACK_BOTTOM = 1 + ABOVE = 2 + BELOW = 3 + + # How the LaTeX exporter organises the units. Format with + # 0: the group + # 1: the piece name + # SKEL_PATH = (PieceWindow.TAG +r':Counters\/' + + # TabWidget.TAG +r':Counters\/' + + # PanelWidget.TAG +':{0}' +r'\/'+ + # ListWidget.TAG +':{0} counters'+r'\/'+ + # PieceSlot.TAG +':{1}') + @classmethod + @property + def SKEL_PATH(cls): + + return (PieceWindow.TAG +r':Counters\/' + + TabWidget.TAG +r':Counters\/' + + PanelWidget.TAG +':{0}' +r'\/'+ + ListWidget.TAG +':{0} counters'+r'\/'+ + PieceSlot.TAG +':{1}') + + def __init__(self, + command = '', # Context menu name + key = '', # Context menu key + markerSpec = '', # Full path in module + markerText = 'null', # Hard coded message + xOffset = 0, + yOffset = 0, + matchRotation = True, + afterKey = '', + description = '', + gpid = '', # Set in JAVA, but with warning + placement = ABOVE, + above = False): + '''Create a place marker trait (VASSAL.counter.PlaceMarker)''' + super(PlaceTrait,self).__init__() + self.setType(command = command, # Context menu name + key = key, # Context menu key + markerSpec = markerSpec, + markerText = markerText, + xOffset = xOffset, + yOffset = yOffset, + matchRotation = matchRotation, + afterKey = afterKey, + description = description, + gpid = gpid, + placement = placement, + above = above) + self.setState() + +Trait.known_traits.append(PlaceTrait) + +# -------------------------------------------------------------------- +class ReplaceTrait(PlaceTrait): + ID = 'replace' + def __init__(self, + command = '', # Context menu name + key = '', # Context menu key + markerSpec = '', # Full path in module + markerText = 'null', # Hard message + xOffset = 0, + yOffset = 0, + matchRotation = True, + afterKey = '', + description = '', + gpid = '', # Set in JAVA + placement = PlaceTrait.ABOVE, + above = False): + super(ReplaceTrait,self).__init__(command = command, + key = key, + markerSpec = markerSpec, + markerText = markerText, + xOffset = xOffset, + yOffset = yOffset, + matchRotation = matchRotation, + afterKey = afterKey, + description = description, + gpid = gpid, + placement = placement, + above = above) + + +Trait.known_traits.append(ReplaceTrait) + +# +# EOF +# +# ==================================================================== +# From traits/report.py # -------------------------------------------------------------------- -HEX_WIDTH = 88.50779626676963 -HEX_HEIGHT = 102.2 -def add_hexgrid(root,zone, - color = rgb(0,0,0), - cornersLegal = False, - dotsVisible = False, - dx = HEX_WIDTH, - dy = HEX_HEIGHT, - edgesLegal = False, - sideways = False, - snapTo = True, - visible = True, - x0 = 0, - y0 = 32, - cls = PICKER+'board.HexGrid'): - return add_node(root,zone,cls, - color = color, - cornersLegal = cornersLegal, - dotsVisible = dotsVisible, - dx = dx, - dy = dy, - edgesLegal = edgesLegal, - sideways = sideways, - snapTo = snapTo, - visible = visible, - x0 = x0, - y0 = y0) - -# -------------------------------------------------------------------- -def add_hexnumbering(root,grid, - color = rgb(255,0,0), - first = 'H', - fontSize = 24, - hDescend = False, - hDrawOff = 0, - hLeading = 1, - hOff = 0, - hType = 'A', - locationFormat = '$gridLocation$', - rotateText = 0, - sep = '', - stagger = True, - vDescend = False, - vDrawOff = 32, - vLeading = 0, - vOff = 0, - vType = 'N', - visible = True, - cls = PICKER+'board.mapgrid.HexGridNumbering'): - return add_node(root,grid,cls, - color = color, - first = first, - fontSize = fontSize, - hDescend = hDescend, - hDrawOff = hDrawOff, - hLeading = hLeading, - hOff = hOff, - hType = hType, - locationFormat = locationFormat, - rotateText = rotateText, - sep = sep, - stagger = stagger, - vDescend = vDescend, - vDrawOff = vDrawOff, - vLeading = vLeading, - vOff = vOff, - vType = vType, - visible = visible) +class ReportTrait (Trait): + ID = 'report' + def __init__(self, + *keys, + nosuppress = True, + description = '', + report = '$location$: $newPieceName$ $menuCommand$ *', + cyclekeys = '', + cyclereps = ''): + '''Create a report trait (VASSAL.counters.ReportActon)''' + super(ReportTrait,self).__init__() + esckeys = ','.join([k.replace(',',r'\,') for k in keys]) + esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys]) + escreps = ','.join([k.replace(',',r'\,') for k in cyclereps]) + + self.setType(keys = esckeys, + report = report, + cycleKeys = esccycl, + cycleReports = escreps, + description = description, + nosuppress = nosuppress) + self.setState(cycle = -1) + +Trait.known_traits.append(ReportTrait) + +# +# EOF +# +# ==================================================================== +# From traits/calculatedproperty.py # -------------------------------------------------------------------- -RECT_WIDTH = 80 -RECT_HEIGHT = 80 -def add_squaregrid(root,zone, - dx = RECT_WIDTH, - dy = RECT_HEIGHT, - edgesLegal = False, - x0 = 0, - y0 = int(0.4*RECT_HEIGHT), - **attr): - return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid', - dx = dx, - dy = dy, - edgesLegal = edgesLegal, - x0 = x0, - y0 = y0, - **attr) - -# -------------------------------------------------------------------- -def add_squarenumbering(root,grid, - hType = 'N', - **attr): - return add_hexnumbering(root,grid, - cls=PICKER+'board.mapgrid.SquareGridNumbering', - hType = hType, - **attr) - -# -------------------------------------------------------------------- -def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True): - # print(f'Make region grid visible: {visible}') - return add_node(root,zone,PICKER+'board.RegionGrid', - fontsize = fontsize, - snapto = snapto, - visble = visible) - -# -------------------------------------------------------------------- -def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False): - if not alsoPiece: - return None, None - - # Find the parent map element by stepping up the tree - from functools import reduce - - map = reduce(lambda nn, _: None if nn is None else nn.parentNode, - range(5), grid) - if map is None or \ - map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']: - if map is None: - print(f' = Map not found from grid') - return None, None - - if piece is not None: - print(f' Using supplied piece') - return piece, map - - # Find the piece by searching from the top of the tree - piece = get_pieces(root,root).get(name,None) - # if piece is None: - # print(list(get_pieces(root,root).keys())) - if verbose: - print(f' Possible piece for region {name}: {piece}') - return piece, map - -# -------------------------------------------------------------------- -def check_region_name(grid,name): - post = '' - targ = name - same = 0 - regs = list(get_regions(grid).keys()) - poss = len([e for e in regs if e.startswith(name)]) - if poss == 0: - return name - - return name + f' {poss}' - # Loop over keys, sorted - # regs.sort() - # for r in regs: - # if targ == r: - # same += 1 - # targ = name + f' {same:02d}' - # print(f' Found region named {r} -> {targ}') - - return targ - -# -------------------------------------------------------------------- -def add_region(root,grid,name,originx,originy,gpid, - alsoPiece=True,piece=None,verbose=True): - # First thing is to check if we have a piece - piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose) - if verbose: - print(f' Region {name} -> piece {piece}') - - - nam = check_region_name(grid,name) - if verbose: - print(f' Region {name} -> real name {nam}') - n = add_node(root,grid,PICKER+'board.Region', - name = nam, - originx = originx, - originy = originy) - if piece is None: - return n, gpid - - if verbose: - print(f' Add piece {name} to region') - _, gpid = add_atstart(root,map,name,piece, - location = nam, - useGridLocation = True, - owningBoard = map.getAttribute('mapName'), - x = 0, - y = 0, - gpid = gpid) - return n, gpid - - -# -------------------------------------------------------------------- -def add_stacking(root,map, - bottom = '40,0', - disabled = False, - down = '37,0', - exSepX = 6, - exSepY = 18, - top = '38,0', - unexSepX = 8, - unexSepY = 16, - up = '39,0'): - return add_node(root,map,MAP+'StackMetrics', - bottom = bottom, - disabled = disabled, - down = down, - exSepX = exSepX, - exSepY = exSepY, - top = top, - unexSepX = unexSepX, - unexSepY = unexSepY, - up = up) - -# -------------------------------------------------------------------- -def add_camera(root,map, - buttonText = '', - canDisable = False, - hotkey = '', - icon = '/images/camera.gif', - propertyGate = '', - tooltip = 'Save map as PNG image'): - return add_node(root,map,MAP+'ImageSaver', - buttonText = buttonText, - canDisable = canDisable, - hotkey = hotkey, - icon = icon, - propertyGate = propertyGate, - tooltip = tooltip) - -# -------------------------------------------------------------------- -def add_forwardtochatter(root,map): - return add_node(root,map,MAP+'ForwardToChatter') - -# -------------------------------------------------------------------- -def add_menudisplayer(root,map): - return add_node(root,map,MAP+'MenuDisplayer') - -# -------------------------------------------------------------------- -def add_mapcenterer(root,map): - return add_node(root,map,MAP+'MapCenterer') - -# -------------------------------------------------------------------- -def add_stackexpander(root,map): - return add_node(root,map,MAP+'StackExpander') - -# -------------------------------------------------------------------- -def add_piecemover(root,map): - return add_node(root,map,MAP+'PieceMover') - -# -------------------------------------------------------------------- -def add_selectionhighlighters(root,map): - return add_node(root,map,MAP+'SelectionHighlighters') - -# -------------------------------------------------------------------- -def add_keybufferer(root,map): - return add_node(root,map,MAP+'KeyBufferer') - -# -------------------------------------------------------------------- -def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2): - return add_node(root,map,MAP+'HighlightLastMoved', - color=color,enabled=enabled,thickness=thickness) - -# -------------------------------------------------------------------- -def add_counterviewer(root,map, - borderWidth = 0, - centerAll = False, - centerText = False, - combineCounterSummary = False, - counterReportFormat = '', - delay = 700, - description = '', - display = 'from top-most layer only', - emptyHexReportForma = '$LocationName$', - enableHTML = True, - extraTextPadding = 0, - fgColor = rgb(0,0,0), - fontSize = 9, - graphicsZoom = 1.0, - hotkey = key('\n',0), - layerList = '', - minDisplayPieces = 2, - propertyFilter = '', - showDeck = False, - showDeckDepth = 1, - showDeckMasked = False, - showMoveSelectde = False, - showNoStack = False, - showNonMovable = False, - showOverlap = False, - showgraph = True, - showgraphsingle = False, - showtext = False, - showtextsingle = False, - stretchWidthSummary = False, - summaryReportFormat = '$LocationName$', - unrotatePieces = False, - version = 3, - verticalOffset = 0, - verticalTopText = 5, - zoomlevel = 1.0): - return add_node(root,map,MAP+'CounterDetailViewer', - borderWidth = borderWidth, - centerAll = centerAll, - centerText = centerText, - combineCounterSummary = combineCounterSummary, - counterReportFormat = counterReportFormat, - delay = delay, - description = description, - display = display, - emptyHexReportForma = emptyHexReportForma, - enableHTML = enableHTML, - extraTextPadding = extraTextPadding, - fgColor = fgColor, - fontSize = fontSize, - graphicsZoom = graphicsZoom, - hotkey = hotkey, - layerList = layerList, - minDisplayPieces = minDisplayPieces, - propertyFilter = propertyFilter, - showDeck = showDeck, - showDeckDepth = showDeckDepth, - showDeckMasked = showDeckMasked, - showMoveSelectde = showMoveSelectde, - showNoStack = showNoStack, - showNonMovable = showNonMovable, - showOverlap = showOverlap, - showgraph = showgraph, - showgraphsingle = showgraphsingle, - showtext = showtext, - showtextsingle = showtextsingle, - stretchWidthSummary = stretchWidthSummary, - summaryReportFormat = summaryReportFormat, - unrotatePieces = unrotatePieces, - version = version, - verticalOffset = verticalOffset, - verticalTopText = verticalTopText, - zoomlevel = zoomlevel) - -# -------------------------------------------------------------------- -def add_globalmap(root,map, - buttonText = '', - color = rgb(255,0,0), - hotkey = key('O',CTRL_SHIFT), - icon = '/images/overview.gif', - scale = 0.2, - tooltip = 'Show/Hide overview window'): - return add_node(root,map,MAP+'GlobalMap', - buttonText = buttonText, - color = color, - hotkey = hotkey, - icon = icon, - scale = scale, - tooltip = 'Show/Hide overview window') - -# -------------------------------------------------------------------- -def add_zoomer(root,map, - inButtonText = '', - inIconName = '/images/zoomIn.gif', - inTooltip = 'Zoom in', - outButtonText = '', - outIconName = '/images/zoomOut.gif', - outTooltip = 'Zoom out', - pickButtonText = '', - pickIconName = '/images/zoom.png', - pickTooltip = 'Select Zoom', - zoomInKey = key('=',CTRL_SHIFT), - zoomLevels = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6], - zoomOutKey = key('-'), - zoomPickKey = key('='), - zoomStart = 3): - '''Zoom start is counting from the back (with default zoom levels, - and zoom start, the default zoom is 1''' - lvls = ','.join([str(z) for z in zoomLevels]) - return add_node(root,map,MAP+'Zoomer', - inButtonText = inButtonText, - inIconName = inIconName, - inTooltip = inTooltip, - outButtonText = outButtonText, - outIconName = outIconName, - outTooltip = outTooltip, - pickButtonText = pickButtonText, - pickIconName = pickIconName, - pickTooltip = pickTooltip, - zoomInKey = zoomInKey, - zoomLevels = lvls, - zoomOutKey = zoomOutKey, - zoomPickKey = zoomPickKey, - zoomStart = zoomStart) - -# -------------------------------------------------------------------- -def add_hidepieces(root,map, - buttonText = '', - hiddenIcon = '/images/globe_selected.gif', - hotkey = key('O'), - showingIcon = '/images/globe_unselected.gif', - tooltip = 'Hide all pieces on this map'): - return add_node(root,map,MAP+'HidePiecesButton', - buttonText = buttonText, - hiddenIcon = hiddenIcon, - hotkey = hotkey, - showingIcon = showingIcon, - tooltip = tooltip) -# -------------------------------------------------------------------- -def add_masskey(root,map, - name, - buttonHotkey, - hotkey, - buttonText = '', - canDisable = False, - deckCount = '-1', - filter = '', - propertyGate = '', - reportFormat = '', - reportSingle = False, - singleMap = True, - target = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS', - tooltip = '', - icon = ''): - '''Default targets are selected units''' - return add_node(root,map,MAP+'MassKeyCommand', - name = name, - buttonHotkey = buttonHotkey, # This hot key - hotkey = hotkey, # Target hot key - buttonText = buttonText, - canDisable = canDisable, - deckCount = deckCount, - filter = filter, - propertyGate = propertyGate, - reportFormat = reportFormat, - reportSingle = reportSingle, - singleMap = singleMap, - target = target, - tooltip = tooltip, - icon = icon) - - -# -------------------------------------------------------------------- -def add_flare(root,map, - circleColor = rgb(255,0,0), - circleScale = True, - circleSize = 100, - flareKey = 'keyAlt', - flareName = 'Map Flare', - flarePulses = 6, - flarePulsesPerSec = 3, - reportFormat = ''): - return add_node(root,map,MAP+'Flare', - circleColor = circleColor, - circleScale = circleScale, - circleSize = circleSize, - flareKey = flareKey, - flareName = flareName, - flarePulses = flarePulses, - flarePulsesPerSec = flarePulsesPerSec, - reportFormat = '') - -# -------------------------------------------------------------------- -def add_atstart(root,map,name,*pieces, - location = '', - useGridLocation = True, - owningBoard = '', - x = 0, - y = 0, - gpid = None): - '''Pieces are existing PieceSlot elements''' - a = add_node(root,map,MODULE+'map.SetupStack', - name = name, - location = location, - owningBoard = owningBoard, - useGridLocation = useGridLocation, - x = x, - y = y) - # copy pieces here - for p in pieces: - c = p.cloneNode(True) - a.appendChild(c) - if gpid is not None: - # Update gpid in element - c.setAttribute('gpid',str(gpid)) - # Update gpid in traits state - traits = get_piece_parts(p.childNodes[0].nodeValue) - for t in traits: - code = t['def'] - stat = t['state'] - if code[0] == 'piece': - stat[3] = gpid - # Put back the traits - c.childNodes[0].nodeValue = dicts_to_piece(traits) - - gpid += 1 - - return a, gpid - -# -------------------------------------------------------------------- -def add_pieceslot(root,elem, - entryName, - gpid, - code, - height=72, - width=72, - icon = ''): - p = add_node(root,elem,WIDGET+'PieceSlot', - entryName = entryName, - gpid = gpid, - height = height, - width = width, - icon = icon) - add_text(root,p,code) - return p - -# -------------------------------------------------------------------- -def add_prototypes(root,elem,*defs): - '''Defs are existing definition elements''' - p = add_node(root,elem,MODULE+'PrototypesContainer') - - for d in defs: - c = d.cloneNode(True) - p.appendChild(c) - - return p - -# -------------------------------------------------------------------- -def add_prototype(root,elem,name,code,description=''): - d = add_node(root,elem,MODULE+'PrototypeDefinition', - name = name, - description = description) - add_text(root,d,code) - return d - -# -------------------------------------------------------------------- -def add_piecewindow(root,elem,name, - defaultWidth = 0, - hidden = False, - hotkey = key('C',ALT), - scale = 1., - text = '', - tooltip = 'Show/hide piece window', - icon = '/images/counter.gif'): - return add_node(root,elem,MODULE+'PieceWindow', - name = name, - defaultWidth = defaultWidth, - hidden = hidden, - hotkey = hotkey, - scale = scale, - text = text, - tooltip = tooltip, - icon = icon) - -# -------------------------------------------------------------------- -def add_tabs(root,elem,entryName): - return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName) - -# -------------------------------------------------------------------- -def add_list(root,elem, - entryName, - height = 0, - width = 0, - scale = 1., - divider = 100): - return add_node(root,elem,WIDGET+'ListWidget', - entryName = entryName, - height = height, - width = width, - scale = scale, - divider = divider) - -# -------------------------------------------------------------------- -def add_panel(root,elem, - entryName, - fixed = False, - nColumns = 1, - vert = False): - return add_node(root,elem,WIDGET+'PanelWidget', - entryName = entryName, - fixed = fixed, - nColumns = nColumns, - vert = vert) - -# -------------------------------------------------------------------- -def add_mapwidget(root,elem,entryName=''): - return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName) - -# -------------------------------------------------------------------- -def add_chartwindow(root,elem, - name, - hotkey = key('A',ALT), - description = '', - text = '', - tooltip = 'Show/hide Charts', - icon = '/images/chart.gif'): - return add_node(root,elem,MODULE+'ChartWindow', - name = name, - hotkey = hotkey, - description = description, - text = text, - tooltip = tooltip, - icon = icon) - -# -------------------------------------------------------------------- -def add_chart(root,elem, - chartName, - fileName, - description = ''): - return add_node(root,elem,WIDGET+'Chart', - chartName = chartName, - description = description, - fileName = fileName) - -# -------------------------------------------------------------------- -def add_dice(root,elem, - addToTotal = 0, - canDisable = False, - hotkey = key('6',ALT), - icon = '/images/die.gif', - keepCount = 1, - keepDice = False, - keepOption = '>', - lockAdd = False, - lockDice = False, - lockPlus = False, - lockSides = False, - nDice = 1, - nSides = 6, - name = '1d6', - plus = 0, - prompt = False, - propertyGate = '', - reportFormat = '** $name$ = $result$ *** &lt;$PlayerName$&gt;', - reportTotal = False, - sortDice = False, - text = '1d6', - tooltip = 'Roll a 1d6'): - return add_node(root,elem,MODULE+'DiceButton', - addToTotal = addToTotal, - canDisable = canDisable, - hotkey = hotkey, - icon = icon, - keepCount = keepCount, - keepDice = keepDice, - keepOption = keepOption, - lockAdd = lockAdd, - lockDice = lockDice, - lockPlus = lockPlus, - lockSides = lockSides, - nDice = nDice, - nSides = nSides, - name = name, - plus = plus, - prompt = prompt, - propertyGate = propertyGate, - reportFormat = reportFormat, - reportTotal = reportTotal, - sortDice = sortDice, - text = text, - tooltip = tooltip) - -# ==================================================================== -def enc_def(args,term=False): - '''Encode a piece (decorator) definition +class CalculatedTrait(Trait): + ID = 'calcProp' + def __init__(self,name='',expression='',description=''): + '''Define a trait that calculates a property''' + super(CalculatedTrait,self).__init__() + self.setType(name = name, + expression = expression, + description = description) + self.setState() - Parameters - ---------- - args : list - List of fields - term : bool - If true, add terminating ';' - Returns - ------- - code : str - The encoded definition - ''' - return ';'.join([str(e).lower() if isinstance(e,bool) else str(e) - for e in args])+(';' if term else '') - -# -------------------------------------------------------------------- -def layer_trait(images, - newNames = None, - activateName = 'Activate', - activateMask = CTRL, - activateChar = 'A', - increaseName = 'Increase', - increaseMask = CTRL, - increaseChar = '[', - decreaseName = '', - decreaseMask = CTRL, - decreaseChar = ']', - resetName = '', - resetKey = '', - resetLevel = 1, - under = False, - underXoff = 0, - underYoff = 0, - loop = True, - name = '', - description = '', - randomKey = '', - randomName = '', - follow = False, - expression = '', - first = 1, - version = 1, # 1:new, 0:old - always = 'true', - activateKey = key('A'), - increaseKey = key('['), - decreaseKey = key(']'), - scale = 1.): - '''Create a layer trait (VASSAL.counter.Embellishment) +Trait.known_traits.append(CalculatedTrait) - Parameters - ---------- - main : dict - Main image dictionary - flipped : dict - Flipped image dictionary +# +# EOF +# +# ==================================================================== +# From traits/restrictcommand.py + +# -------------------------------------------------------------------- +class RestrictCommandsTrait(Trait): + ID = 'hideCmd' + HIDE = 'Hide' + DISABLE = 'Disable' + def __init__(self, + name = '', + hideOrDisable = HIDE, + expression = '',# Restrict when true + keys = []): + '''Create a layer trait (VASSAL.counter.RestrictCommands)''' + super(RestrictCommandsTrait,self).__init__() + encKeys = ','.join([k.replace(',',r'\,') for k in keys]) + self.setType(name = name, + hideOrDisable = hideOrDisable, + expression = expression, + keys = encKeys) + self.setState(state='') + def setKeys(self,keys): + self['keys'] = ','.join([k.replace(',',r'\,') for k in keys]) + - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state - ''' - if newNames is None: - newNames = ['']*len(images) - args = ['emb2', # Code - activateName, activateMask, activateChar, # ACTIVATE ; MASK ; KEY - increaseName, increaseMask, increaseChar, - decreaseName, decreaseMask, decreaseChar, - resetName, resetKey, resetLevel, - under, underXoff, underYoff, - ','.join(images), - ','.join(newNames), - loop, - name, - randomKey, randomName, - follow, expression, - first, - version, - always, - activateKey, - increaseKey, - decreaseKey, - description, - scale ] - stat = '1' # CURRENT LEVEL - return enc_def(args), stat - -# -------------------------------------------------------------------- -def prototype_trait(name): - '''Create a prototype trait (VASSAL.counter.UsePrototype) +Trait.known_traits.append(RestrictCommandsTrait) + +# +# EOF +# +# ==================================================================== +# From traits/label.py + +class LabelTraitCodes: + TOP = 't' + BOTTOM = 'b' + CENTER = 'c' + LEFT = 'l' + RIGHT = 'r' + PLAIN = 0 + BOLD = 1 + ITALIC = 2 + +# -------------------------------------------------------------------- +class LabelTrait(Trait): + ID = 'label' + def __init__(self, + label = None, + labelKey = '', + menuCommand ='Change label', + fontSize = 10, + background = 'none', + foreground = '255,255,255', + vertical = LabelTraitCodes.TOP, + verticalOff = 0, + horizontal = LabelTraitCodes.CENTER, + horizontalOff = 0, + verticalJust = LabelTraitCodes.BOTTOM, + horizontalJust = LabelTraitCodes.CENTER, + nameFormat = '$pieceName$ ($label$)', + fontFamily = 'Dialog', + fontStyle = LabelTraitCodes.PLAIN, + rotate = 0, + propertyName = 'TextLabel', + description = '', + alwaysUseFormat = False): + '''Create a label trait (can be edited property)''' + super(LabelTrait,self).__init__() + self.setType(labelKey = labelKey, + menuCommand = menuCommand, + fontSize = fontSize, + background = background, + foreground = foreground, + vertical = vertical, + verticalOff = verticalOff, + horizontal = horizontal, + horizontalOff = horizontalOff, + verticalJust = verticalJust, + horizontalJust = horizontalJust, + nameFormat = nameFormat, + fontFamily = fontFamily, + fontStyle = fontStyle, + rotate = rotate, + propertyName = propertyName, + description = description, + alwaysUseFormat = alwaysUseFormat) + self.setState(label = (nameFormat if label is None else label)) + + +Trait.known_traits.append(LabelTrait) + +# +# EOF +# +# ==================================================================== +# From traits/layer.py + +# -------------------------------------------------------------------- +class LayerTrait(Trait): + ID = 'emb2' + def __init__(self, + images = [''], + newNames = None, + activateName = 'Activate', + activateMask = CTRL, + activateChar = 'A', + increaseName = 'Increase', + increaseMask = CTRL, + increaseChar = '[', + decreaseName = '', + decreaseMask = CTRL, + decreaseChar = ']', + resetName = '', + resetKey = '', + resetLevel = 1, + under = False, + underXoff = 0, + underYoff = 0, + loop = True, + name = '', + description = '', + randomKey = '', + randomName = '', + follow = False, + expression = '', + first = 1, + version = 1, # 1:new, 0:old + always = True, + activateKey = key('A'), + increaseKey = key('['), + decreaseKey = key(']'), + scale = 1.): + '''Create a layer trait (VASSAL.counter.Embellishment)''' + super(LayerTrait,self).__init__() + if newNames is None and images is not None: + newNames = ['']*len(images) + self.setType( + activateName = activateName, + activateMask = activateMask, + activateChar = activateChar, + increaseName = increaseName, + increaseMask = increaseMask, + increaseChar = increaseChar, + decreaseName = decreaseName, + decreaseMask = decreaseMask, + decreaseChar = decreaseChar, + resetName = resetName, + resetKey = resetKey, + resetLevel = resetLevel, + under = under, + underXoff = underXoff, + underYoff = underYoff, + images = ','.join(images), + newNames = ','.join(newNames), + loop = loop, + name = name, + randomKey = randomKey, + randomName = randomName, + follow = follow, + expression = expression, + first = first, + version = version, + always = always, + activateKey = activateKey, + increaseKey = increaseKey, + decreaseKey = decreaseKey, + description = description, + scale = scale) + self.setState(level=1) + +Trait.known_traits.append(LayerTrait) + +# +# EOF +# +# ==================================================================== +# From traits/globalcommand.py + +# -------------------------------------------------------------------- +class GlobalCommandTrait(Trait): + ID = 'globalkey' + def __init__(self, + commandName = '', + key = '', # Command received + globalKey = '', # Command to send to targets + properties = '', # Filter target on this expression + ranged = False, + range = 1, + reportSingle = True, + fixedRange = True, + rangeProperty = '', + description = '', + deskSelect = '-1', + target = ''): + '''Create a global key command in piece + (VASSAL.counters.CounterGlobalKeyCommand)''' + self.setType(commandName = commandName, + key = key, + globalKey = globalKey, + properties = properties, + ranged = ranged, + range = range, + reportSingle = reportSingle, + fixedRange = fixedRange, + rangeProperty = rangeProperty, + description = description, + deskSelect = deskSelect, + target = target) + self.setState() + +Trait.known_traits.append(GlobalCommandTrait) + +# +# EOF +# +# ==================================================================== +# From traits/globalhotkey.py + +# -------------------------------------------------------------------- +class GlobalHotkeyTrait(Trait): + ID = 'globalhotkey' + def __init__(self, + name = '', # Command received + key = '', # Command key received + globalHotkey = '', # Key to send + description = ''): + '''Create a global key command in piece + (VASSAL.counters.GlobalHotkey)''' + self.setType(name = name, + key = key, + globalHotkey = globalHotkey, + description = description) + self.setState() + +Trait.known_traits.append(GlobalHotkeyTrait) + +# +# EOF +# +# ==================================================================== +# From traits/nostack.py + +# -------------------------------------------------------------------- +class NoStackTrait(Trait): + ID = 'immob' + NORMAL_SELECT = '' + SHIFT_SELECT = 'i' + CTRL_SELECT = 't' + ALT_SELECT = 'c' + NEVER_SELECT = 'n' + NORMAL_BAND_SELECT = '' + ALT_BAND_SELECT = 'A' + ALT_SHIFT_BAND_SELECT = 'B' + NEVER_BAND_SELECT = 'Z' + NORMAL_MOVE = 'N' + SELECT_MOVE = 'I' + NEVER_MOVE = 'V' + NORMAL_STACK = 'L' + NEVER_STACK = 'R' + IGNORE_GRID = 'g' + def __init__(self, + select = NORMAL_SELECT, + bandSelect = NORMAL_BAND_SELECT, + move = NORMAL_MOVE, + canStack = False, + ignoreGrid = False, + description = ''): + + selectionOptions = (select + + (self.IGNORE_GRID if ignoreGrid else '') + + bandSelect) + movementOptions = move + stackingOptions = self.NORMAL_STACK if canStack else self.NEVER_STACK + + '''Create a mark trait (static property)''' + super(NoStackTrait,self).__init__() + + self.setType(selectionOptions = selectionOptions, + movementOptions = movementOptions, + stackingOptions = stackingOptions, + description = description) + self.setState() + + +Trait.known_traits.append(NoStackTrait) + +# +# EOF +# +# ==================================================================== +# From traits/deselect.py + +# -------------------------------------------------------------------- +class DeselectTrait(Trait): + ID = 'deselect' + THIS = 'D' # Deselect only this piece + ALL = 'A' # Deselect all pieces + ONLY = 'S' # Select this piece only + def __init__(self, + command = '', + key = '', + description = '', + unstack = False, + deselect = THIS): + '''Create a deselect trait''' + super(DeselectTrait,self).__init__() + self.setType(command = command, + key = key, + description = description, + unstack = unstack, + deselect = deselect) + self.setState() + + +Trait.known_traits.append(DeselectTrait) + +# +# EOF +# +# ==================================================================== +# From traits/restrictaccess.py + +# -------------------------------------------------------------------- +class RestrictAccessTrait(Trait): + ID = 'restrict' + def __init__(self, + sides = [], + byPlayer = False, + noMovement = True, + description = '', + owner = '',): + '''Create a layer trait (VASSAL.counter.Restricted)''' + super(RestrictAccessTrait,self).__init__() + encSides = ','.join(sides) + self.setType(sides = encSides, + byPlayer = byPlayer, + noMovement = noMovement, + description = description) + self.setState(owner=owner) + +Trait.known_traits.append(RestrictAccessTrait) + +# +# EOF +# +# ==================================================================== +# From traits/rotate.py + +# -------------------------------------------------------------------- +class RotateTrait(Trait): + ID = 'rotate' + def __init__(self, + nangles = 6, + rotateCWKey = key(']'), + rotateCCWKey = key('['), + rotateCW = 'Rotate CW', + rotateCCW = 'Rotate CCW', + rotateRndKey = '', + rotateRnd = '', + name = 'Rotate', + description = 'Rotate piece', + rotateDirectKey = '', + rotateDirect = '', + directExpression = '', + directIsFacing = True, + angle = 0): + '''Create a Rotate trait''' + super(RotateTrait,self).__init__() + if nangles == 1: + self.setType(nangles = nangles, + rotateKey = rotateCWKey, + rotate = rotateCW, + rotateRndKey = rotateRndKey, + rotateRnd = rotateRnd, + name = name, + description = description, + rotateDirectKey = rotateDirectKey, + rotateDirect = rotateDirect, + directExpression = directExpression, + directIsFacing = directIsFacing) + else: + self.setType(nangles = nangles, + rotateCWKey = rotateCWKey, + rotateCCWKey = rotateCCWKey, + rotateCW = rotateCW, + rotateCCW = rotateCCW, + rotateRndKey = rotateRndKey, + rotateRnd = rotateRnd, + name = name, + description = description, + rotateDirectKey = rotateDirectKey, + rotateDirect = rotateDirect, + directExpression = directExpression, + directIsFacing = directIsFacing) + + self.setState(angle = int(angle) if nangles > 1 else float(angle)) + +Trait.known_traits.append(RotateTrait) + +# +# EOF +# +# ==================================================================== +# From traits/stack.py + +# -------------------------------------------------------------------- +class StackTrait(Trait): + ID = 'stack' + def __init__(self, + board = '', + x = '', + y = '', + pieceIds = [], + layer = -1): + '''Create a stack trait in a save file''' + self.setType() # NAME + # print('Piece IDs:',pieceIds) + self.setState(board = board, + x = x, + y = y, + pieceIds = ';'.join([str(p) for p in pieceIds]), + layer = f'@@{layer}') + +Trait.known_traits.append(StackTrait) + +# +# EOF +# +# ==================================================================== +# From traits/mark.py + +# -------------------------------------------------------------------- +class MarkTrait(Trait): + ID = 'mark' + def __init__(self,name='',value=''): + '''Create a mark trait (static property)''' + super(MarkTrait,self).__init__() + self.setType(name = name) + self.setState(value = value) + + +Trait.known_traits.append(MarkTrait) + +# +# EOF +# +# ==================================================================== +# From traits/mask.py + +# -------------------------------------------------------------------- +# Inset +# obs;88,130;ag_hide_1.png;Reveal;I;?;sides:Argentine;Peek;;true;; +# obs;88,130;ag_hide_1.png;Reveal;I;?;side:Argentine;;;true;; +# +# Peek +# obs;88,130;ag_hide_1.png;Reveal;P89,130;?;sides:Argentine;Peek;;true;; +# +# Image +# +class MaskTrait(Trait): + ID = 'obs' + INSET = 'I' + BACKGROUND = 'B' + PEEK = 'P' + IMAGE = 'G' + INSET2 = '2' + PLAYER = 'player:' + SIDE = 'side:' + SIDES = 'sides:' + def __init__(self, + keyCommand = '', + imageName = '', + hideCommand = '', + displayStyle = '', + peekKey = '', + ownerImage = '', + maskedName = '?', + access = '',#? + peekCommand = '', + description = '', + autoPeek = True, + dealKey = '', + dealExpr = ''): + '''Create a mark trait (static property)''' + super(MaskTrait,self).__init__() + disp = displayStyle + if displayStyle == self.PEEK: + disp += peekKey + elif displayStyle == self.IMAGE: + disp += ownerImage + + acc = self.PLAYER + if isinstance(access,list): + acc = self.SIDES + ':'.join(access) + elif access.startswith('player'): + acc = self.PLAYER + elif access.startswith('side'): + acc = self.SIDE + + self.setType(keyCommand = keyCommand, + imageImage = imageName, + hideCommand = hideCommand, + displayStyle = disp, + maskedName = maskedName, + access = acc, # ? + peekCommand = peekCommand, + description = description, + autoPeek = autoPeek, + dealKey = dealKey, + dealExpr = dealExpr) + self.setState(value='null') + + @classmethod + def peekDisplay(cls,key):#Encoded key + return cls.PEEK + key + + @classmethod + def peekImage(cls,ownerImage): + return cls.IMAGE + ownerImage + + @classmethod + def sides(cls,*names): + return cls.SIDES+':'.join(names) - Parameters - ---------- - family : str - Side in game - - Return - ------ - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['prototype', name ] - stat = '' # State is nothing - return enc_def(args), stat +Trait.known_traits.append(MaskTrait) -# -------------------------------------------------------------------- -def mark_trait(key,value): - '''Create a marker trait (VASSAL.counter.Marker) +# +# EOF +# +# ==================================================================== +# From traits/trail.py + +# -------------------------------------------------------------------- +class TrailTrait(Trait): + ID = 'footprint' + def __init__(self, + key = key('T'), + name = 'Movement Trail', + localVisible = False, + globalVisible = False, + radius = 10, + fillColor = rgb(255,255,255), + lineColor = rgb(0,0,0), + activeOpacity = 100, + inactiveOpacity = 50, + edgesBuffer = 20, + displayBuffer = 30, + lineWidth = 1, + turnOn = '', + turnOff = '', + reset = '', + description = ''): + ''' Create a movement trail trait ( VASSAL.counters.Footprint)''' + super(TrailTrait,self).__init__() + self.setType(key = key,# ENABLE KEY + name = name,# MENU + localVisible = localVisible,# LOCAL VISABLE + globalVisible = globalVisible,# GLOBAL VISABLE + radius = radius,# RADIUS + fillColor = fillColor,# FILL COLOR + lineColor = lineColor,# LINE COLOR + activeOpacity = activeOpacity,# ACTIVE OPACITY + inactiveOpacity = inactiveOpacity,# INACTIVE OPACITY + edgesBuffer = edgesBuffer,# EDGES BUFFER + displayBuffer = displayBuffer,# DISPLAY BUFFER + lineWidth = int(lineWidth),# LINE WIDTH + turnOn = turnOn,# TURN ON KEY + turnOff = turnOff,# TURN OFF KEY + reset = reset,# RESET KEY + description = description) # DESC + self.setState(isGlobal = False, + map = '', + points = 0, # POINTS (followed by [; [X,Y]*] + init = False) + +Trait.known_traits.append(TrailTrait) - Parameters - ---------- - family : str - Side in game - - Return - ------ - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['mark', key ] - stat = value # State is nothing - return enc_def(args), stat +# +# EOF +# +# ==================================================================== +# From traits/delete.py # -------------------------------------------------------------------- -def report_trait(*keys, - nosuppress = True, - description = '', - report = '$location$: $newPieceName$ $menuCommand$ *', - cyclekeys = '', - cyclereps = ''): - '''Create a report trait (VASSAL.counters.ReportActon) - - Parameters - ---------- - *keys : tuple of str - The keys to report actions for - - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - esckeys = ','.join([k.replace(',',r'\,') for k in keys]) - esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys]) - escreps = ','.join([k.replace(',',r'\,') for k in cyclereps]) - args = ['report', # CODE - esckeys, # ESCAPED KEYS (commas escaped) - report, # REPORT - esccycl, # CYCLE KEYS (commas escaped) - escreps, # CYCKE REPORTS (commas escaped) - description, # DESCRIPTION - nosuppress] # NOSUPPRESS - stat = '-1' # CYCLE STATE - return enc_def(args), stat - -# -------------------------------------------------------------------- -def moved_trait(image='moved.gif', - xoff = 36, - yoff = -38, - name = 'Mark Moved', - key = key('M')): - '''Create a moved trait (VASSAL.counters.MovementMarkable) +class DeleteTrait(Trait): + ID = 'delete' + def __init__(self, + name = 'Delete', + key = key('D')): + '''Create a delete trait (VASSAL.counters.Delete)''' + super(DeleteTrait,self).__init__() + self.setType(name = name, + key = key, + dummy = '') + self.setState() - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['markmoved', # CODE - image, # IMAGE - xoff, yoff, # XOFF ; YOFF - name, # MENU - key, # KEY, MODIFIER - ''] # ? - stat = False # MOVED? - return enc_def(args), stat - -# -------------------------------------------------------------------- -def trail_trait(key = key('T'), - name = 'Movement Trail', - localVisible = False, - globalVisible = False, - radius = 10, - fillColor = rgb(255,255,255), - lineColor = rgb(0,0,0), - activeOpacity = 100, - inactiveOpacity = 50, - edgesBuffer = 20, - displayBuffer = 30, - lineWidth = 1, - turnOn = '', - turnOff = '', - reset = '', - description = ''): - ''' Create a movement trail trait ( VASSAL.counters.Footprint) - - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state - ''' - args = ['footprint', # CODE - key, # ENABLE KEY - name, # MENU - localVisible, # LOCAL VISABLE - globalVisible, # GLOBAL VISABLE - radius, # RADIUS - fillColor, # FILL COLOR - lineColor, # LINE COLOR - activeOpacity, # ACTIVE OPACITY - inactiveOpacity, # INACTIVE OPACITY - edgesBuffer, # EDGES BUFFER - displayBuffer, # DISPLAY BUFFER - lineWidth, # LINE WIDTH - turnOn, # TURN ON KEY - turnOff, # TURN OFF KEY - reset, # RESET KEY - description] # DESC - stat = [False, # GLOBAL VIS - '', # MAP - 0] # POINTS (followed by [; [X,Y]*] - return enc_def(args), enc_def(stat) - -# -------------------------------------------------------------------- -def delete_trait(name = 'Delete', key = key('D')): - '''Create a delete trait (VASSAL.counters.Delete) - - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['delete', name, key, ''] - stat = '' # Nothing - return enc_def(args), stat +Trait.known_traits.append(DeleteTrait) -# -------------------------------------------------------------------- -def sendto_trait(mapName, - boardName, +# +# EOF +# +# ==================================================================== +# From traits/sendto.py + +# -------------------------------------------------------------------- +class SendtoTrait(Trait): + ID = 'sendto' + LOCATION = 'L' + ZONE = 'Z' + REGION = 'R' + GRID = 'G' + def __init__(self, + mapName = '', + boardName = '', name = '', key = key('E'), restoreName = 'Restore', @@ -1891,1771 +6198,5318 @@ def sendto_trait(mapName, xoff = 1, yoff = 1, description = '', - destination = 'L', + destination = LOCATION, zone = '', region = '', expression = '', position = ''): - '''Create a send to trait (VASSAL.counter.SendToLocation) + '''Create a send to trait (VASSAL.counter.SendToLocation)''' + self.setType(name = name,# NAME + key = key,# KEY , MODIFIER + mapName = mapName,# MAP + boardName = boardName,# BOARD + x = x, + y = y,# X ; Y + restoreName = restoreName,# BACK + restoreKey = restoreKey,# KEY , MODIFIER + xidx = xidx, + yidx = yidx,# XIDX ; YIDX + xoff = xoff, + yoff = yoff,# XOFF ; YOFF + description = description,# DESC + destination = destination,# DEST type + zone = zone,# ZONE + region = region,# REGION + expression = expression,# EXPRESSION + position = position) # GRIDPOS + self.setState(backMap = '', backX = '', backY = '') + +Trait.known_traits.append(SendtoTrait) - Parameters - ---------- - side : str - Sub-map to move to - - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['sendto', # CODE - name, # NAME - key, # KEY , MODIFIER - mapName, # MAP - boardName, # BOARD - x, y, # X ; Y - restoreName, # BACK - restoreKey, # KEY , MODIFIER - xidx, yidx, # XIDX ; YIDX - xoff, yoff, # XOFF ; YOFF - description, # DESC - destination, # DEST - zone, # ZONE - region, # REGION - expression, # EXPRESSION - position ] # GRIDPOS - stat = ['', # BACKMAP - '', # X - ''] # Y - return enc_def(args), enc_def(stat) - -# -------------------------------------------------------------------- -def basic_trait(name, - filename, # Can be empty - gpid, # Can be empty - cloneKey='', # Deprecated - deleteKey=''):# Deprecated - '''Create a basic unit (VASSAL.counters.BasicPiece) +# +# EOF +# +# ==================================================================== +# From traits/moved.py + +# -------------------------------------------------------------------- +class MovedTrait(Trait): + ID = 'markmoved' + def __init__(self, + image = 'moved.gif', + xoff = 36, + yoff = -38, + name = 'Mark moved', + key = key('M'), + dummy = '' # Description + # ignoreSame = True + ): + '''Create a moved trait (VASSAL.counters.MovementMarkable)''' + super(MovedTrait,self).__init__() + self.setType(image = image, + xoff = xoff, + yoff = yoff, + name = name, + key = key, + dummy = dummy, # Description + # ignoreSame = ignoreSame + ) + self.setState(moved = False) + +Trait.known_traits.append(MovedTrait) - Parameters - ---------- - main : dict - Information - gpid : int - Piece global identifier - image : bool - If true, add image +# +# EOF +# +# ==================================================================== +# From traits/skel.py - Returns - ------- - code : str - Encoded trait - stat : str - Encoded state (empty string) - ''' - args = ['piece', # CODE - cloneKey, # CLONEKEY - deleteKey, # DELETEKEY - filename, # IMAGE - name] # NAME - stat = ['null', # MAPID (possibly 'null') - 0, # X - 0, # Y - gpid, # GLOBAL PIECE ID - 0] # PROPERTY COUNT (followed by [; KEY; VALUE]+) - return enc_def(args), enc_def(stat) - -# -------------------------------------------------------------------- -def enc_parts(*parts): - '''Encode parts of a full piece definition - - Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece) - definition or state is separated by a litteral TAB character. - Beyond the first TAB separator, additional escape characters - (BACKSLAH) are added in front of the separator. This is to that - VASSAL.utils.SequenceDecoder does not see consequitive TABs as a - single TAB. - Parameters - ---------- - parts : tuple - The various trait definitions or states +# +# EOF +# +# ==================================================================== +# From traits/submenu.py + +# -------------------------------------------------------------------- +class SubMenuTrait(Trait): + ID = 'submenu' + def __init__(self, + subMenu = '', # Title + keys = [], # Keys + description = ''): + '''Create a sub menu (VASSAL.counters.SubMenu)''' + self.setType(subMenu = subMenu, # CLONEKEY + keys = ','.join([k.replace(',',r'\,') + for k in keys]), + description = description) + self.setState() # PROPERTY COUNT (followed by [; KEY; VALUE]+) + def setKeys(self,keys): + '''Set the keys''' + self['keys'] = ','.join([k.replace(',',r'\,') for k in keys]) + +Trait.known_traits.append(SubMenuTrait) - Returns - ------- - code : str - The encoded piece - ''' - ret = '' - sep = r' ' - for i, p in enumerate(parts): - if i != 0: - ret += '\\'*(i-1) + sep - ret += str(p) +# +# EOF +# +# ==================================================================== +# From traits/basic.py + +# -------------------------------------------------------------------- +class BasicTrait(Trait): + ID = 'piece' + def __init__(self, + name = '', + filename = '', # Can be empty + gpid = '', # Can be empty + cloneKey = '', # Deprecated + deleteKey = ''): # Deprecated + '''Create a basic unit (VASSAL.counters.BasicPiece)''' + self.setType(cloneKey = cloneKey, # CLONEKEY + deleteKey = deleteKey, # DELETEKEY + filename = filename, # IMAGE + name = name) # NAME + self.setState(map = 'null', # MAPID (possibly 'null') + x = 0, + y = 0, + gpid = gpid, + properties = 0) # PROPERTY COUNT (followed by [; KEY; VALUE]+) + +Trait.known_traits.append(BasicTrait) - return ret +# +# EOF +# +# ==================================================================== +# From traits/trigger.py + +# -------------------------------------------------------------------- +class TriggerTrait(Trait): + ID = 'macro' + WHILE = 'while' + UNTIL = 'until' + COUNTED = 'counted' # - Always one "do ... while" + def __init__(self, + name = '', + command = '', # Context menu name + key = '', # Context menu key + property = '', # Enable/Disable + watchKeys = [], + actionKeys = [], # What to do + loop = False, + preLoop = '', # Key + postLoop = '', # Key + loopType = COUNTED, # Loop type + whileExpression = '', + untilExpression = '', + count = 0, + index = False, + indexProperty = '', + indexStart = '', + indexStep = ''): + '''Create a layer trait (VASSAL.counter.Trigger)''' + super(TriggerTrait,self).__init__() + encWKeys = Trait.encodeKeys(watchKeys, ',') + encAKeys = Trait.encodeKeys(actionKeys,',') + self.setType(name = name, + command = command, # Context menu name + key = key, # Context menu key + property = property, # Enable/Disable + watchKeys = encWKeys, + actionKeys = encAKeys, # What to do + loop = loop, + preLoop = preLoop, # Key + postLoop = postLoop, # Key + loopType = loopType, # Loop type + whileExpression = whileExpression, + untilExpression = untilExpression, + count = count, + index = index, + indexProperty = indexProperty, + indexStart = indexStart, + indexStep = indexStep) + self.setState(state='') + + def getActionKeys(self): + return Trait.decodeKeys(self['actionKeys'],',') + + def getWatchKeys(self): + return Trait.decodeKeys(self['watchKeys'],',') + + def setActionKeys(self,keys): + self['actionKeys'] = Trait.encodeKeys(keys,',') + + def setWatchKeys(self,keys): + self['watchKeys'] = Trait.encodeKeys(keys,',') + + -# -------------------------------------------------------------------- -def add_piece(typ, state): - '''Create and add piece command (VASSAL.command.AddPiece) +Trait.known_traits.append(TriggerTrait) - Each part is separated by a SLASH ('/'). The first part is the - command type (PLUS '+') to perform. Second is the IDENTIFIER, - possibly 'null'. Then comes the fully encoded type of the piece, - followed by the state of the piece (also fully encoded). +# +# EOF +# +# ==================================================================== +# From traits/nonrect.py + +# -------------------------------------------------------------------- +class NonRectangleTrait(Trait): + ID = 'nonRect2' + CLOSE = 'c' + MOVETO = 'm' + LINETO = 'l' + CUBICTO = 'l' + QUADTO = 'l' + def __init__(self, + scale = 1., + filename = '', + path = [], + image = None): + '''Create a NonRectangle trait (static property)''' + super(NonRectangleTrait,self).__init__() + l = [] + if len(filename) > 0: + l.append(f'n{filename}') + + if len(path) <= 0: + path = self.getShape(image) + + if len(path) > 0: + # print(path) + l += [f'{p[0]},{int(p[1])},{int(p[2])}' if len(p) > 2 else p + for p in path] + + self.setType(scale = scale, + code = ','.join(l)) + self.setState() + + @classmethod + def getShape(cls,image): + if image is None: + return [] + + from PIL import Image + from io import BytesIO + + code = [] + with Image.open(BytesIO(image)) as img: + alp = img.getchannel('A') # Alpha channel + # Find least and largest non-transparent pixel in each row + rows = [] + w = alp.width + h = alp.height + bb = alp.getbbox() + for r in range(bb[1],bb[3]): + ll, rr = bb[2], bb[0] + for c in range(bb[0],bb[2]): + if alp.getpixel((c,r)) != 0: + ll = min(c,ll) + rr = max(c,rr) + rows += [[r-h//2,ll-w//2,rr-w//2]] + + # Now produce the code - we start with the top line + code = [(cls.MOVETO,rows[0][1],rows[0][0]-1), + (cls.LINETO,rows[0][2],rows[0][0]-1)] + + # Now loop down right side of image + for c in rows: + last = code[-1] + if last[1] != c[2]: + code += [(cls.LINETO, c[2], last[2])] + code += [(cls.LINETO, c[2], c[0])] + + # Now loop up left side of image + for c in rows[::-1]: + last = code[-1] + if last[1] != c[1]: + code += [(cls.LINETO,c[1],last[2])] + code += [(cls.LINETO,c[1],c[0])] - Parameters - ---------- - typ : str - Encoded piece definition - state : str - Encoded state definition + # Terminate with close + code += [(cls.CLOSE)] - Returns - ------- - cmd : str - The add command encoded - ''' - args = ['+', # Type - 'null', # IDENTIFER - typ, # DEFINITION - state] # STATE - return '/'.join(args) + return code -# -------------------------------------------------------------------- -def add_proto(typ,state): - '''Create an add prototype command. This is the same as 'add_piece' - Parameters - ---------- - typ : str - Encoded piece definition - state : str - Encoded state definition +Trait.known_traits.append(NonRectangleTrait) - Returns - ------- - cmd : str - The add command encoded - ''' - return add_piece(typ,state) - +# +# EOF +# +# ==================================================================== +# From traits/click.py # -------------------------------------------------------------------- -def add_proto_code(decs): - typs = [d[0] for d in decs] - stts = [d[1] for d in decs] - typ = enc_parts(*typs) - stt = enc_parts(*stts) - - return add_proto(typ,stt) +class ClickTrait(Trait): + ID = 'button' + def __init__(self, + key = '', + x = 0, + y = 0, + width = 0, + height = 0, + description = '', + context = False, + whole = True, + version = 1, + points = []): + '''Create a click trait (static property)''' + super(ClickTrait,self).__init__() + self.setType(key = key, + x = x, + y = y, + width = width, + height = height, + description = description, + context = context, + whole = whole, + version = version, + npoints = len(points), + points = ';'.join([f'{p[0]};{p[1]}' + for p in points])) + self.setState() + + +Trait.known_traits.append(ClickTrait) -# ==================================================================== # -# Below specific to the export app +# EOF # # ==================================================================== -def get_bb(buffer): - '''Get bounding box of image - - Parameters - ---------- - buffer : bytes - The image bytes - - Returns - ------- - ulx, uly, lrx, lry : tuple - The coordinates of the bounding box - ''' - from PIL import Image - from io import BytesIO - - with Image.open(BytesIO(buffer)) as img: - bb = img.getbbox() - - return bb +# From game.py + +# -------------------------------------------------------------------- +class Game(Element): + TAG = Element.BUILD+'GameModule' + def __init__(self,build,node=None, + name = '', + version = '', + ModuleOther1 = "", + ModuleOther2 = "", + VassalVersion = "3.6.7", + description = "", + nextPieceSlotId = 20): + '''Create a new Game object + + Parameters + ---------- + build : xml.dom.Document + root note + node : xml.dom.Node + To read from, or None + name : str + Name of module + version : str + Version of module + ModuleOther1 : str + Free form string + ModuleOther2 : str + Free form string + VassalVersion : str + VASSAL version this was created for + description : str + Speaks volumes + nextPieceSlotId : int + Starting slot ID. + ''' + super(Game,self).__init__(build, self.TAG, + node = node, + name = name, + version = version, + ModuleOther1 = ModuleOther1, + ModuleOther2 = ModuleOther2, + VassalVersion = VassalVersion, + description = description, + nextPieceSlotId = nextPieceSlotId) + def nextPieceSlotId(self): + '''Increment next piece slot ID''' + ret = int(self.getAttribute('nextPieceSlotId')) + self.setAttribute('nextPieceSlotId',str(ret+1)) + return ret + # + def addBasicCommandEncoder(self,**kwargs): + '''Add a `BasicCommandEncoder` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : BasicCommandEncoder + The added element + ''' + return self.add(BasicCommandEncoder,**kwargs) + def addGlobalTranslatableMessages(self,**kwargs): + '''Add a `GlobalTranslatableMessages` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : GlobalTranslatableMessages + The added element + ''' + return self.add(GlobalTranslatableMessages,**kwargs) + def addPlayerRoster(self,**kwargs): + '''Add a `PlayerRoster` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PlayerRoster + The added element + ''' + return self.add(PlayerRoster,**kwargs) + def addChessClock(self,**kwargs): + '''Add a `ChessClockControl` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PlayerRoster + The added element + ''' + return self.add(ChessClockControl,**kwargs) + def addLanguage(self,**kwargs): + '''Add a `Language` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Language + The added element + ''' + return self.add(Language,**kwargs) + def addChatter(self,**kwargs): + '''Add a `Chatter` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Chatter + The added element + ''' + return self.add(Chatter,**kwargs) + def addKeyNamer(self,**kwargs): + '''Add a `KeyNamer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : KeyNamer + The added element + ''' + return self.add(KeyNamer,**kwargs) + def addNotes(self,**kwargs): + '''Add a `Notes` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Notes + The added element + ''' + return self.add(Notes,**kwargs) + def addLanguage(self,**kwargs): + '''Add a `Language` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Language + The added element + ''' + return self.add(Language,**kwargs) + def addChatter(self,**kwargs): + '''Add a `Chatter` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Chatter + The added element + ''' + return self.add(Chatter,**kwargs) + def addKeyNamer(self,**kwargs): + '''Add a `KeyNamer` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : KeyNamer + The added element + ''' + return self.add(KeyNamer,**kwargs) + def addGlobalProperties(self,**kwargs): + '''Add a `GlobalProperties` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : GlobalProperties + The added element + ''' + return self.add(GlobalProperties,**kwargs) + def addGlobalOptions(self,**kwargs): + '''Add a `GlobalOptions` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : GlobalOptions + The added element + ''' + return self.add(GlobalOptions,**kwargs) + def addTurnTrack(self,**kwargs): + '''Add a `TurnTrack` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : TurnTrack + The added element + ''' + return self.add(TurnTrack,**kwargs) + def addDocumentation(self,**kwargs): + '''Add a `Documentation` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Documentation + The added element + ''' + return self.add(Documentation,**kwargs) + def addPrototypes(self,**kwargs): + '''Add a `Prototypes` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Prototypes + The added element + ''' + return self.add(Prototypes,**kwargs) + def addPieceWindow(self,**kwargs): + '''Add a `PieceWindow` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PieceWindow + The added element + ''' + return self.add(PieceWindow,**kwargs) + def addChartWindow(self,**kwargs): + '''Add a `ChartWindow` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : ChartWindow + The added element + ''' + return self.add(ChartWindow,**kwargs) + def addInventory(self,**kwargs): + '''Add a `Inventory` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Inventory + The added element + ''' + return self.add(Inventory,**kwargs) + def addMap(self,**kwargs): + '''Add a `Map` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Map + The added element + ''' + return self.add(Map,**kwargs) + def addDiceButton(self,**kwargs): + '''Add a `DiceButton` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : DiceButton + The added element + ''' + return self.add(DiceButton,**kwargs) + def addPredefinedSetup(self,**kwargs): + '''Add a `PredefinedSetup` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : PredefinedSetup + The added element + ''' + return self.add(PredefinedSetup,**kwargs) + def addGameMassKey(self,**kwargs): + '''Add a `GameMassKey` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : GameMassKey + The added element + ''' + return self.add(GameMassKey,**kwargs) + def addStartupMassKey(self,**kwargs): + '''Add a `StartupMassKey` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : StartupMassKey + The added element + ''' + return self.add(StartupMassKey,**kwargs) + def addMenu(self,**kwargs): + '''Add a `Menu` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Menu + The added element + ''' + return self.add(Menu,**kwargs) + def addSymbolicDice(self,**kwargs): + '''Add a `SymbolicDice` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : SymbolicDice + The added element + ''' + return self.add(SymbolicDice,**kwargs) + + + # ---------------------------------------------------------------- + def getGlobalProperties(self,single=True): + '''Get all or a sole `GlobalPropertie` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `GlobalPropertie` child, otherwise fail. + If `False` return all `GlobalPropertie` children in this element + Returns + ------- + children : list + List of `GlobalPropertie` children (even if `single=True`) + ''' + return self.getAllElements(GlobalProperties,single) + def getBasicCommandEncoder(self,single=True): + '''Get all or a sole `Ba` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Ba` child, otherwise fail. + If `False` return all `Ba` children in this element + Returns + ------- + children : list + List of `Ba` children (even if `single=True`) + ''' + return self.getAllElements(BasicCommandEncoder,single) + def getGlobalTranslatableMessages(self,single=True): + '''Get all or a sole `GlobalTranslatableMessage` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `GlobalTranslatableMessage` child, otherwise fail. + If `False` return all `GlobalTranslatableMessage` children in this element + Returns + ------- + children : list + List of `GlobalTranslatableMessage` children (even if `single=True`) + ''' + return self.getAllElements(GlobalTranslatableMessages,single) + def getLanguages(self,single=False): + '''Get all or a sole `Language` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Language` child, otherwise fail. + If `False` return all `Language` children in this element + Returns + ------- + children : list + List of `Language` children (even if `single=True`) + ''' + return self.getAllElements(Language,single) + def getChessClocks(self,asdict=False): + '''Get all or a sole `Language` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Language` child, otherwise fail. + If `False` return all `Language` children in this element + Returns + ------- + children : list + List of `Language` children (even if `single=True`) + ''' + return self.getElementsByKey(ChessClockControl,'name',asdict) + def getChatter(self,single=True): + '''Get all or a sole `Chatter` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Chatter` child, otherwise fail. + If `False` return all `Chatter` children in this element + Returns + ------- + children : list + List of `Chatter` children (even if `single=True`) + ''' + return self.getAllElements(Chatter,single) + def getKeyNamer(self,single=True): + '''Get all or a sole `KeyNamer` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `KeyNamer` child, otherwise fail. + If `False` return all `KeyNamer` children in this element + Returns + ------- + children : list + List of `KeyNamer` children (even if `single=True`) + ''' + return self.getAllElements(KeyNamer,single) + def getDocumentation(self,single=True): + '''Get all or a sole `Documentation` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Documentation` child, otherwise fail. + If `False` return all `Documentation` children in this element + Returns + ------- + children : list + List of `Documentation` children (even if `single=True`) + ''' + return self.getAllElements(Documentation,single) + def getPrototypes(self,single=True): + '''Get all or a sole `Prototypes` (i.e., the containers of + prototypes, not a list of actual prototypes) element(s) from + this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Prototypes` child, otherwise fail. + If `False` return all `Prototypes` children in this element + Returns + ------- + children : list + List of `Prototype` children (even if `single=True`) + + ''' + return self.getAllElements(Prototypes,single) + def getPlayerRoster(self,single=True): + '''Get all or a sole `PlayerRo` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `PlayerRo` child, otherwise fail. + If `False` return all `PlayerRo` children in this element + Returns + ------- + children : list + List of `PlayerRo` children (even if `single=True`) + ''' + return self.getAllElements(PlayerRoster,single) + def getGlobalOptions(self,single=True): + '''Get all or a sole `GlobalOption` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `GlobalOption` child, otherwise fail. + If `False` return all `GlobalOption` children in this element + Returns + ------- + children : list + List of `GlobalOption` children (even if `single=True`) + ''' + return self.getAllElements(GlobalOptions,single) + def getInventories(self,asdict=True): + '''Get all Inventorie element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Inventorie` elements. If `False`, return a list of all Inventorie` children. + Returns + ------- + children : dict or list + Dictionary or list of `Inventorie` children + ''' + return self.getElementsByKey(Inventory,'name',single) + def getPieceWindows(self,asdict=True): + '''Get all PieceWindow element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `PieceWindow` elements. If `False`, return a list of all PieceWindow` children. + Returns + ------- + children : dict or list + Dictionary or list of `PieceWindow` children + ''' + return self.getElementsByKey(PieceWindow,'name',asdict) + def getChartWindows(self,asdict=True): + '''Get all ChartWindow element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `ChartWindow` elements. If `False`, return a list of all ChartWindow` children. + Returns + ------- + children : dict or list + Dictionary or list of `ChartWindow` children + ''' + return self.getElementsByKey(ChartWindow,'name',asdict) + def getDiceButtons(self,asdict=True): + '''Get all DiceButton element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `DiceButton` elements. If `False`, return a list of all DiceButton` children. + Returns + ------- + children : dict or list + Dictionary or list of `DiceButton` children + ''' + return self.getElementsByKey(DiceButton,'name',asdict) + def getPredefinedSetups(self,asdict=True): + '''Get all PredefinedSetup element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `PredefinedSetup` elements. If `False`, return a list of all PredefinedSetup` children. + Returns + ------- + children : dict or list + Dictionary or list of `PredefinedSetup` children + ''' + return self.getElementsByKey(PredefinedSetup,'name',asdict) + def getNotes(self,single=True): + '''Get all or a sole `Note` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Note` child, otherwise fail. + If `False` return all `Note` children in this element + Returns + ------- + children : list + List of `Note` children (even if `single=True`) + ''' + return self.getAllElements(Notes,single) + def getTurnTracks(self,asdict=True): + '''Get all TurnTrack element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `TurnTrack` elements. If `False`, return a list of all TurnTrack` children. + Returns + ------- + children : dict or list + Dictionary or list of `TurnTrack` children + ''' + return self.getElementsByKey(TurnTrack,'name',asdict) + def getPieces(self,asdict=False): + '''Get all Piece element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Piece` elements. If `False`, return a list of all Piece` children. + Returns + ------- + children : dict or list + Dictionary or list of `Piece` children + ''' + return self.getElementsByKey(PieceSlot,'entryName',asdict) + def getSpecificPieces(self,*names,asdict=False): + '''Get all SpecificPiece element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `SpecificPiece` elements. If `False`, return a list of all SpecificPiece` children. + Returns + ------- + children : dict or list + Dictionary or list of `SpecificPiece` children + ''' + return self.getSpecificElements(PieceSlot,'entryName', + *names,asdict=asdict) + def getMap(self,asdict=False): + return self.getElementsByKey(Map,'mapName',asdict) + def getWidgetMaps(self,asdict=True): + '''Get all WidgetMap element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `WidgetMap` elements. If `False`, return a list of all WidgetMap` children. + Returns + ------- + children : dict or list + Dictionary or list of `WidgetMap` children + ''' + return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict) + def getMaps(self,asdict=True): + '''Get all Map element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Map` elements. If `False`, return a list of all Map` children. + Returns + ------- + children : dict or list + Dictionary or list of `Map` children + ''' + maps = self.getMap(asdict=asdict) + wmaps = self.getWidgetMaps(asdict=asdict) + if asdict: + maps.update(wmaps) + else: + maps.extend(wmaps) + return maps + def getBoards(self,asdict=True): + '''Get all Board element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` + elements. If `False`, return a list of all Board` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + + ''' + return self.getElementsByKey(Board,'name',asdict) + def getGameMassKeys(self,asdict=True): + '''Get all GameMassKey element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` + elements. If `False`, return a list of all Board` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + + ''' + return self.getElementsByKey(GameMassKey,'name',asdict) + def getStartupMassKeys(self,asdict=True): + '''Get all StartupMassKey element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` + elements. If `False`, return a list of all Board` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + + ''' + return self.getElementsByKey(StartupMassKey,'name',asdict) + def getMenus(self,asdict=True): + '''Get all Menu element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `Board` + elements. If `False`, return a list of all Board` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `Board` children + + ''' + return self.getElementsByKey(Menu,'text',asdict) + def getSymbolicDices(self,asdict=True): + '''Get all Menu element(s) from this + + Parameters + ---------- + asdict : bool + If `True`, return a dictonary that maps key to `SymbolicDice` + elements. If `False`, return a list of all `SymbolicDice` + children. + + Returns + ------- + children : dict or list + Dictionary or list of `SymbolicDice` children + + ''' + return self.getElementsByKey(SymbolicDice,'name',asdict) + def getAtStarts(self,single=False): + '''Get all or a sole `AtStart` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `AtStart` child, otherwise fail. + If `False` return all `AtStart` children in this element + Returns + ------- + children : list + List of `AtStart` children (even if `single=True`) + ''' + return self.getAllElements(AtStart,single) + + +# -------------------------------------------------------------------- +class BasicCommandEncoder(GameElement): + TAG = Element.MODULE+'BasicCommandEncoder' + def __init__(self,doc,node=None): + super(BasicCommandEncoder,self).__init__(doc,self.TAG,node=node) +# +# EOF +# # ==================================================================== -def piece_body(family,main,flipped,gpid): - '''Create an full add piece command from the side, image, and ID - information. This adds (in reverse order) - - - BasicPiece - - Side prototype - - Flip layer (if 'flipped' is not none) - - Parameters - ---------- - family : str - Side in the game - main : dict - Main image information - flipped : dict - Flipped image information, possibly None - gpid : int - Global piece identifier +# From buildfile.py - Returns - ------- - cmd : str - The command to add the piece - ''' - # Create the layer, prototype, and basic piece traits - decs = [] - if flipped is not None: - imgs = [ main['filename'], flipped['filename'] ] - decs.extend([ layer_trait(images=imgs, - newNames=['','Reduced +'], - activateName = '', - decreaseName = '', - increaseName = 'Flip', - increaseKey = key('F'), - decreaseKey = '', - name='Step'), - report_trait(key('F')) ]) - for m in main.get('mains',[]): - decs.append(prototype_trait(m + ' prototype')) - ech = main.get('echelon',None) - cmd = main.get('command',None) - if ech is not None: decs.append(prototype_trait(ech + ' prototype')) - if cmd is not None: decs.append(prototype_trait(cmd + ' prototype')) - - decs.extend([ prototype_trait(family+' prototype'), - basic_trait(main['name'], main['filename'],gpid) - ]) - - typs = [d[0] for d in decs] - stts = [d[1] for d in decs] - typ = enc_parts(*typs) - stt = enc_parts(*stts) - - return add_piece(typ,stt) - -# -------------------------------------------------------------------- -def proto_body(family): - '''Create an full add prototype com from the side information. - This adds (in reverse order) - - - An empty BasicPiece - - An eliminate (or sendto) trait - - A delete trait - - A mark moved trait - - A move trail trait - - Parameters - ---------- - family : str - Side in the game - - Returns - ------- - cmd : str - The command to add the piece - ''' - # Dummy image - decs = [ report_trait(key('E'),key('R'),key('M')), - moved_trait(), - trail_trait(), - delete_trait(), - sendto_trait('DeadMap',family+' pool','Eliminate',key('E'), - 'Restore',key('R'),description="Eliminate unit"), - mark_trait('Faction',family), - basic_trait('','','') ] - typs = [d[0] for d in decs] - stts = [d[1] for d in decs] - typ = enc_parts(*typs) - stt = enc_parts(*stts) +# -------------------------------------------------------------------- +class BuildFile(Element): + def __init__(self,root=None): + '''Construct from a DOM object, if given, otherwise make new''' + from xml.dom.minidom import Document + super(BuildFile,self).__init__(None,'',None) + + self._root = root + if self._root is None: + self._root = Document() + + self._node = self._root + + def addGame(self,**kwargs): + '''Add a `Game` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Game + The added element + ''' + return Game(self,**kwargs) + + def getGame(self): + '''Get the `Game`''' + return Game(self, + node=self._root.\ + getElementsByTagName('VASSAL.build.GameModule')[0]) + + def encode(self): + '''Encode into XML''' + return self._root.toprettyxml(indent=' ', + encoding="UTF-8", + standalone=False) - return add_proto(typ,stt) +# +# EOF +# +# ==================================================================== +# From moduledata.py # -------------------------------------------------------------------- -def add_splash(root,doc,categories,title='',verbose=False): - '''Add an about page - - Parametersg - ---------- - root : xml.dom.minidom.Document - Root of the document - doc : xml.dom.minidom.Element - The documentation element - categories : dict - Catalog of images - title : str - Title of the game - ''' - fronts = categories.get('front',{}).get('all',[]) - front = list(fronts.values())[0] if len(fronts) > 0 else None - if front is None: - return +class Data(Element): + TAG = 'data' + def __init__(self,doc,node=None,version='1'): + super(Data,self).__init__(doc,self.TAG,node=node,version=version) + + def addVersion(self,**kwargs): + '''Add a `Version` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Version + The added element + ''' + return self.add(Version,**kwargs) + def addVASSALVersion(self,**kwargs): + '''Add a `VASSALVersion` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : VASSALVersion + The added element + ''' + return self.add(VASSALVersion,**kwargs) + def addName(self,**kwargs): + '''Add a `Name` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Name + The added element + ''' + return self.add(Name,**kwargs) + def addDescription(self,**kwargs): + '''Add a `Description` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Description + The added element + ''' + return self.add(Description,**kwargs) + def addDateSaved(self,**kwargs): + '''Add a `DateSaved` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : DateSaved + The added element + ''' + return self.add(DateSaved,**kwargs) + def getVersion(self,single=True): + '''Get all or a sole `Version` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Version` child, otherwise fail. + If `False` return all `Version` children in this element + Returns + ------- + children : list + List of `Version` children (even if `single=True`) + ''' + return self.getAllElements(Version,single=single) + def getVASSALVersion(self,single=True): + '''Get all or a sole `VASSALVersion` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `VASSALVersion` child, + otherwise fail. If `False` return all `VASSALVersion` + children in this element + Returns + ------- + children : list + List of `VASSALVersion` children (even if `single=True`) + + ''' + return self.getAllElements(VASSALVersion,single=single) + def getName(self,single=True): + return self.getAllElements(Name,single=single) + def getDescription(self,single=True): + '''Get all or a sole `Description` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `Description` child, + otherwise fail. If `False` return all `De` children in + this element + Returns + ------- + children : list + List of `De` children (even if `single=True`) + + ''' + return self.getAllElements(Description,single=single) + def getDateSaved(self,single=True): + '''Get all or a sole `DateSaved` element(s) from this + + Parameters + ---------- + single : bool + If `True`, there can be only one `DateSaved` child, otherwise fail. + If `False` return all `DateSaved` children in this element + Returns + ------- + children : list + List of `DateSaved` children (even if `single=True`) + ''' + return self.getAllElements(DateSaved,single=single) + +# -------------------------------------------------------------------- +class DataElement(Element): + def __init__(self,data,tag,node=None,**kwargs): + super(DataElement,self).__init__(data,tag,node=node,**kwargs) - if verbose: - print(f'Adding about page with {title}') + def getData(self): + return self.getParent(Data) - add_about(root,doc, f'About {title}', fileName = front['filename']) +# -------------------------------------------------------------------- +class Version(DataElement): + TAG = 'version' + def __init__(self,data,node=None,version=''): + super(Version,self).__init__(data,self.TAG,node=node) + if node is None: + self.addText(version) # -------------------------------------------------------------------- -def add_rules(root,doc,zipfile,rules,verbose=False): - '''Add rules to the module +class VASSALVersion(DataElement): + TAG = 'VassalVersion' + def __init__(self,data,node=None,version='3.6.7'): + super(VASSALVersion,self).__init__(data,self.TAG,node=node) + if node is None: + self.addText(version) - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - doc : xml.dom.minidom.Element - The documentation element - zipfile : zipfile.ZipFile - The module archive - rules : str - File name of rules (PDF) - ''' - if rules is None or rules == '': - return +# -------------------------------------------------------------------- +class Name(DataElement): + TAG = 'name' + def __init__(self,data,node=None,name=''): + super(Name,self).__init__(data,self.TAG,node=node) + if node is None: + self.addText(name) + +# -------------------------------------------------------------------- +class Description(DataElement): + TAG = 'description' + def __init__(self,data,node=None,description=''): + super(Description,self).__init__(data,self.TAG,node=node) + if node is None: + self.addText(description) + +# -------------------------------------------------------------------- +class DateSaved(DataElement): + TAG = 'dateSaved' + def __init__(self,data,node=None,milisecondsSinceEpoch=-1): + super(DateSaved,self).__init__(data,self.TAG,node=node) + if node is None: + from time import time + s = f'{int(time()*1000)}' if milisecondsSinceEpoch < 0 else \ + str(milisecondsSinceEpoch) + self.addText(s) + +# -------------------------------------------------------------------- +class ModuleData(Element): - if verbose: - print('Adding rules PDF to module') + def __init__(self,root=None): + '''Construct from a DOM object, if given, otherwise make new''' + from xml.dom.minidom import Document + super(ModuleData,self).__init__(None,'',None) - zipfile.write(rules,'rules.pdf') + self._root = root + if self._root is None: + self._root = Document() + + self._node = self._root + + def addData(self,**kwargs): + '''Add a `Data` element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Data + The added element + ''' + return Data(self,**kwargs) + + def getData(self): + return Data(self, + node=self._root.getElementsByTagName(Data.TAG)[0]) + + def encode(self): + return self._root.toprettyxml(indent=' ', + encoding="UTF-8", + standalone=False) - add_pdffile(root,doc,title='Show rules',pdfFile='rules.pdf') +# +# EOF +# +# ==================================================================== +# From save.py -# -------------------------------------------------------------------- -def add_notes(root,doc,zipfile,title,version,sides,pdffile,infofile,rules, - verbose=False): - '''Add rules to the module +# ==================================================================== +class SaveIO: + '''Wrapper around a save file + + Save file is + + "!VCSK" KEY content + + Key is two bytes drawn as a random number in 0-255. Content is + two bytes per character. Content characters are encoded with the + random key. + + Save file (.vsav) content is + + "begin_save" ESC + "\" ESC + [commands]* ESC + "PLAYER" name password side ESC + [map+"BoardPicker" name x y ESC]+ + "SETUP_STACK" ESC + "TURN"+name state ESC + "end_save" + + Commands are + + "+/" id "/" body "\" + + where body are + + "stack" "/" mapName ; x ; y ; ids "\" + piece_type "/" piece_state (x and y set here too) "\" + + x and y are pixel coordinates (sigh!). This means we have to know + + - the pixel location of a hex + - the hex coordinates of that hex + - whether rows and columns are descending + - if even hexes are higher or not + + The two first items _must_ be user supplied (I think). When we + add stacks or pieces, we must then get the numerical hex + coordinates - not what the user specified in the VASSAL editor or + the like. Of course, this means opening the module before writing + the patch.py script. + + It seems like every piece is added in a stack. + + The id is a numerical value. Rather big (e.g., 1268518000806). It + is the current number of miliseconds since epoch, with offset to + disambiguate. + + The ID is the current time, taken from a milisecond clock, + possibly adjusted up if there is a clash. This is all managed by + the GameState class. - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - doc : xml.dom.minidom.Element - The documentation element - zipfile : zipfile.ZipFile - The module archive - sides : list - Sides in the game - pdffile : str - The images PDF - infofile : str - The images information - rules : str - File name of rules (PDF) ''' - factions = "".join([f'
  • {s}
  • ' for s in sides]) - notes = f''' - - -

    Draft VASSAL module of {title} (Version {version})

    -

    - This is a draft VASSAL module of {title} generated from -

    -
      -
    • images in {pdffile}, and
    • -
    • information in {infofile}
    • -
    -

    - both generated by LaTeX using the wargame [1] package, and - processed by the Python script wgexport.py from that - package. -

    -

    - The module is not complete and the developer should edit - the module as needed in the VASSAL module editor. -

    -

    - [1] https://gitlab.com/wargames_tex/wargame_tex -

    -

    Content of the module

    -

    - The wgexport.py Python script has identified the - factions of the game to be -

    -
      - '''+factions+f''' -
    -

    - and generated prototype pieces for these (along with possible - non-faction pieces. The script also generated a map ("dead map") - that contains boards for each faction and is meant to store - eliminated units ("<faction> pool"). These pools have a - generic shape and colour, and the developer should take care to - correct these as needed. -

    -

    - Boards of the game present in the {pdffile} and {infofile} files - have been generated. Each board has a single zone in it that - encompasses the full board, and a hex grid is added to that - zone. The module developer should adjust the zone and embedded - hex grid according to the actual map, using the VASSAL module - editor. The hex grid (as well as coordinates) is drawn by - default so as to make this obvious to the developer. The hex - sizes should be correct, and mainly the offset needs to be - adjusted. -

    -

    - Images marked as OOBs have also been added as maps to the - module. These also have a single zone with a rectangular grid - imposed in it. This grid, and the grid coordinates are drawn by - default, so it is easier to see and to remember to adjust it. The - module developer should adjust this, possibly adding zones, so - that it matches up with the graphics. The developer can add - units to the OOB via 'At-start' stacks in the VASSAL editor. -

    -

    - All pieces identified by {infofile} and present in {pdffile} have - been generated. Double sided pieces (identified by images which - also has a suffix-"flipped" image) are created as such. -

    -

    - Charts identified by the {infofile} and {pdffile} have been added - to the module. The are added as separate (pop-up) frames, but - the module developer can change this in the VASSAL module editor. -

    -

    - If the {infofile} and {pdffile} identified a front page, then that - has been added to module as the splash screen. -

    -

    - If a PDF rules file ({rules}) was passed to the Python script, - then that has been included in the module and is accessible from - the "Help" menu. -

    -

    After finalising the module

    -

    - After the developer has finalised the module (added more pieces, - fixed board zones a and grids, add in more commands), then this - Help menu entry should be deleted. -

    - - - ''' - if version.lower() == 'draft': - if verbose: - print(f' Adding draft notes {version}') - zipfile.writestr('help/notes.html',notes) - add_helpfile(root,doc,title='Draft Module notes', - fileName='help/notes.html') - - keys = f''' - - -

    {title} (Version {version}) Key bindings

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Key Where Effect
    Alt-A-Show the charts panel
    Alt-B-Show the OOBs
    Alt-C-Show the counters panel
    Alt-E-Show the eliminated units
    Alt-I-Show/refresh inventory window
    Alt-M-Show map
    Alt-T-Increase turn track
    Alt-Shift-T-Decrease turn track
    Alt-6-Roll the dice
    Ctrl-DBoard,CounterDelete selected counters
    Ctrl-EBoard,CounterEliminate selected counters
    Ctrl-FBoard,CounterFlip selected counters
    Ctrl-MBoard,CounterToggle "moved" markers on selected counters
    Ctrl-OBoardHide/show counters
    Ctrl-RBoard,CounterRestore unit (from dead pool)
    Ctrl-TBoard,CounterToggle move trail
    Ctrl-+BoardZoom in
    Ctrl--BoardZoom out
    Ctrl-=BoardSelect zoom
    Ctrl-Shift-OBoardShow overview map
    ←,→,↑↓BoardScroll board left, right, up, down (slowly)
    PnUp,PnDnBoardScroll board up/down (fast)
    Ctrl-PnUp,Ctrl-PnDnBoardScroll board left/right (fast)
    Mouse-scroll up/downBoardScroll board up//down
    Shift-Mouse-scroll up/downBoardScroll board right/leftown
    Ctrl-Mouse-scroll up/downBoardZoom board out/in
    Mouse-2BoardCentre on mouse
    - - - ''' - if verbose: - print(f'Add key bindings help file') - zipfile.writestr('help/keys.html',keys) - add_helpfile(root,doc,title='Key bindings', - fileName='help/keys.html') + VCS_HEADER = b'!VCSK' + VK_ESC = chr(27) + DEC_MAP = { + # 0-9 + 0x30: 0x30, + 0x31: 0x30, + 0x32: 0x30, + 0x33: 0x30, + 0x34: 0x30, + 0x35: 0x30, + 0x36: 0x30, + 0x37: 0x30, + 0x38: 0x30, + 0x39: 0x30, + # A-F + 0x41: 0x37, + 0x42: 0x37, + 0x43: 0x37, + 0x44: 0x37, + 0x45: 0x37, + 0x46: 0x37, + # a-f + 0x61: 0x57, + 0x62: 0x57, + 0x63: 0x57, + 0x64: 0x57, + 0x65: 0x57, + 0x66: 0x57 + } + ENC_MAP = [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9', + b'a',b'b',b'c',b'd',b'e',b'f'] + + # ---------------------------------------------------------------- + @classmethod + def decHex(cls,b): + '''Decode a single char into a number + + If the encoded number is b, then the decoded number is + + b - off - return notes - -# -------------------------------------------------------------------- -def add_tut(root,doc,zipfile,tutorial,verbose=False): - if tutorial is None: - return None - - if verbose: - print(f'Adding tutorial {tutorial}') - zipfile.write(tutorial,'tutorial.vlog') + where off is an offset that depends on b + + off = 0x30 if 0x30 <= b <= 0x39 + 0x37 if 0x41 <= b <= 0x46 + 0x57 if 0x61 <= b <= 0x66 + ''' + return b - cls.DEC_MAP[b] + # -------------------------------------------------------------------- + @classmethod + def readByte(cls,inp,key): + '''Read a single byte of information from input stream + + Two characters (c1 and c2) are read from input stream, and the + decoded byte is then + + ((dechex(c1) << 4 | dechex(c2)) ^ key) & 0xFF + + Parameters + ---------- + inp : stream + Input to read from + key : int + Key to decode the input + + Returns + ------- + b : int + The read byte + ''' + try: + pair = inp.read(2) + except Exception as e: + from sys import stderr + print(e,file=stderr) + return None + + if len(pair) < 2: + return None + + return ((cls.decHex(pair[0]) << 4 | cls.decHex(pair[1])) ^ key) & 0xFF + # -------------------------------------------------------------------- + @classmethod + def readSave(cls,file,alsometa=False): + '''Read data from save file. The data is read into lines + returned as a list. + + ''' + from zipfile import ZipFile + + # We open the save file as a zip file + with ZipFile(file,'r') as z: + # open the save file in the archive + save = z.open('savedGame','r') + + # First, we check the header + head = save.read(len(cls.VCS_HEADER)) + assert head == cls.VCS_HEADER, \ + f'Read header {head} is not {cls.VCS_HEADER}' + + # Then, read the key + pair = save.read(2) + key = (cls.decHex(pair[0]) << 4 | cls.decHex(pair[1])) + + # Now read content, one byte at a time + content = '' + while True: + byte = cls.readByte(save,key) + if byte is None: + break - return add_tutorial(root,doc,name='Tutorial',logfile='tutorial.vlog') + # Convert byte to character + content += chr(byte) + + lines = content.split(cls.VK_ESC) + + if alsometa: + savedata = z.read(VSav.SAVE_DATA) + moduledata = z.read(VMod.MODULE_DATA) + + if not alsometa: + return key, lines + + return key,lines,savedata,moduledata + + # -------------------------------------------------------------------- + @classmethod + def writeByte(cls,out,byte,key): + '''Write a single byte + + Parameters + ---------- + out : IOStream + Stream to write to + byte : char + Single byte to write + key : int + Key to encode with (defaults to 0xAA - alternating 0's and 1's) + ''' + b = ord(byte) ^ key + pair = cls.ENC_MAP[(b & 0xF0) >> 4], cls.ENC_MAP[b & 0x0F] + out.write(pair[0]) + out.write(pair[1]) + + # -------------------------------------------------------------------- + @classmethod + def writeInZip(cls,z,key,lines,filename='savedGame'): + '''Write a save file in a zip file (VMod)''' + # open the save file in the archive + with z.open(filename,'w') as save: + # Write header + save.write(cls.VCS_HEADER) + + # Split key + pair = cls.ENC_MAP[(key & 0xF0) >> 4], cls.ENC_MAP[(key & 0x0F)] + save.write(pair[0]) + save.write(pair[1]) + + # Form content + content = cls.VK_ESC.join(lines) + + # Write each character as two + for c in content: + cls.writeByte(save, c, key) + + # -------------------------------------------------------------------- + @classmethod + def writeSave(cls,file,key,lines,savedata=None,moduledata=None): + '''Write a save file''' + from zipfile import ZipFile, ZIP_DEFLATED + + # We open the save file as a zip file + with ZipFile(file,'w',ZIP_DEFLATED) as z: + cls.writeInZip(z,key,lines,filename='savedGame') -# -------------------------------------------------------------------- -def add_basics_to_map(root,map,**attr): - '''Add a map to the document + if savedata is not None: + z.writestr(VSav.SAVE_DATA,savedata) + z.writestr(VMod.MODULE_DATA,moduledata) + +# ==================================================================== +# +# VSave file +# +class SaveFile: + def __init__(self,game,firstid=None): + '''Creates a save file to add positions to''' + from time import time + self._game = game + self._counters = {} + self._stacks = {} + self._pieces = self._game.getPieces(asdict=True) + self._nextId = (int(time()*1000) - 360000 + if firstid is None else firstid) + + def add(self,grid,**kwargs): + '''Add pieces to the save. + + Parameters + ---------- + grid : BaseGrid + Grid to add pieces to + kwargs : dict + Either a map from piece name to hex position, + Or a map from hex position to list of pieces + ''' + for k,v in kwargs.items(): + # print('Add to save',k,v) + self._add(grid,k,v) + + def _add(self,grid,k,v): + '''Add to the save''' + # print(f'Adding {k} -> {v}') + loc = None + piece = self._pieces.get(k,None) + pieces = [] + boardName = grid.getBoard()['name'] + # print(f'Board name: {boardName}') + if piece is not None: + # print(f'Key is piece: {k}->{piece}') + pieces.append(piece) + loc = v + else: + # Key is not a piece name, so a location + loc = k + # Convert value to iterable + try: + iter(v) + except: + v = list(v) + + for vv in v: + if isinstance(vv,PieceSlot): + pieces.append(vv) + continue + if isinstance(vv,str): + piece = self._pieces.get(vv,None) + if piece is None: + continue + pieces.append(piece) + + # print(f'Loc: {loc}') + if len(pieces) < 1: + return + + if (boardName,loc) not in self._stacks: + # print(f'Adding stack {boardName},{loc}') + coord = grid.getLocation(loc) + if coord is None: + print(f'did not get coordinates from {loc}') + return + self._stacks[(boardName,loc)] = { + 'x': coord[0], + 'y': coord[1], + 'pids': [] } + + place = self._stacks[(boardName,loc)] + for piece in pieces: + name = piece['entryName'] + count = self._counters.get(name,None) + + if count is None: + count = {'pid': self._nextId, + 'piece': piece, + 'board': boardName, + 'x': place['x'], + 'y': place['y'], + } + self._counters[name] = count + self._nextId += 1 - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - parent : xml.dom.minidom.Element - The parent element - button : str - Button text - name : str - Name of the map - stacking : bool - Whether to enable stacking + + # print(f'Adding to stack {boardName},{loc}: {count[0]}') + place['pids'].append(count['pid']) - Returns - ------- - map : xml.dom.minidom.Element - The map element - bpicker : xml.dom.minidom.Element - The board picker in map - ''' - stacking = add_stacking(root,map) - add_camera (root,map) - add_forwardtochatter (root,map) - add_menudisplayer (root,map) - add_mapcenterer (root,map) - add_stackexpander (root,map) - add_piecemover (root,map) - add_keybufferer (root,map) - add_selectionhighlighters(root,map) # TBD - add_highlightlastmoved (root,map) - add_zoomer (root,map) + def getLines(self): + '''Get the final lines of code''' + key = 0xAA # fixed key + + lines = ['begin_save', + '', + '\\'] - return stacking + self._pieceLines(lines) + self._otherLines(lines) + + lines.append('end_save') + return lines + + def _pieceLines(self,lines): + '''Add piece lines to save file + + Parameters + ---------- + lines : list + The lines to add + ''' + # print(self._counters) + for counter in self._counters.values(): + iden = counter['pid'] + piece = counter['piece'] + traits = piece.getTraits() + traits = Trait.flatten(traits,self._game) + # Get last - trait (basic piece), and modify coords + basic = traits[-1] + basic['map'] = counter['board'] + basic['x'] = counter['x'] + basic['y'] = counter['y'] + # Set old location if possible + parent = piece.getParent(DummyElement,checkTag=False) + if parent is not None and parent._node.nodeName == AtStart.TAG: + oldLoc = parent['location'] + oldBoard = parent['owningBoard'] + oldMap = self._game.getBoards()[oldBoard].getMap()['mapName'] + oldX = parent['x'] + oldY = parent['y'] + oldZone = None + zones = self._game.getBoards()[oldBoard].getZones() + for zone in zones.values(): + grid = zone.getGrids()[0] + if grid is None: continue + + coord = grid.getLocation(oldLoc) + if coord is None: continue + + oldZone = zone['name'] + oldX = coord[0] + oldY = coord[1] + break + + if oldZone is not None: + basic['properties'] = \ + f'6;OldZone;{oldZone};OldLocationName;{oldLoc};'+\ + f'OldX;{oldX};OldY;{oldY};'+\ + f'OldBoard;{oldBoard};OldMap;{oldMap}' + else: + basic['properties'] = \ + f'5;OldLocationName;{oldLoc};'+\ + f'OldX;{oldX};OldY;{oldY};'+\ + f'6;OldBoard;{oldBoard};OldMap;{oldMap}' + + for trait in traits: + if trait.ID == TrailTrait.ID: + trait['map'] = oldMap + trait['points'] = f'1;{oldX},{oldY}' + trait['init'] = True + + # Wrapper + wrap = DummyWithTraits(self._game,traits=[]) + wrap.setTraits(*traits,iden=str(iden)) + lines.append(wrap._node.childNodes[0].nodeValue+'\\') + + layer = -1 + for key,dat in self._stacks.items(): + pids = dat.get('pids',None) + x = dat['x'] + y = dat['y'] + if pids is None or len(pids) < 1: + print(f'No pieces at {key[0]},{key[1]}') + continue + + iden = self._nextId + self._nextId += 1 + stack = StackTrait(board=key[0],x=x,y=y,pieceIds=pids,layer=layer) + layer = 1 + wrap = DummyWithTraits(self._game,traits=[]) + wrap.setTraits(stack,iden=iden) + lines.append(wrap._node.childNodes[0].nodeValue+'\\') + + def _otherLines(self,lines): + '''Add other lines to save''' + lines.append('UNMASK\tnull') + for r in self._game.getPlayerRoster(): + lines.extend(r.encode()) + for n in self._game.getNotes(single=False): + lines.extend(n.encode()) + setupStack = False + for m in self._game.getMaps(asdict=False): + for bp in m.getBoardPicker(single=False): + lines.extend(bp.encode()) + if not setupStack: + atstart = m.getAtStarts(single=False) + if atstart and len(atstart) > 0: + lines.append('SETUP_STACK') + setupStack = True + + # for tk,tt in self._game.getTurnTracks(asdict=True): + # lines.extend(tt.encode()) + # -------------------------------------------------------------------- -def add_elim(root,game,sides,categories,verbose=False): - '''Add an eliminated map - - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The parent element - sides : list of str - List of sides in the game - ''' - if verbose: - print(' Adding eliminated map and boards') - - if sides is None or len(sides) == 0: - return - - map = add_map(root,game,'DeadMap', - buttonName = '', #'Eliminated units', - icon = '/images/playerAway.gif', - markMoved = 'Never', - launch = True, - allowMultiple = True, - hotkey=key('E',ALT)) - bpicker = add_boardpicker(root,map, - slotHeight = 400, - slotWidth = 400) - add_setup(root,bpicker,'DeadMap',2,[s+' pool' for s in sides]) - add_masskey(root,map,'Restore', - key('R'), - key('R'), - icon = '/images/Undo16.gif', - tooltip = "Restore selected units") - - for i,s in enumerate(sides): - if verbose: - print(f' Adding {s} pool') - - color = [0,0,0,64] - color[i % 3] = 255 - - w = 400 - h = 400 - c = ','.join([str(c) for c in color]) - dimg = categories.get('pool',{}).get('all',{}).get(s,None) - img = '' - - if dimg: - bb = get_bb(dimg['img']) - w = bb[2] - bb[0] - h = bb[3] - bb[1] - c = '' - img = dimg['filename'] - if verbose: - print(f' Using image provided by the user: {img}') - - add_board(root,bpicker,f'{s} pool', - image = img, - color = c, - width = w, - height = h) - - add_basics_to_map(root,map) +class SaveData(ModuleData): + def __init__(self,root=None): + '''Convinience wrapper''' + super(SaveData,self).__init__(root=root) +# ==================================================================== +# From vsav.py # -------------------------------------------------------------------- -def add_hex_grid(root,parent,color=rgb(255,0,0),visible=True,grid=True): - '''Add a hex grid, either to a zone or to the multizone container''' - - hexes = add_hexgrid(root,parent,color=color,visible=visible, - edgesLegal=True) - if not grid: - return hexes - - add_hexnumbering(root,hexes, - color = color, - hType = 'A', - hOff = -1, - vType = 'N', - vOff = -1, - visible = visible) - - return hexes +class VSav: + SAVE_DATA = 'savedata' + + def __init__(self,build,vmod): + '''Create a VASSAL save file programmatically + + Parameters + ---------- + build : xml.dom.Document + `buildFile.xml` as XML + vmod : VMod + Module file + ''' + from time import time + self._vmod = vmod + self._game = build.getGame() + self._start = int(time()*1000) + -# -------------------------------------------------------------------- -def get_picture_info(picture,name,width,height,verbose): - ''' - Returns - ------- - hex_width, hex_height : float, float - Scale hex width - scx, scy : float, float, float, float - Scale to image and picture (x,y) - rot90 : bool - True if rotated +/-90 degrees - tran : callable - Translation function - ''' - if picture is None: - print(f'WARNING: No Tikz picture information.' - f"Are you sure you used the `[zoned]' option for the " - f"tikzpicture environment of {name}?") - f = lambda x,y: (x,y) - return HEX_WIDTH,HEX_HEIGHT,1,1,False,f - - # Get picture bounding box - tll = picture['lower left'] - tur = picture['upper right'] - # Get picture transformation - pa = picture['xx'] - pb = picture['xy'] - pc = picture['yx'] - pd = picture['yy'] - # Get picture offset (always 0,0?) - pdx = picture['dx'] - pdy = picture['dy'] - # Define picture global transformation - pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y) - # Globally transform (rotate) picture bounding box - pll = pr(*tll) - pur = pr(*tur) - # Calculate widht, height, and scaling factors - pw = pur[0] - pll[0] - ph = pur[1] - pll[1] - scw = width / pw - sch = height / ph - # Extract picture scales and rotation - # Courtesy of - # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix - from math import sqrt, atan2, degrees, isclose - psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1) - psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1) - prt = degrees(atan2(pc,pd)) - if not any([isclose(abs(prt),a) for a in [0,90,180,270]]): - raise RuntimeException('Rotations of Tikz pictures other than ' - '0 or +/-90,+/- 180, or +/-270 not supported. ' - 'found {prt}') - rot90 = isclose(abs(prt),90) - if any([isclose(prt,a) for a in [90,270,180,-180]]): - print(f'WARNING: rotations by {prt} not fully supported') - - hex_width = psx * HEX_WIDTH - hex_height = psy * HEX_HEIGHT - if verbose: - print(f' Picture transformations: {pa},{pb},{pc},{pd}\n' - f' Scale (x,y): {psx},{psy}\n' - f' Rotation (degrees): {prt}') - - # When translating the Tikz coordinates, it is important to note - # that the Tikz y-axis point upwards, while the picture y-axis - # point downwards. This means that the upper right corner is at - # (width,0) and the lower left corner is at (0,height). - def tranx(x,off=-pll[0]): - # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}') - return int(scw * (x + off)+.5) - def trany(y,off=-pur[1]): - # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}') - return -int(sch * (y + off)+.5) - tran = lambda x,y : (tranx(x), trany(y)) - - return hex_width, hex_height, scw * psx, sch * psy, rot90, tran - -# -------------------------------------------------------------------- -def get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90, - labels,coords,targs,nargs): - targs['dx'] = hex_width - targs['dy'] = hex_height - if labels is not None: - labmap = { - 'auto': { - 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' }, - 'auto=numbers' : { - 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' }, - 'auto=alpha column': { - 'hLeading': 0, 'vLeading': 0, 'hType': 'A', 'vType': 'N', - 'hOff': -1 }, - 'auto=alpha 2 column': {# Not supported - 'hLeading': 1, 'vLeading': 1, 'hType': 'A', 'vType': 'N' }, - 'auto=inv y x plus 1': { - 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N', - 'hOff': 0, 'vDescend': True }, - 'auto=x and y plus 1': { - 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N', - 'hOff': 0, 'vOff': 0 } - } - for l in labels.split(','): - nargs.update(labmap.get(l,{})) - - # Field/Direction | Normal | Rotated | - # ----------------|--------|---------| - # hOff | column | column | - # vOff | row | row | - # hDescend | column | row | True, incr to left - # vDescend | row | column | True, incr upward - # First='H' | column | column | - - # In wargame offset=0 means that the first coordinates are 0,0. - # But offset 0,0 in VASSAL means start at 1,1 - horiz = 'column' - vert = 'row' - nargs['vOff'] = nargs.get('vOff',0)-coords[vert] ['offset']-1 - nargs['hOff'] = nargs.get('hOff',0)-coords[horiz]['offset']-1 - - # In VASSAL, vDesend=False means rows increase downward, - # while in wargame, factor=-1 means rows increase downward - horiz = 'row' if rot90 else 'column' - vert = 'column' if rot90 else 'row' - nargs['hDescend'] = coords[horiz]['factor'] != 1 - nargs['vDescend'] = coords[vert] ['factor'] != -1 # flipped y - targs['edgesLegal'] = True - - # Check for inversed columns - inv_col = coords['column']['factor'] == -1 - ell = hex_width if inv_col else 0 - - # Some special stuff in case of 90 degree rotation - if rot90: - targs.update({'sideways': True}) - nargs['hOff'] -= 1 - nargs['hDescend'] = not nargs['hDescend'] - if inv_col: - nargs['hOff'] += 3 - - margin = i.get('board frame',{}).get('margin',0) - mx = scx * margin - my = scy * margin - if inv_col: mx = 0 # Bug in wargame? - - # Grid offset is defined relative to the full image, - # not the zone: - # - # X,Y offset: The horizontal and vertical position of - # the center of the first hex of the grid. - # - # That is, (x,y)=(0,0) is in the top-left corner of the - # image, while in TikZ it is in the bottom left corner. - # This means we shift by the boudning box offsets plus - # some stuff to align with the zone. - # - # In case of inversed columns, we need to add some extra stuff. - x0, y0 = llx-hex_width/3 + ell + mx, ury + my - - # Rotated? - if rot90: - x0, y0 = ury+2*hex_width/3 + ell + mx, llx + my - - # Below should be double-checked. - - if not rot90: - if coords.get('column',{}).get('top short','') == 'isodd' \ - and coords.get('column',{}).get('offset',0) == -1: - y0 -= hex_width/2 - elif coords.get('column',{}).get('top short','') == 'iseven' \ - and coords.get('column',{}).get('offset',-1) == 0: - y0 -= hex_width/2 - - targs.update({'x0': int(x0),'y0': int(y0)}) - -# -------------------------------------------------------------------- -def get_rectparams(i,llx,ury,width,height,rot90,targs,nargs): - targs['dx'] = width - targs['dy'] = height - targs['x0'] = int(llx - width/2) - targs['y0'] = int(ury + height/2) - targs['color'] = rgb(0,255,0) - nargs['color'] = rgb(0,255,0) - nargs['vDescend'] = True - nargs['vOff'] = -3 - nargs.update({'sep':',','vLeading':0,'hLeading':0}) - -# -------------------------------------------------------------------- -def add_zones(root,zoned,name,board,width,height,gpid, - labels=None,coords=None,picinfo=None, - verbose=False,visible=True): - grids = [] - picture = None - if verbose: - print(f' Adding zones to {name} zoned={"zoned" in board}') - - for k, v in board.items(): - if k == 'labels': labels = v - if k == 'coords': coords = v - if k == 'zoned': picture = v - if 'zone' not in k or k == 'zoned': - continue - - gridtype = (add_hexgrid - if 'hex' in k.lower() - else add_squaregrid) - gridnum = (add_hexnumbering - if 'hex' in k.lower() - else add_squarenumbering) - grids.append([k,v,gridtype,gridnum]) - - if len(grids) < 1: return gpid - if verbose: - print(f' Got zones: {[k[0] for k in grids]}') - # Either get picture information from argument, or deduce from the - # found 'zoned' key. - if picinfo is None: - picinfo = get_picture_info(picture,name,width,height,verbose) - - hex_width, hex_height, scx, scy, rot90, tran = picinfo - - - for g in grids: - n, i, typ, num = g - if verbose: - print(f' Adding zone {n}') - if 'scope' in n: - llx,lly = tran(*i['global lower left']) - urx,ury = tran(*i['global upper right']) - path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]] - nm = n.replace('zone scope ','') - elif 'path' in n: - path = [tran(*ii) for ii in i['path']] - llx = min([px for px,py in path]) - ury = max([py for px,py in path]) - nm = n.replace('zone path ','') - - # Checkf if we have "point" type elements in this object and - # add them to dict. - points = [ v for k,v in i.items() - if (k.startswith('point') and - isinstance(v,dict) and \ - v.get('type','') == 'point')] - - pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path]) - if verbose: - print(f' Zone path ({llx},{ury}): {pathstr}') - zone = add_zone(root,zoned, - name=nm, - locationFormat = ("$name$" - if 'pool' in n.lower() else - "$gridLocation$"), - useParentGrid=False, - path=pathstr) - - # Do not add grids to pools - if 'pool' in n.lower(): - continue - - targs = {'color':rgb(255,0,0),'visible':visible} - nargs = {'color':rgb(255,0,0),'visible':visible} - if 'hex' in n.lower(): - get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90, - labels,coords,targs,nargs) - else: - width = hex_width / HEX_WIDTH * RECT_WIDTH - height = hex_height / HEX_HEIGHT * RECT_HEIGHT - get_rectparams(i,llx,ury,width,height,rot90,targs,nargs) + def createSaveData(self,description=None): + '''Create `savedgame`''' + desc = (self._game['description'] + if description is None else description) + self._saveData = SaveData(root=None) + data = self._saveData.addData() + data.addVersion (version =self._game['version']) + data.addVASSALVersion(version =self._game['VassalVersion']) + data.addDescription (description=desc) + data.addDateSaved (milisecondsSinceEpoch=self._start) + return self._saveData + + def createModuleData(self): + '''Create `moduleData`''' + self._moduleData = ModuleData() + data = self._moduleData.addData() + data.addVersion (version =self._game['version']) + data.addVASSALVersion(version =self._game['VassalVersion']) + data.addName (name =self._game['name']) + data.addDescription (description=self._game['description']) + data.addDateSaved (milisecondsSinceEpoch=self._start) + return self._moduleData + + def addSaveFile(self): + '''Add a save file to the module + + Returns + ------- + vsav : SaveFile + Save file to add content to + ''' + self._saveFile = SaveFile(game=self._game,firstid=self._start) + return self._saveFile + + def run(self,savename='Save.vsav',description=None): + '''Run this to generate the save file + + Parameters + ---------- + savename : str + Name of save file to write + description : str + Short description of the save file + ''' + from zipfile import ZipFile, ZIP_DEFLATED + + self.createSaveData(description=description) + self.createModuleData() + + with self._vmod.getInternalFile(savename,'w') as vsav: + with ZipFile(vsav,'w',ZIP_DEFLATED) as zvsav: + # The key is set to 0xAA (alternating ones and zeros) + SaveIO.writeInZip(zvsav,0xAA,self._saveFile.getLines()) - # print(targs,nargs) - if 'turn' in n.lower(): nargs['sep'] = 'T' - if 'oob' in n.lower(): nargs['sep'] = 'O' + zvsav.writestr(VMod.MODULE_DATA, self._moduleData.encode()) + zvsav.writestr(VSav.SAVE_DATA, self._saveData.encode()) + +# +# EOF +# +# ==================================================================== +# From vmod.py +# ==================================================================== +# +# Wrapper around a module +# +class VMod: + BUILD_FILE = 'buildFile.xml' + MODULE_DATA = 'moduledata' + + def __init__(self,filename,mode): + '''Interface to VASSAL Module (a Zip file)''' + self._mode = mode + self._vmod = self._open(filename,mode) + + def __enter__(self): + '''Enter context''' + return self + + def __exit__(self,*e): + '''Exit context''' + self._vmod.close() + return None - if verbose: - print(f' Adding grid') + def _open(self,filename,mode): + '''Open a file in VMod''' + from zipfile import ZipFile, ZIP_DEFLATED - if len(points) <= 0: - grid = typ(root,zone,**targs) - num(root,grid,**nargs) - else: - if verbose: - print(f' Use a region grid') - grid = add_regiongrid(root,zone,snapto=True,visible=visible) - for p in points: - pn = p["name"] - pc = p["coords"] - if verbose: - print(f' Add region {pn} {p}') + return ZipFile(filename,mode,compression=ZIP_DEFLATED) + + def removeFiles(self,*filenames): + '''Open a temporary zip file, and copy content from there to + that file, excluding filenames mentioned in the arguments. + Then close current file, rename the temporary file to this, + and reopen in 'append' mode. The deleted files are returned + as a dictionary. + + Parameters + ---------- + filenames : tuple + List of files to remove from the VMOD + + Returns + ------- + files : dict + Dictionary from filename to content of the removed files. + + Note, the VMOD is re-opened in append mode after this + ''' + from tempfile import mkdtemp + from zipfile import ZipFile + from shutil import move, rmtree + from os import path + + tempdir = mkdtemp() + ret = {} - n, gpid = add_region(root,grid,pn,*tran(*pc),gpid, - alsoPiece=True,verbose=verbose) - + try: + tempname = path.join(tempdir, 'new.zip') + with self._open(tempname, 'w') as tmp: - # Once we've dealt with this grid, we should see if we have - # any embedded zones we should deal with. - add_zones(root,zoned,name,i,width,height,gpid, - labels=labels,coords=coords,picinfo=picinfo, - verbose=verbose) - - # Adding a global grid - #targs['color'] = rgb(0,255,0) - #nargs['color'] = rgb(0,255,0) - #grid = typ(root,zoned,**targs) - #num(root,grid,**nargs) - - return gpid - - -# -------------------------------------------------------------------- -def add_inv(root,game,sides,**attr): - '''Add inventory to module''' - filt = '{' + '||'.join([f'Faction=="{s}"' for s in sides])+'}' - grp = 'Faction,Command,Echelon,Type' - inv = add_inventory(root,game, - include = filt, - groupBy = grp, - sortFormat = '$PieceName$', - tooltip = 'Show inventory of all pieces', - zoomOn = True, - **attr) - return inv - - -# -------------------------------------------------------------------- -def add_map_board(root,game,name,board,gpid,verbose=False,visible=False): - '''A add a single board to the module - ''' + for item in self._vmod.infolist(): + data = self._vmod.read(item.filename) + + if item.filename not in filenames: + tmp.writestr(item, data) + else: + ret[item.filename] = data - map = add_map(root,game, name) - bpicker = add_boardpicker(root,map) - - add_counterviewer (root,map) - add_hidepieces (root,map) - add_globalmap (root,map) - add_basics_to_map (root,map) - add_masskey (root,map,'Flip', key('F'),key('F'), - icon = '/images/Undo16.gif', - tooltip='Flip selected units') - add_masskey (root,map,'Eliminate',key('E'),key('E'), - icon = '/images/playerAway.gif', - tooltip='Eliminate selected units') - - # Get the image so we may make a rectangle - if verbose: - print(f' Adding board {board["name"]} -> {board["filename"]}') - ulx,uly,lrx,lry = get_bb(board['img']) - eboard = add_board(root,bpicker,name,image=board['filename']) - zoned = add_zoned(root,eboard) - #hexes = add_hexgrid(root,zoned) - add_zonehighlighter(root,zoned) - - if not 'zones' in board: - full = add_zone(root,zoned, - name='full', - useParentGrid=False, - path=(f'{ulx},{uly};' + - f'{lrx},{uly};' + - f'{lrx},{lry};' + - f'{ulx},{lry}')) - - # We do not have board info, so add a grid to the whole thing - add_hex_grid(root,full,rgb(255,0,0),True,True,visible=visible) - return - - # If we get here, we have board info! - w = abs(ulx-lrx) - h = abs(uly-lry) - return add_zones(root,zoned,name,board['zones'],w,h,gpid, - verbose=verbose,visible=visible) - -# -------------------------------------------------------------------- -def add_boards(root,game,categories,sides,gpid,verbose=False,visible=False): - '''Add all boards to the module, including eliminated map + name = self._vmod.filename + self._vmod.close() + move(tempname, name) - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The parent element - categories : dict - Catalogue of images - sides : list of str - List of sides in the game - Name of the map - ''' - if verbose: - print('Adding maps and boards') + self._mode = 'a' + self._vmod = self._open(name,'a') + finally: + rmtree(tempdir) + + # Return the removed files + return ret - # Loop over defined boards - for bn, b in categories.get('board',{}).get('all',{}).items(): - gpid = add_map_board(root,game,bn,b,gpid,verbose,visible) + def fileName(self): + '''Get name of VMod file''' + return self._vmod.filename + + def addFiles(self,**files): + '''Add a set of files to this + + Parameters + ---------- + files : dict + Dictionary that maps file name to content. + ''' + for filename,data in files.items(): + self.addFile(filename,data) + + def addFile(self,filename,content): + '''Add a file to this + + Parameters + ---------- + filename : str + File name in module + content : str + File content + Returns + ------- + element : File + The added element + ''' + self._vmod.writestr(filename,content) + + def addExternalFile(self,filename,target=None): + '''Add an external file element to this + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : ExternalFile + The added element + ''' + if target is None: target = filename + self._vmod.write(filename,target) - add_elim(root,game,sides,categories,verbose) + def getFileNames(self): + '''Get all filenames in module''' + return self._vmod.namelist() + + def getFiles(self,*filenames): + '''Return named files as a dictionary. + + Parameters + ---------- + filenames : tuple + The files to get + Returns + ------- + files : dict + Mapping of file name to file content + ''' + fn = self.getFileNames() + ret = {} + for f in filenames: + if f not in fn: + continue + + ret[f] = self._vmod.read(f) + + return ret + + def getDOM(self,filename): + '''Get content of a file decoded as XML DOM + + Parameters + ---------- + filename : str + Name of file in module + ''' + from xml.dom.minidom import parseString + + r = self.getFiles(filename) + if filename not in r: + raise RuntimeException(f'No {filename} found!') + + return parseString(r[filename]) + + def getBuildFile(self): + '''Get the buildFile.xml decoded as a DOM tree''' + return self.getDOM(VMod.BUILD_FILE) - return gpid + def getModuleData(self): + '''Get the moduledata decoded as a DOM tree''' + return self.getDOM(VMod.MODULE_DATA) -# -------------------------------------------------------------------- -def add_oob(root,widget,name,oob,gpid,hotkey='',verbose=False,visible=False): - '''A add a single board to the module - ''' - # Short hand - cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr) + def getInternalFile(self,filename,mode): + return self._vmod.open(filename,mode) - if verbose: - print(f' Adding oob {name}') + def addVSav(self,build): + '''Add a `VSav` element to this - map = add_widgetmap(root,widget, name,markMoved='Never',hotkey='') - bpicker = add_boardpicker(root,map) + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : VSav + The added element + ''' + return VSav(build=build,vmod=self) - add_counterviewer (root,map) - add_basics_to_map (root,map) - - # Get the image so we may make a rectangle - if verbose: - print(f' Adding OOB {oob["name"]} -> {oob["filename"]}') +# +# EOF +# +# ==================================================================== +# From exporter.py + +class Exporter: + def __init__(self): + '''Base class for exporters''' + pass + + + def setup(self): + '''Should be defined to set-up for processing, for example + generating images and such. This is executed in a context + where the VMod file has been opened for writing via + `self._vmod`. Thus, files can be added to the module at this + stage. + ''' + pass + + def createBuildFile(self,ignores='(.*markers?|all|commons|[ ]+)'): + '''Should be defined to make the `buildFile.xml` document + + Parameters + ---------- + ignores : str + Regular expression to match ignored categories for factions + determination. Python's re.fullmatch is applied to this + regular exression against chit categories. If the pattern + is matched, then the chit is not considered to belong to a + faction. + + ''' + pass + + def createModuleData(self): + '''Should be defined to make the `moduledata` document''' + pass + + def run(self,vmodname,patch=None): + '''Run the exporter to generate the module + ''' + with VMod(vmodname,'w') as vmod: + self._vmod = vmod + self.setup() + self.createBuildFile() + self.createModuleData() + self.runPatch(patch) + self._vmod.addFiles(**{VMod.BUILD_FILE : + self._build.encode(), + VMod.MODULE_DATA : + self._moduleData.encode()}) + Verbose().message('Created VMOD') - ulx,uly,lrx,lry = get_bb(oob['img']) - board = add_board(root,bpicker,name,image=oob['filename']) - zoned = add_zoned(root,board) - add_zonehighlighter(root,zoned) - if not 'zones' in oob: - zone = add_zone(root,zoned, - name='full', - useParentGrid=False, - path=(f'{ulx},{uly};' + - f'{lrx},{uly};' + - f'{lrx},{lry};' + - f'{ulx},{lry}')) - # We do not have board info, so add a grid to the whole thing - grid = add_squaregrid(root,zone,visible=visible) - add_squarenumbering(root,grid,visible=visible) - return + def runPatch(self,patch): + '''Run user specified patch script. The script should define - # If we get here, we have board info! - w = abs(ulx-lrx) - h = abs(uly-lry) - return add_zones(root,zoned,name,oob['zones'],w,h,gpid, - verbose=verbose,visible=visible) - -# -------------------------------------------------------------------- -def add_oobs(root,game,categories,gpid,verbose=False,visible=False): - '''Add all OObs to the module + ``` + def patch(buildFile,moduleData,vmod,verbose): + ``` - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The parent element - categories : dict - Catalogue of images - ''' - # Short hand - oobc = categories.get('oob',{}).get('all',{}).items() - if len(oobc) < 1: - return gpid - - oobs = add_chartwindow(root,game, 'OOBs', - hotkey = key('B',ALT), - description = 'OOBs', - text = '', - icon = '/images/inventory.gif', - tooltip = 'Show/hide OOBs') - tabs = add_tabs(root,oobs,'OOBs') - - if verbose: - print('Adding OOBs') - - # Loop over defined boards - for on, o in oobc: - widget = add_mapwidget(root,tabs, entryName=on) - gpid = add_oob(root,widget,on,o,gpid, - verbose = verbose, - hotkey = key('B',ALT), - visible = visible) - - return gpid - -# -------------------------------------------------------------------- -def add_markproto(root,protos,key,value): - decs = [ mark_trait(key,value.capitalize()), - basic_trait('','','') ] - typs = [d[0] for d in decs] - stts = [d[1] for d in decs] - typ = enc_parts(*typs) - stt = enc_parts(*stts) - body = add_proto(typ,stt) - add_prototype(root,protos,value+' prototype',body) - -# -------------------------------------------------------------------- -def add_counters(root,game,categories, - unittypes,echelons,commands,verbose=False): - '''Add all counters to the module. + where `buildFile` is the `buildFile.xml` and `moduleData` are + the XML documents as `xml.dom.Document` objects, `vmod` is a + `VMod` instance, and `verbose` is a `bool` selecting verbose + mode or not. + ''' + if patch is None or patch == '': + return + + from importlib.util import spec_from_file_location, module_from_spec + from pathlib import Path + from sys import modules - This will add all counters to the module, as well as prototypes - for each sub-category. + p = Path(patch) + with VerboseGuard(f'Will patch module with {p.stem}.patch function') \ + as v: - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The game element - categories : dict - Catalogue of images - ''' - # Short hand - # Create prototypes container before adding pieces - protos = add_prototypes(root,game) - for utype in unittypes: add_markproto(root,protos,'Type',utype) - for ech in echelons: add_markproto(root,protos,'Echelon',ech) - for cmd in commands: add_markproto(root,protos,'Command',cmd) - - # Stating global piece ID - gpid = 20 - - # A window to hold the pieces - piecew = add_piecewindow(root,game,'Counters', - hotkey = key('C',ALT)) - # Tab container for each sub-category - piecet = add_tabs(root,piecew,entryName='Counters') - - if verbose: - print('Adding counters and prototypes') - - # Loop over sub-categories - for subn, subc in categories.get('counter',{}).items(): - # Friendly message - if verbose: - print(f' Adding {subn} counters and prototype') + spec = spec_from_file_location(p.stem, p.absolute()) + module = module_from_spec(spec) + spec.loader.exec_module(module) + modules[p.stem] = module + + # Patch must accept xml.dom.document,xml.dom.document,ZipFile + module.patch(self._build, + self._moduleData, + self._vmod, + Verbose().verbose) + +# ==================================================================== +# From latexexporter.py - # Create a panel for this sub-category - piecep = add_panel(root,piecet, - entryName = subn, - fixed = False) - # Add a list to the panel - piecel = add_list(root,piecep, - entryName=f'{subn} counters') +# ==================================================================== +# +# Exporter class +# +class LaTeXExporter(Exporter): + def __init__(self, + vmodname = 'Draft.vmod', + pdfname = 'export.pdf', + infoname = 'export.json', + title = 'Draft', + version = 'draft', + description = '', + rules = None, + tutorial = None, + patch = None, + visible = True, + vassalVersion = '3.6.7', + nonato = False, + nochit = False, + counterScale = 1): + '''Exports a PDF and associated JSON files to a VASSAL module. + + Parameters + ---------- + vmodname : str + Name of module file to write + pdfname : str + Name of PDF file to read images from + infoname : str + Name of JSON file to read meta data from + title : str + Name of module + version : str + Version of midule + description : str + Short description of the module + rules : str + Optional name PDF file to attach as rules + tutorial : str + Optional name of a VASSAL log file to use as tutorial + patch : str + Optional name of Python script to post process the module + visible : bool + Make grids visible + vassalVersion : str + VASSAL version to encode this module for + ''' + self._vmodname = vmodname + self._pdfname = pdfname + self._infoname = infoname + self._title = title + self._version = version + self._description = description + self._rules = rules + self._tutorial = tutorial + self._patch = patch + self._visible = visible or version.lower() == 'draft' + self._vassalVersion = vassalVersion + self._nonato = nonato + self._nochit = nochit + self._counterScale = counterScale + self._battleMark = 'wgBattleMarker' + self._battleMarks = [] + self._oddsMarks = [] + self._resultMarks = [] + self._markBattle = key(NONE,0)+',wgMarkBattle' + self._clearBattle = key(NONE,0)+',wgClearBattle' + self._clearAllBattle = key(NONE,0)+',wgClearAllBattle' + self._zeroBattle = key(NONE,0)+',wgZeroBattle' + self._incrBattle = key(NONE,0)+',wgIncrBattle' + self._setBattle = key(NONE,0)+',wgSetBattle' + self._getBattle = key(NONE,0)+',wgGetBattle' + self._markOdds = key(NONE,0)+',wgMarkOdds' + self._markResult = key(NONE,0)+',wgMarkResult' + self._clearMoved = key(NONE,0)+',wgClearMoved' + self._zeroBattleAF = key(NONE,0)+',wgZeroBattleAF' + self._zeroBattleDF = key(NONE,0)+',wgZeroBattleDF' + self._zeroBattleFrac = key(NONE,0)+',wgZeroBattleFrac' + self._zeroBattleOdds = key(NONE,0)+',wgZeroBattleOdds' + self._zeroBattleShft = key(NONE,0)+',wgZeroBattleShift' + self._zeroBattleIdx = key(NONE,0)+',wgZeroBattleIdx' + self._calcBattleAF = key(NONE,0)+',wgCalcBattleAF' + self._calcBattleDF = key(NONE,0)+',wgCalcBattleDF' + self._calcBattleFrac = key(NONE,0)+',wgCalcBattleFrac' + self._calcBattleOdds = key(NONE,0)+',wgCalcBattleOdds' + self._calcBattleShft = key(NONE,0)+',wgCalcBattleShift' + self._calcBattleIdx = key(NONE,0)+',wgCalcBattleIdx' + self._calcBattleRes = key(NONE,0)+',wgCalcBattleResult' + self._clearBattlePhs = key(NONE,0)+',wgClearBattlePhs' + self._resolveBattle = key(NONE,0)+',wgResolveBattle' + self._rollDice = key(NONE,0)+',wgRollDice' + self._diceInitKey = key(NONE,0)+',wgInitDice' + self._clearKey = key('C') + self._clearAllKey = key('C',CTRL_SHIFT) + self._deleteKey = key('D') + self._eliminateKey = key('E') + self._flipKey = key('F') + self._trailKey = key('M') + self._restoreKey = key('R') + self._markKey = key('X') + self._resolveKey = key('Y') + self._rotateCCWKey = key('[') + self._rotateCWKey = key(']') + self._chartsKey = key('A',ALT) + self._oobKey = key('B',ALT) + self._countersKey = key('C',ALT) + self._deadKey = key('E',ALT) + self._diceKey = key('6',ALT) + self._battleCounter = 'wgBattleCounter' + self._currentBattle = 'wgCurrentBattle' + self._currentAttacker = 'wgCurrentAttacker' + self._battleNo = 'wgBattleNo' + self._battleAF = 'wgBattleAF' + self._battleDF = 'wgBattleDF' + self._battleFrac = 'wgBattleFrac' + self._battleIdx = 'wgBattleIdx' + self._battleOdds = 'wgBattleOdds' + self._battleOddsM = 'wgBattleOddsMarker' + self._battleShift = 'wgBattleShift' + self._battleCtrl = 'wgBattleCtrl' + self._battleCalc = 'wgBattleCalc' + self._battleUnit = 'wgBattleUnit' + self._battleResult = 'wgBattleResult' + self._autoOdds = 'wgAutoOdds' + self._autoResults = 'wgAutoResults' + self._noClearMoves = 'wgNoClearMoves' + self._noClearBattles = 'wgNoClearBattles' + self._debug = 'wgDebug' + self._verbose = 'wgVerbose' + self._hidden = None + self._hiddenName = 'wg hidden unit' + self._dice = {} + self._diceInit = None + + with VerboseGuard('Overall settings') as v: + v(f'Module file name: {self._vmodname}') + v(f'PDF file name: {self._pdfname}') + v(f'JSON file name: {self._infoname}') + v(f'Game title: {self._title}') + v(f'Game version: {self._version}') + v(f'Description: {self._description}') + v(f'Rules PDF file: {self._rules}') + v(f'Tutorial log: {self._tutorial}') + v(f'Patch scripts: {self._patch}') + v(f'Visible grids: {self._visible}') + v(f'Scale of counters: {self._counterScale}') + + def setup(self): + # Start the processing + self._info = self.convertPages() + self._categories, \ + self._mains, \ + self._echelons, \ + self._commands = self.writeImages(self._counterScale) - if verbose: - print(f' Prototype: {subn} prototype') - # Create prototype for this sub-category - proto = add_prototype(root,protos, - name = f'{subn} prototype', - code = proto_body(subn), - description = f'Prototype for {subn} counters') - - # Loop over counters in this sub-category - for cn, c in subc.items(): - # Flipped counters are not added directly - if cn.endswith('flipped'): continue - if verbose: - print(f' counter: {cn}') - - # Get image bounding box - bb = get_bb(c['img']) + def run(self): + super(LaTeXExporter,self).run(self._vmodname,self._patch) + + - # Get flipped image, if any - cf = subc.get(cn + ' flipped', None) + # ================================================================ + def createProcess(self,args): + '''Spawn a process and pipe output here + + Parameters + ---------- + args : list + List of process command line elements + Returns + ------- + pipe : subprocess.Pipe + Pipe to read from + ''' + from os import environ + from subprocess import Popen, PIPE + + return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE) + + # ---------------------------------------------------------------- + def addPws(self,opw=None,upw=None): + '''Add a `Pws` element to arguments + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + Returns + ------- + element : Pws + The added element + ''' + '''Add password options''' + args = [] + if upw is not None: args.extend(['-upw',upw]) + if opw is not None: args.extend(['-opw',opw]) + return args + + # ---------------------------------------------------------------- + def getPdfInfo(self,upw=None,opw=None,timeout=None): + '''Get information about the PDF + + Parameters + ---------- + opw : str + Owner password (optional) + upw : str + User password (optional) + timeout : int + Time out in miliseconds for subprocesses + + Returns + ------- + info : dict + Image information + ''' + args = ['pdfinfo', self._pdfname ] + args.extend(self.addPws(opw=opw,upw=upw)) + + with VerboseGuard(f'Getting information from PDF {self._pdfname}'): + proc = self.createProcess(args) + try: + out, err = proc.communicate(timeout=timeout) + except: + proc.kill() + proc.communicate() + raise RuntimeError(f'Failed to get PDF info: {e}') + + d = {} + for field in out.decode('utf8','ignore').split('\n'): + if field == '': + continue + subfields = field.split(':') + key, value = subfields[0], ':'.join(subfields[1:]) + if key != '': + d[key] = (int(value.strip()) if key == 'Pages' + else value.strip()) + + if 'Pages' not in d: + raise ValueError(f'Page count not found from {self._pdfname}') + + return d + + # ---------------------------------------------------------------- + def getImagesInfo(self): + '''Read in JSON information, and return as dictionary''' + from json import load + + with VerboseGuard(f'Getting information from JSON {self._infoname}'): + with open(self._infoname) as file: + info = load(file) + + return info + + # ================================================================ + def convertPage(self,page,opw=None,upw=None,timeout=None): + '''Convert a page from PDF into an image (bytes) + + Parameters + ---------- + page : int + Page number in the PDF to convert + opw : str + Owner password (optional) + upw : str + User password (optional) + timeout : int + Time out in miliseconds for subprocesses + + Returns + ------- + info : dict + Image information + ''' + args = ['pdftocairo', + '-transp', + '-singlefile', + '-f', str(page), + '-l', str(page), + '-png' ] + args.extend(self.addPws(opw=opw,upw=upw)) + args.append(self._pdfname) + args.append('-') + + proc = self.createProcess(args) - # Create a slot for this piece - sl = add_pieceslot(root,piecel, - entryName = cn, - gpid = gpid, - code = piece_body(subn,c,cf,gpid), - height = bb[3]-bb[1], - width = bb[2]-bb[0]) + try: + out, err = proc.communicate(timeout=timeout) + except Exception as e: + proc.kill() + proc.communicate() + raise RuntimeError(f'Failed to convert page {page} of ' + f'{self._pdfname}: {e}') + + # Just return the bytes + return out + + + # ---------------------------------------------------------------- + def ignoreEntry(self,info,ignores=['<>','<>']): + '''Check if we should ignore an entry in the JSON file''' + return info['category'] in ignores - # Store the global piece identifier - do we need this? - c['gpid'] = gpid + # ---------------------------------------------------------------- + def scaleImage(self,buffer,factor): + from PIL import Image + from io import BytesIO + from math import isclose - # Increment global piece identifier - gpid += 1 + if isclose(factor,1): return buffer - # Return next free piece identifier - return gpid + # print(f'Scaling image by factor {factor}') + with Image.open(BytesIO(buffer)) as img: + w, h = img.width, img.height + cpy = img.resize((int(factor*w),int(factor*h))) -# -------------------------------------------------------------------- -def add_charts(root,game,categories,verbose=False): - '''Add all boards to the module + with BytesIO() as out: + cpy.save(out,format='PNG') + return out.getvalue() - This will select all 'chart' elements from the catalogue and add - them as individual (popup) panels to the module. + + # ---------------------------------------------------------------- + def convertPages(self,opw=None,upw=None,timeout=None): + '''Reads in JSON and pages from PDF and stores information + dictionary, which is returned + + Parameters + ---------- + opw : str + Owner password (optional) + upw : str + User password (optional) + timeout : int + Time out in miliseconds for subprocesses + + Returns + ------- + info : dict + Image information + ''' + oargs = {'opw':opw,'upw':upw } + docinfo = self.getPdfInfo() + imgsinfo = self.getImagesInfo() + + if len(imgsinfo) - 1 != docinfo['Pages']: + raise RuntimeError(f'Number of pages in {pdfname} ' + f'(doc["Pages"]) not matched in JSON ' + f'{self._infoname} -> {len(imagesinfo)}') + + with VerboseGuard(f'Converting {docinfo["Pages"]} ' + f'pages in {self._pdfname}') as v: + for i,info in enumerate(imgsinfo): + if self.ignoreEntry(info): continue + + if i == 0: v(end='') + v(f'[{info["number"]}]',end=' ',flush=True) + info['img'] = self.convertPage(info['number'],**oargs) + + v('done') + + return imgsinfo + + # ---------------------------------------------------------------- + def getBB(self,buffer): + '''Get bounding box of image - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The game element - categories : dict - Catalogue of images - - ''' - chartc = categories.get('chart',{}).get('all',{}).items() - if len(chartc) < 1: - return + Parameters + ---------- + buffer : bytes + The image bytes - # Short hand - cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr) - cw = lambda parent,tag,**attr : create_elem(root,parent,vw(tag),**attr) - - if verbose: - print('Adding charts') - - charts = add_chartwindow(root,game, 'Charts', - hotkey = key('A',ALT), - description = 'Charts', - tooltip = 'Show/hide Charts') - tabs = add_tabs(root,charts,'Charts') + Returns + ------- + ulx, uly, lrx, lry : tuple + The coordinates of the bounding box + ''' + from PIL import Image + from io import BytesIO + with Image.open(BytesIO(buffer)) as img: + bb = img.getbbox() - # Loop over defined charts - for cn, c in chartc: - if verbose: - print(f' Chart {cn} ({c["filename"]})') - add_chart(root,tabs, - chartName = cn, - description = cn, - fileName = c['filename']) + return bb -# -------------------------------------------------------------------- -def add_diceroll(root,game,categories,verbose=False): - '''Add a dice button (1d6) + # ---------------------------------------------------------------- + def getWH(self,buffer): + '''Get bounding box of image - Parameters - ---------- - root : xml.dom.minidom.Document - Root of the document - game : xml.dom.minidom.Element - The game element - categories : dict - Catalogue of images - ''' - if verbose: - print('Adding dice') - - add_dice(root,game, - hotkey = key('6',ALT), - nDice = 1, - nSides = 6, - text = '1d6', - tooltip = 'Roll a 1d6', - name = '1d6') - - -# ==================================================================== -def create_build(info, - categories, - unittypes, - echelons, - commands, - zipfile, - title, - version, - desc = '', - rules = None, - tutorial = None, - pdffile = '', - infofile = '', - verbose = False, - visible = False): - '''Create the buildFile.xml document - - Parameters - ---------- - info : dict - Information dicationary - categories : dict - Catalogue of images - unittypes : set - Set of used unit types - zipfile : zipfile.ZipFile - Module archive - title : str - Title of module - version : str - Version number of module - desc : str - Short description of the game - rules : str - Filename of PDF rules document (if any) + Parameters + ---------- + buffer : bytes + The image bytes + + Returns + ------- + ulx, uly, lrx, lry : tuple + The coordinates of the bounding box + ''' + from PIL import Image + from io import BytesIO + + with Image.open(BytesIO(buffer)) as img: + w, h = img.width, img.height + + return w,h + + # ---------------------------------------------------------------- + def getOutline(self,buffer): + '''Get bounding box of image + + Parameters + ---------- + buffer : bytes + The image bytes + + Returns + ------- + ulx, uly, lrx, lry : tuple + The coordinates of the bounding box + ''' + from PIL import Image + from io import BytesIO + + # print(buffer) + with Image.open(BytesIO(buffer)) as img: + bb = img.getbbox() + + for r in range(bb[0],bb[2]): + for c in range(bb[1],bb[3]): + pass #print(img.getpixel((c,r))) + + return None + + + # ================================================================ + def writeImages(self,counterScale=1): + '''From the information gathered about the images (including + their bitmap representation, generate image files in the + module + + ''' + categories = {} + unittypes = [] + echelons = [] + commands = [] + + with VerboseGuard(f'Writing images in VMod ' + f'{self._vmod.fileName()}',end=' ') as v: + for info in self._info: + if self.ignoreEntry(info): continue + + typ = info.get('category','counter') + sub = info.get('subcategory','all') + + info['filename'] = info['name'].replace(' ','_') + '.png' + imgfn = 'images/'+info['filename'] + if imgfn not in self._vmod.getFileNames(): + if typ == 'counter': + # print(f'Possibly scale file {imgfn}') + info['img'] = self.scaleImage(info['img'], + counterScale) + # self.message(f'Writing image {imgfn}') + self._vmod.addFile(imgfn,info['img']) + + if sub == '': + info['subcategory'] = 'all' + sub = 'all' + + # Add into catalogue + if typ not in categories: + v('') + v(f'Adding category "{typ}"') + v('',end=' ') + categories[typ] = {} + cat = categories[typ] + + if sub not in cat: + v('') + v(f'Adding sub-category "{sub}"') + v('',end=' ') + cat[sub] = {} + tgt = cat[sub] + + v(f'[{info["name"]}]',end=' ',flush=True,noindent=True) + #self.message(f'Adding "{info["name"]}" to catalogue') + tgt[info['name']] = info + + if self._nonato: continue + + # Get NATO App6c information, if any + natoapp6c = info.get('natoapp6c',None) + if natoapp6c is not None: + from re import sub + def clean(s): + return sub('.*=','', + (sub(r'\[[^]]+\]','',s.strip()) + .replace('{','') + .replace('}','') + .replace('/',' '))).strip() + mains = clean(natoapp6c.get('main', '')) + lower = clean(natoapp6c.get('lower', '')) + upper = clean(natoapp6c.get('upper', '')) + echelon = clean(natoapp6c.get('echelon','')) + command = clean(natoapp6c.get('command','')) - Returns - ------- - xml : str - The encoded document - ''' - from xml.dom.minidom import Document - - if verbose: - print(f'Creating buildFile.xml: {title},{version}') - - # Create document - root = Document() - - # Add game element - game = add_game(root,root, - name = title, - version = version, - description = desc) - - # Extract the sides of the game from the counter sub-categories - # Should be a little more clever about the case here - dsides = {k.lower() : k for k in categories.get('counter',{}).keys()} - sides = None - for skip in ['all','common', 'marker', 'markers']: - if skip in dsides: del dsides[skip] - sides = list(dsides.values()) - if verbose: - print('Identified factions: ',sides) - - # Use the standard command encoder - add_basiccommandencoder(root,game) - - # Add document parts - doc = add_documentation(root,game) - add_splash(root, doc, categories, title,verbose) - add_rules(root, doc, zipfile, rules, verbose) - add_tut(root, doc, zipfile, tutorial, verbose) - notes = add_notes(root,doc,zipfile,title,version, - sides,pdffile,infofile,rules,verbose) - - - # Create roster or player sides - roster = add_roster(root,game) - for s in sides: - add_entry(root,roster,s) - - # Add global options - mainly defer to user settings - add_globaloptions(root,game, - autoReport = 'Use Preferences Setting', - centerOnMove = 'Use Preferences Setting', - nonOwnerUnmaskable = 'Use Preferences Setting', - playerIdFormat = '$playerName$') - # Add global properties - gp = add_globalproperties(root,game) - # Add a turn tracker - tt = add_turntrack(root,game,'Turn',counter={ - 'property': 'Turn', - 'phases': { - 'property': 'Phase', - 'names': sides }}) - add_globalproperty(root,gp,'TurnTracker.defaultDocked',True) - - # Add all pieces to the module (including prototypes) - gpid = add_counters(root,game,categories, - unittypes,echelons,commands,verbose) - # Add an inventory - add_inv(root,game,sides) - # Add all boards to the module (including eliminated map) - gpid = add_boards(root,game,categories,sides,gpid, - verbose=verbose,visible=visible) - # Add all OOBs to the module - gpid = add_oobs(root,game,categories,gpid, - verbose=verbose,visible=visible) - # Add charts to the module - add_charts(root,game,categories,verbose=verbose) - # Add a dice rool - add_diceroll(root,game,categories,verbose=verbose) - - # Set next available piece identifier on the game - game.setAttribute('nextPieceSlotId',str(gpid+1)) - - # Return the document - return root, notes - -# -------------------------------------------------------------------- -def create_data(title,version,desc='',verbose=False): - '''Create the module data XML + + if mains is not None: + if len(lower) > 0: mains += ' '+lower + if len(upper) > 0: mains += ' '+upper + mains = sub(r'\[[^]]+\]','',mains)\ + .replace('{','').replace('}','')#.split(',') + unittypes.append(mains.replace(',',' ')) + unittypes.extend([s.strip().replace(',',' ') + for s in mains.split(',')]) + #if len(mains) > 1: + # unittypes.append('+'.join(mains)) + info['mains'] = mains + + if len(echelon) > 0: + echelons.append(echelon) + info['echelon'] = echelon + + if len(command) > 0: + commands.append(command) + info['command'] = command + + + # Finished loop over infos. Make unit types, echelons, + # commands unique + v('done') + + return categories, set(unittypes), set(echelons), set(commands) + + # ================================================================ + def createModuleData(self): + '''Create the `moduleData` file in the module + ''' + with VerboseGuard(f'Creating module data'): + self._moduleData = ModuleData() + data = self._moduleData.addData() + data.addVersion (version=self._version) + data.addVASSALVersion(version=self._vassalVersion) + data.addName (name=self._title) + data.addDescription (description=self._description) + data.addDateSaved () + + # ================================================================ + def createBuildFile(self, + ignores = '(.*markers?|all|commons|.*hidden|[ ]+)'): + '''Create the `buildFile.xml` file in the module. + + Parameters + ---------- + ignores : str + Regular expression to match ignored categories for factions + determination. Python's re.fullmatch is applied to this + regular exression against chit categories. If the pattern + is matched, then the chit is not considered to belong to a + faction. + + ''' + from re import fullmatch, IGNORECASE + with VerboseGuard(f'Creating build file') as v: + self._build = BuildFile() # 'buildFile.xml') + self._game = self._build.addGame(name = self._title, + version = self._version, + description = self._description) + doc = self.addDocumentation() + self._game.addBasicCommandEncoder() + + # Extract the sides + self._sides = [ k + for k in self._categories.get('counter',{}).keys() + if fullmatch(ignores, k, IGNORECASE) is None] + v(f'Got sides: {", ".join(self._sides)}') + + v(f'Adding Global options') + go = self._game.addGlobalOptions( + autoReport = GlobalOptions.PROMPT, + centerOnMove = GlobalOptions.PROMPT, + nonOwnerUnmaskable = GlobalOptions.PROMPT, + playerIdFormat = '$playerName$') + go.addOption(name='undoHotKey',value=key('Z')) + go.addOption(name='undoIcon', value='/images/Undo16.gif') + # go.addOptoin(name='stepHotKey',value='') + go.addBoolPreference(name = self._verbose, + default = True, + desc = 'Be verbose', + tab = self._title) + go.addBoolPreference(name = self._debug, + default = False, + desc = 'Show debug chat messages', + tab = self._title) + go.addBoolPreference(name = self._autoOdds, + default = False, + desc = 'Calculate Odds on battle declaration', + tab = self._title) + go.addBoolPreference(name = self._autoResults, + default = False, + desc = 'Resolve battle results automatically', + tab = self._title) + go.addBoolPreference(name = self._noClearMoves, + default = False, + desc = ('Do not remove moved markers ' + 'on phase change'), + tab = self._title) + go.addBoolPreference(name = self._noClearBattles, + default = False, + desc = ('Do not remove battle markers ' + 'on phase change'), + tab = self._title) + + v(f'Adding player roster') + roster = self._game.addPlayerRoster() + for side in self._sides: + roster.addSide(side) + + v(f'Adding global properties') + glob = self._game.addGlobalProperties() + glob.addProperty(name='TurnTracker.defaultDocked', + initialValue=True) + + self._battleMarks = self._categories\ + .get('counter',{})\ + .get('BattleMarkers',[]) + if len(self._battleMarks) > 0: + v(f'We have battle markers') + + glob.addProperty(name = self._battleCounter, + initialValue = 0, + isNumeric = True, + min = 0, + max = len(self._battleMarks), + wrap = True, + description = 'Counter of battles') + glob.addProperty(name = self._currentBattle, + initialValue = 0, + isNumeric = True, + min = 0, + max = len(self._battleMarks), + wrap = True, + description = 'Current battle number') + glob.addProperty(name = self._currentAttacker, + initialValue = 0, + isNumeric = True, + min = 0, + max = 1, + wrap = True, + description = 'Current unit is attacker') + glob.addProperty(name = self._battleAF, + initialValue = 0, + isNumeric = True, + description = 'Current battle AF') + glob.addProperty(name = self._battleDF, + initialValue = 0, + isNumeric = True, + description = 'Current battle DF') + glob.addProperty(name = self._battleFrac, + initialValue = 0, + isNumeric = True, + description = 'Current battle fraction') + glob.addProperty(name = self._battleShift, + initialValue = 0, + isNumeric = True, + description = 'Current battle odds shift') + glob.addProperty(name = self._battleOdds, + initialValue = '', + isNumeric = False, + description = 'Current battle odds') + glob.addProperty(name = self._battleResult, + initialValue = '', + isNumeric = False, + description = 'Current battle results') + glob.addProperty(name = self._battleIdx, + initialValue = 0, + isNumeric = True, + description = 'Current battle odds index') + + self._oddsMarks = self._categories\ + .get('counter',{})\ + .get('OddsMarkers',[]) + if len(self._oddsMarks) > 0: + v(f'We have odds markers') + + self._resultMarks = self._categories\ + .get('counter',{})\ + .get('ResultMarkers',[]) + if len(self._resultMarks) > 0: + v(f'We have result markers') + + self.addNotes() + v(f'Adding turn track') + turns = self._game.addTurnTrack(name='Turn', + counter={ + 'property': 'Turn', + 'phases': { + 'property': 'Phase', + 'names': self._sides } }) + turns.addHotkey(hotkey = self._clearMoved+'Phase', + name = 'Clear moved markers') + if len(self._battleMarks) > 0: + turns.addHotkey(hotkey = self._clearBattlePhs, + name = 'Clear battle markers') + + self._dice = self._categories\ + .get('die-roll',{}) + if len(self._dice) > 0: + v(f'We have symbolic dice') + self._diceInit = [] + # from pprint import pprint + # pprint(self._dice,depth=3) + for die, faces in self._dice.items(): + ico = self.getIcon(die+'-die-icon','') + # print(f'Die {die} icon="{ico}"') + + dmin = +100000 + dmax = -100000 + symb = self._game.addSymbolicDice( + name = die+'Dice', + text = die if ico == '' else '', + icon = ico, + tooltip = f'{die} die roll', + format = (f'{{"{die} die roll: "+result1' + # f'+" "' + f'}}'), + resultWindow = True); + sdie = symb.addDie(name = die); + for face, fdata in faces.items(): + fn = fdata['filename'] + val = int(fn.replace('.png','').replace(die+'-','')) + dmin = min(dmin,val) + dmax = min(dmax,val) + sdie.addFace(icon = fn, + text = str(val), + value = val); + + self._diceInit.extend([ + GlobalPropertyTrait( + ['',self._diceInitKey, + GlobalPropertyTrait.DIRECT, + f'{{{dmin}}}'], + name = die+'Dice_result', + numeric = True, + min = dmin, + max = dmax, + description = f'Initialize {die}Dice'), + ReportTrait( + self._diceInitKey, + report=(f'{{{self._debug}?("Initialize ' + f'{die}Dice_result to {dmin}"):""}}')) + ]) + - Parameters - ---------- - title : str - Title of the game - version : str - Version number of the module - desc : str - Short description - - Returns - ------- - xml : str - The encoded document - ''' - from xml.dom.minidom import Document - from time import time - - if verbose: - print('Create module data') - - # The document - root = Document() - - # Top of the document - data = root.createElement('data') - data.setAttribute("version","1") - root.appendChild(data) - - # Version number of module - vers = root.createElement('version') - vers.appendChild(root.createTextNode(str(version))) - data.appendChild(vers) - - # Name of the game - name = root.createElement('name') - name.appendChild(root.createTextNode(title)) - data.appendChild(name) - - # VASSAL version (should get this in some other way) - vvers = root.createElement('VassalVersion') - vvers.appendChild(root.createTextNode("3.6.7")) - data.appendChild(vvers) - - # Description, if any - desc = root.createElement('description') - data.appendChild(desc) - - # Automatically set the save data - save = root.createElement('dateSaved') - save.appendChild(root.createTextNode(f'{int(time())}')) - data.appendChild(save) - - return root - - -# ==================================================================== -def create_vmod(vmodname, - info, - title, - version, - desc = '', - rules = None, - tutorial = None, - pdfname = '', - infoname = '', - patch = None, - verbose = False, - visible = False): - '''Create a VASSAL module from the information passed + # Add start-up key + self._game.addStartupMassKey( + name = 'Initialise dice results', + hotkey = self._diceInitKey, + target = '', + filter = f'{{BasicName=="{self._hiddenName}"}}', + whenToApply = StartupMassKey.EVERY_LAUNCH, + reportFormat=f'{{{self._debug}?("Init Dice results"):""}}') + + + - Parameters - ---------- - vmodname : str - Name of the module - info : list - List of images and image data - title : str - Name of the game - version : str - Version of the module - desc : str - Short description of the game - rules : str - Rules PDF (optional) + + + self.addKeybindings(doc) + self.addCounters() + self.addInventory() + self.addBoards() + self.addDeadMap() + self.addOOBs() + self.addCharts() + self.addDie() + + # ---------------------------------------------------------------- + def addDocumentation(self): + '''Add documentation to the module. This includes rules, + key-bindings, and about elements. + ''' + with VerboseGuard('Adding documentation') as v: + doc = self._game.addDocumentation() + if self._rules is not None: + self._vmod.addExternalFile(self._rules,'rules.pdf') + doc.addBrowserPDFFile(title = 'Show rules', + pdfFile = 'rules.pdf') + + if self._tutorial is not None: + self._vmod.addExternalFile(self._tutorial,'tutorial.vlog') + doc.addTutorial(name = 'Tutorial', + logfile = 'tutorial.vlog', + launchOnStartup = True) + + + fronts = self._categories.get('front',{}).get('all',[]) + front = list(fronts.values())[0] if len(fronts) > 0 else None + if front is not None: + v(f'Adding about page') + doc.addAboutScreen(title=f'About {self._title}', + fileName = front['filename']) + + return doc + + # ---------------------------------------------------------------- + def addKeybindings(self,doc): + keys = [ + ['Alt-A', '-', 'Show the charts panel'], + ['Alt-B', '-', 'Show the OOBs'], + ['Alt-C', '-', 'Show the counters panel'], + ['Alt-E', '-', 'Show the eliminated units'], + ['Alt-I', '-', 'Show/refresh inventory window'], + ['Alt-M', '-', 'Show map'], + ['Alt-T', '-', 'Increase turn track'], + ['Alt-Shift-T', '-', 'Decrease turn track'], + ['Alt-6', '-', 'Roll the dice'], + ['Ctrl-D', 'Board,Counter','Delete counters'], + ['Ctrl-E', 'Board,Counter','Eliminate counters'], + ['Ctrl-F', 'Board,Counter','Flip counters'], + ['Ctrl-M', 'Board,Counter','Toggle "moved" markers'], + ['Ctrl-O', 'Board', 'Hide/show counters'], + ['Ctrl-R', 'Board,Counter','Restore unit'], + ['Ctrl-T', 'Board,Counter','Toggle move trail'], + ['Ctrl-Z', 'Board', 'Undo last move'], + ['Ctrl-+', 'Board', 'Zoom in'], + ['Ctrl--', 'Board', 'Zoom out'], + ['Ctrl-=', 'Board', 'Select zoom'], + ['Ctrl-Shift-O', 'Board','Show overview map'], + ['←,→,↑↓','Board', + 'Scroll board left, right, up, down (slowly)'], + ['PnUp,PnDn','Board', 'Scroll board up/down (fast)'], + ['Ctrl-PnUp,Ctrl-PnDn','Board', 'Scroll board left/right (fast)'], + ['Mouse-scroll up/down', 'Board', 'Scroll board up//down'], + ['Shift-Mouse-scroll up/down','Board','Scroll board right/leftown'], + ['Ctrl-Mouse-scroll up/down','Board','Zoom board out/in'], + ['Mouse-2', 'Board', 'Centre on mouse']] + if self._battleMarks: + for a,l in zip(['Ctrl-D','Ctrl-Shift-O', 'Ctrl-+', 'Ctrl-+'], + [['Ctrl-C', 'Counter', 'Clear battle'], + ['Ctrl-Shift-C','Board', 'Clear all battle'], + ['Ctrl-X', 'Board,Counter','Mark battle'], + ['Ctrl-Y', 'Board,Counter','Resolve battle'], + ]): + ks = [k[0] for k in keys] + didx = ks.index(a) + keys.insert(didx,l) + + self._vmod.addFile('help/keys.html', + Documentation.createKeyHelp( + keys, + title=self._title, + version=self._version)) + doc.addHelpFile(title='Key bindings',fileName='help/keys.html') + + # ---------------------------------------------------------------- + def addNatoPrototypes(self,prototypes): + # Add unit categories as prototypes + for n,c in zip(['Type','Echelon','Command'], + [self._mains, self._echelons, self._commands]): + sc = set([cc.strip() for cc in c]) + with VerboseGuard(f'Adding prototypes for "{n}"') as vv: + for i,cc in enumerate(sc): + cc = cc.strip() + if len(cc) <= 0: continue + vv(f'[{cc}] ',end='',flush=True,noindent=True) + p = prototypes.addPrototype(name = f'{cc} prototype', + description = '', + traits = [MarkTrait(n,cc), + BasicTrait()]) + vv('') + + # ---------------------------------------------------------------- + def addBattleControlPrototype(self,prototypes): + # Control of battles. + # + # This has traits to + # + # - Zero battle counter + # - Increment battle counter + # - Set current battle number + # - Mark battle + # - Calculate odds + # + # When wgMarkBattle is issued to this piece, then + # + # - Increment battle counter + # - Set global current battle + # - Trampoline to GCK markBattle + # - For all selected pieces, issue markBattle + # - All wgBattleUnit pieces then + # - Get current battle # and store + # - Add marker on top of it self + # - Issue calculateOddsAuto + # - If auto odds on, go to calcOddsStart, + # - Trampoline to GCK calcOddsAuto + # - Which sends calcOddsStart to all markers + # - For each mark + # - Set current battle to current global + # - Trampoline calcOdds via GKC + # - Send calcBattleOdds to wgBattleCalc + # - Zero odds + # - Calculate fraction + # - Zero fraction + # - Calculate total AF + # - Zero AF + # - via trampoline to GKC + # - Calculate total DF + # - Zero DF + # - via trampoline to GKC + # - Real fraction calculation + # - From calculate fraction + # - Access via calculate trait + # - Calculate shift + # - Zero shift + # - Trampoline to GKC + # - Access via calculate trait + # - Calculate index + # - Via calculated OddsIndex + # - Calculate odds real + # - Via calculated Index to odds + # - Do markOddsAuto which selects between odds + # - Do markOddsReal+OddsIndex + # - Set global battle # + # - Place marker + # - Take global battle # + # - De-select all other marks to prevent + # further propagation + # + if len(self._battleMarks) <= 0: + return False + + n = len(self._battleMarks) + # --- Battle counter control - reset and increment ----------- + traits = [ + GlobalPropertyTrait( + ['',self._zeroBattle,GlobalPropertyTrait.DIRECT,'{0}'], + ['',self._incrBattle,GlobalPropertyTrait.DIRECT, + f'{{({self._battleCounter}%{n})+1}}'], + name = self._battleCounter, + numeric = True, + min = 0, + max = n, + wrap = True, + description = 'Zero battle counter', + ), + GlobalPropertyTrait( + ['',self._setBattle,GlobalPropertyTrait.DIRECT, + f'{{{self._battleCounter}}}'], + name = self._currentBattle, + numeric = True, + min = 0, + max = n, + wrap = True, + description = 'Zero battle counter', + ), + ReportTrait(self._zeroBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": zero battle counter: "' + f'+{self._battleCounter}):""}}')), + ReportTrait(self._incrBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": ' + f'increment battle counter: "' + f'+{self._battleCounter}):""}}')), + ReportTrait(self._setBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": set current battle: "+' + f'{self._battleCounter}+" -> "+' + f'{self._currentBattle}):""}}')), + GlobalHotkeyTrait(name = '', + key = self._markBattle+'Trampoline', + globalHotkey = self._markBattle, + description = 'Mark selected for battle'), + ReportTrait(self._markBattle+'Trampoline', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": forward mark battle: "+' + f'{self._battleCounter}):""}}')), + GlobalHotkeyTrait(name = '', + key = self._calcBattleOdds+'Start', + globalHotkey = self._calcBattleOdds+'Auto', + description = 'Trampoline to global'), + ReportTrait(self._calcBattleOdds+'Start', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": forward odds: "+' + f'{self._battleCounter}):""}}')), + DeselectTrait(command = '', + key = self._calcBattleOdds+'Deselect', + deselect = DeselectTrait.ALL), + ReportTrait(self._calcBattleOdds+'Deselect', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": select only this: "+' + f'{self._battleCounter}):""}}')), + TriggerTrait(command = '', + key = self._calcBattleOdds+'Auto', + actionKeys = [self._calcBattleOdds+'Start'], + property = f'{{{self._autoOdds}==true}}'), + ReportTrait(self._calcBattleOdds+'Auto', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": forward odds: "+' + f'{self._battleCounter}):""}}')), + TriggerTrait(command = '', + key = self._markBattle, + actionKeys = [self._incrBattle, + self._setBattle, + self._markBattle+'Trampoline', + self._calcBattleOdds+'Auto']), + ReportTrait(self._markBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": mark battle: "+' + f'{self._battleCounter}):""}}')), + GlobalHotkeyTrait(name = '', + key = self._clearAllBattle+'Trampoline', + globalHotkey = self._clearAllBattle, + description = 'Mark selected for battle'), + TriggerTrait(command = '', + key = self._clearAllBattle, + actionKeys = [self._clearAllBattle+'Trampoline', + self._zeroBattle]), + ReportTrait(self._clearBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": clear battle: "+' + f'{self._battleCounter}):""}}')), + GlobalHotkeyTrait(name = '', + key = self._clearMoved+'Trampoline', + globalHotkey = self._clearMoved, + description = 'Clear moved markers'), + MarkTrait(name=self._battleCtrl,value=True), + BasicTrait()] + prototypes.addPrototype(name = self._battleCtrl, + description = '', + traits = traits) + return True + + # ---------------------------------------------------------------- + def addBattleCalculatePrototype(self,prototypes): + # --- Batttle AF, DF, Odds ----------------------------------- + # This calculate odds derivation from stated odds. + calcIdx = 0 + maxIdx = len(self._oddsMarks)+1 + minIdx = 0 + idx2Odds = '""' + calcFrac = 1 + if len(self._oddsMarks) > 0: + odds = [o.replace('odds marker','').strip() for + o in self._oddsMarks] + ratios = all([':' in o for o in odds]) + if ratios: # All is ratios! + def calc(s): + num, den = [float(x.strip()) for x in s.split(':')] + return num/den + ratios = [[calc(s),s] for s in odds] + ind = [i[0] for i in sorted(enumerate(ratios), + key=lambda x:x[1][0])] + #print(f'=== Rations: {ratios}, Index: {ind[::-1]}') + calcIdx = ':'.join([f'{self._battleFrac}>={ratios[i][0]}?' + f'({i+1})' + for i in ind[::-1]]) + ':0' + idx2Odds = ':'.join([f'OddsIndex=={i+1}?' + f'"{ratios[i][1]}"' + for i in ind[::-1]]) + ':""' + calcFrac = (f'{{{self._battleDF}==0?0:' + f'(((double)({self._battleAF}))' + fr'\/{self._battleDF})}}') + #print(calcIdx,idx2Odds) + else: + try: + nums = [[int(o),o] for o in odds] + calcFrac = f'{{{self._battleAF}-{self._battleDF}}}' + ind = [i[0] for i in sorted(enumerate(nums), + key=lambda x:x[1])] + calcIdx = ':'.join([f'{self._battleFrac}>={nums[i][0]}?' + f'({i+1})' + for i in ind[::-1]])+':0' + idx2Odds = ':'.join([f'OddsIndex=={i+1}?"{nums[i][1]}"' + for i in ind[::-1]]) + ':""' + vidx2Odds = '\t'+idx2Odds.replace(':',':\n\t') + #print(f'Index to odds: {vidx2Odds}') + except: + pass + + traits = [ + CalculatedTrait(# This should be changed to game rules + name = 'OddsShift', + expression = f'{{{self._battleShift}}}', + description = 'Calculated internal oddsshift'), + CalculatedTrait(# This should be changed to game rules + name = 'OddsIndexRaw', + expression = f'{{{calcIdx}}}', + description = 'Calculated internal odds index'), + CalculatedTrait(# This should be changed to game rules + name = 'OddsIndexLimited', + expression = (f'{{OddsIndexRaw>{maxIdx}?{maxIdx}:' + f'OddsIndexRaw<{minIdx}?{minIdx}:' + f'OddsIndexRaw}}'), + description = 'Calculated internal limited odds index'), + CalculatedTrait(# This should be changed to game rules + name = 'OddsIndex', + expression = (f'{{OddsIndexLimited+OddsShift}}'), + description = 'Calculated internal odds index (with shift)'), + CalculatedTrait(# This should be changed to game rules + name = 'BattleFraction', + expression = calcFrac, + description = 'Calculated fraction off battle'), + GlobalPropertyTrait( + ['',self._zeroBattleShft,GlobalPropertyTrait.DIRECT,'{0}'], + name = self._battleShift, + numeric = True, + description = 'Zero battle odds shift', + ), + GlobalPropertyTrait( + ['',self._zeroBattleAF,GlobalPropertyTrait.DIRECT,'{0}'], + name = self._battleAF, + numeric = True, + description = 'Zero battle AF', + ), + GlobalPropertyTrait( + ['',self._zeroBattleDF,GlobalPropertyTrait.DIRECT,'{0}'], + name = self._battleDF, + numeric = True, + description = 'Zero battle AF', + ), + # {wgBattleDF==0?0:(double(wgBattleAF)/wgBattleDF)} + GlobalPropertyTrait( + ['',self._zeroBattleFrac,GlobalPropertyTrait.DIRECT,'{0}'], + ['',self._calcBattleFrac+'Real',GlobalPropertyTrait.DIRECT, + '{BattleFraction}'], + name = self._battleFrac, + description = 'Calculate battle fraction', + ), + GlobalPropertyTrait( + ['',self._zeroBattleIdx,GlobalPropertyTrait.DIRECT,'{0}'], + ['',self._calcBattleIdx,GlobalPropertyTrait.DIRECT, + '{OddsIndex}'], + name = self._battleIdx, + description = 'Calculate battle odds index', + ), + GlobalPropertyTrait( + ['',self._zeroBattleOdds,GlobalPropertyTrait.DIRECT,'{""}'], + ['',self._calcBattleOdds+'Real',GlobalPropertyTrait.DIRECT, + f'{{{idx2Odds}}}'], + name = self._battleOdds, + description = 'Calculate battle odds', + ), + GlobalHotkeyTrait(name = '',# Forward to units + key = self._calcBattleAF+'Trampoline', + globalHotkey = self._calcBattleAF, + description = 'Calculate total AF'), + GlobalHotkeyTrait(name = '',# Forward to units + key = self._calcBattleDF+'Trampoline', + globalHotkey = self._calcBattleDF, + description = 'Calculate total DF'), + GlobalHotkeyTrait(name = '',# Forward to units + key = self._calcBattleShft+'Trampoline', + globalHotkey = self._calcBattleShft, + description = 'Calculate total shift'), + TriggerTrait(command = '', + key = self._calcBattleAF, + actionKeys = [self._zeroBattleAF, + self._calcBattleAF+'Trampoline']), + TriggerTrait(command = '', + key = self._calcBattleDF, + actionKeys = [self._zeroBattleDF, + self._calcBattleDF+'Trampoline']), + TriggerTrait(command = '', + key = self._calcBattleShft, + actionKeys = [self._zeroBattleShft, + self._calcBattleShft+'Trampoline']), + TriggerTrait(command = '', + key = self._calcBattleFrac, + actionKeys = [self._zeroBattleFrac, + self._calcBattleAF, + self._calcBattleDF, + self._calcBattleFrac+'Real']), + TriggerTrait(command = '', + key = self._calcBattleOdds, + actionKeys = [self._zeroBattleOdds, + self._calcBattleFrac, + self._calcBattleShft, + self._calcBattleIdx, + self._calcBattleOdds+'Real']), + ReportTrait(self._zeroBattleAF, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Reset AF: "+' + f'{self._battleAF}):""}}')), + ReportTrait(self._zeroBattleDF, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Reset DF: "+' + f'{self._battleDF}):""}}')), + ReportTrait(self._zeroBattleFrac, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Reset fraction: "+' + f'{self._battleFrac}):""}}')), + ReportTrait(self._zeroBattleOdds, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Reset odds: "+' + f'{self._battleOdds}):""}}')), + ReportTrait(self._calcBattleAF, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Total AF: "+' + f'{self._battleAF}):""}}')), + ReportTrait(self._calcBattleDF, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Total DF: "+' + f'{self._battleDF}):""}}')), + ReportTrait(self._calcBattleShft, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle odds shift: "+' + f'{self._battleShift}):""}}')), + ReportTrait(self._calcBattleFrac, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle fraction: "+' + f'{self._battleFrac}):""}}')), + ReportTrait(self._calcBattleOdds, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle odds: "+' + f'{self._battleOdds}+" ("+' + f'{self._battleIdx}+")"):""}}')), + ReportTrait(self._calcBattleFrac+'Real', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle fraction: "+' + f'{self._battleFrac}+' + f'" AF="+{self._battleAF}+' + f'" DF="+{self._battleDF}' + f'):""}}')), + ReportTrait(self._calcBattleOdds+'Real', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle odds: "+' + f'{self._battleOdds}+' + f'" ("+{self._battleIdx}+","+OddsShift+","+' + f'" raw="+OddsIndexRaw+","+' + f'" limited="+OddsIndexLimited+","+' + f'" -> "+OddsIndex+","+' + f'{self._battleShift}+")"+' + f'" Fraction="+{self._battleFrac}+' + f'" AF="+{self._battleAF}+' + f'" DF="+{self._battleDF}' + f'):""}}')), + ReportTrait(self._calcBattleOdds+'Real', + report=(f'{{{self._verbose}?' + f'("! Battle # "' + f'+{self._battleNo}' + f'+" AF="+{self._battleAF}' + f'+" DF="+{self._battleDF}' + f'+" => "+{self._battleOdds}' + # f'+" "' + f'):""}}')), + MarkTrait(name=self._battleCalc,value=True), + BasicTrait()] + prototypes.addPrototype(name = self._battleCalc, + description = '', + traits = traits) + + # ---------------------------------------------------------------- + def addBattleUnitPrototype(self,prototypes): + # --- Battle units that set battle markers ------------------- + # + # - Trait to add battle number 1 to max + # + # - Trigger trait for each of these using the global property + # for the current battle + # + traits = [ + DynamicPropertyTrait(['',self._getBattle, + DynamicPropertyTrait.DIRECT, + f'{{{self._currentBattle}}}'], + ['',self._clearBattle, + DynamicPropertyTrait.DIRECT, + f'{{-1}}'], + name = self._battleNo, + numeric = True, + value = f'{{-1}}', + description = 'Set battle number'), + GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT, + '{IsAttacker}'], + name = self._currentAttacker, + numeric = True, + description = 'Set attacker'), + ReportTrait(self._getBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" current battle # "+' + f'{self._currentBattle}+" -> "+' + f'{self._battleNo}):""}}')), + ReportTrait(self._clearBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" Clear this global="+' + f'{self._currentBattle}+" this="+' + f'{self._battleNo}):""}}')), + ] + place = [] + trig = [] + rept = [] + for i, bm in enumerate(self._battleMarks): + kn = self._markBattle+str(i+1) + path = PlaceTrait.SKEL_PATH.format('BattleMarkers',bm) + + place.append( + PlaceTrait(command = '',#f'Add battle marker {i}', + key = kn, + markerSpec = path, + markerText = 'null', + xOffset = -8, + yOffset = -16, + matchRotation = False, + afterKey = self._getBattle, + gpid = self._game.nextPieceSlotId(), + description = f'Add battle marker {i+1}', + placement = PlaceTrait.ABOVE, + above = False)) + trig.append( + TriggerTrait(command = '',#Mark battle', + key = self._markBattle, + actionKeys = [self._getBattle, + self._setBattle,kn], + property = f'{{{self._currentBattle}=={i+1}}}')) + rept.append( + ReportTrait(kn, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" placing marker ({i+1})' + f' ="+ {self._currentBattle}+"' + f'={kn}"):""}}'))) + + oth = [ + GlobalHotkeyTrait(name = 'Declare battle', + key = self._markKey, + globalHotkey = self._markKey, + description = 'Mark for combat'), + GlobalPropertyTrait( + ['',self._calcBattleAF,GlobalPropertyTrait.DIRECT, + f'{{EffectiveAF+{self._battleAF}}}'], + name = self._battleAF, + numeric = True, + description = 'Update battle AF'), + GlobalPropertyTrait( + ['',self._calcBattleDF,GlobalPropertyTrait.DIRECT, + f'{{EffectiveDF+{self._battleDF}}}'], + name = self._battleDF, + numeric = True, + description = 'Update battle AF'), + GlobalPropertyTrait( + ['',self._calcBattleShft,GlobalPropertyTrait.DIRECT, + f'{{OddsShift}}'], + name = self._battleShift, + numeric = True, + description = 'Update battle shift', + ), + CalculatedTrait(#This could be redefined in module + name = 'EffectiveAF', + expression = '{CF}', + description = 'Current attack factor'), + CalculatedTrait(#This could be redefined in module + name = 'EffectiveDF', + expression = '{DF}', + description = 'Current defence factor'), + CalculatedTrait(#This could be redefined in module + name = 'IsAttacker', + expression = '{Phase.contains(Faction)}', + description = 'Check if current phase belongs to faction'), + CalculatedTrait(#This could be redefined in module + name = 'OddsShift', + expression = f'{{{self._battleShift}}}', + description = 'Check if current phase belongs to faction'), + ReportTrait(self._calcBattleAF, + report=(f'{{{self._verbose}?' + f'("! "+BasicName+' + f'" add "+EffectiveAF+' + f'" to total attack factor -> "+' + f'{self._battleAF}' + f'):""}}')), + ReportTrait(self._calcBattleDF, + report=(f'{{{self._verbose}?' + f'("! "+BasicName+' + f'" add "+EffectiveDF+' + f'" to total defence factor -> "+' + f'{self._battleDF}' + f'):""}}')), + ReportTrait(self._calcBattleShft, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+' + f'" Updating odds shift with "+OddsShift+' + f'" -> "+{self._battleShift}):""}}')), + ] + traits.extend( + place+ + trig+ + oth+ + [MarkTrait(name=self._battleUnit,value=True), + BasicTrait()]) + prototypes.addPrototype(name = self._battleUnit, + description = '', + traits = traits) + # ---------------------------------------------------------------- + def addBattleCorePrototype(self,prototypes): + # --- Core traits for battle markers (number, odds, results) + # - Set the global current battle number + # - Get the current global battle number + # - Clear this counter + # - Trampoline to global command to clear all marks for this battle + traits = [ + # NoStackTrait(select = NoStackTrait.NORMAL_SELECT, + # move = NoStackTrait.NORMAL_MOVE, + # canStack = False, + # ignoreGrid = False), + GlobalPropertyTrait(['',self._setBattle,GlobalPropertyTrait.DIRECT, + f'{{{self._battleNo}}}'], + name = self._currentBattle, + numeric = True, + description = 'Set current battle'), + GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT, + '{IsAttacker}'], + name = self._currentAttacker, + numeric = True, + description = 'Set attacker'), + DynamicPropertyTrait(['',self._getBattle, + DynamicPropertyTrait.DIRECT, + f'{{{self._currentBattle}}}'], + name = self._battleNo, + numeric = True, + value = f'{{{self._battleNo}}}', + description = 'Set battle number'), + DynamicPropertyTrait(['',self._getBattle, + DynamicPropertyTrait.DIRECT, + f'{{{self._currentAttacker}}}'], + name = 'IsAttacker', + numeric = True, + value = 'false', + description = 'Set attacker'), + DeleteTrait('',self._clearBattle), + GlobalHotkeyTrait(name = '', + key = self._clearBattle+'Trampo', + globalHotkey = self._clearBattle, + description = 'Clear selected battle'), + TriggerTrait(command = 'Clear', + key = self._clearKey, + actionKeys = [self._setBattle, + self._clearBattle+'Trampo']), + ReportTrait(self._setBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" battle # "+' + f'{self._battleNo}+" -> "+' + f'{self._currentBattle}):""}}')), + ReportTrait(self._getBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" current battle # "+' + f'{self._currentBattle}+" -> "+' + f'{self._battleNo}):""}}')), + ReportTrait(self._clearBattle, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" Clear this global="+' + f'{self._currentBattle}+" this="+' + f'{self._battleNo}):""}}')), + ReportTrait(self._clearKey, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" To clear battle # global="+' + f'{self._currentBattle}+" this="+' + f'{self._battleNo}):""}}')), + ReportTrait(self._clearBattle+'Trampo', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" ' + f'Forward clear battle # global="+' + f'{self._currentBattle}+" this="+' + f'{self._battleNo}):""}}')), + MarkTrait(name=self._battleMark,value=True), + BasicTrait() + ] + prototypes.addPrototype(name = self._currentBattle, + description = '', + traits = traits) + + # ---------------------------------------------------------------- + def addBattlePrototypes(self,prototypes): + if not self.addBattleControlPrototype(prototypes): + return + + self.addBattleCalculatePrototype(prototypes) + self.addBattleUnitPrototype(prototypes) + self.addBattleCorePrototype(prototypes) + + # ---------------------------------------------------------------- + def markerTraits(self): + return [DeleteTrait(), + RotateTrait()] + + # ---------------------------------------------------------------- + def battleMarkerTraits(self,c): + '''Derives from the CurrentBattle prototype and adds a submenu + to place odds counter on the battle marker''' + traits = [PrototypeTrait(name=self._currentBattle), + NonRectangleTrait(filename = c['filename'], + image = c['img'])] + + subs = [] + ukeys = [] + place = [] + trig = [] + rept = [] + repp = [] + for i, odds in enumerate(self._oddsMarks): + on = odds.replace('odds marker','').strip() + om = odds.replace(':',r'\:') + kn = self._markOdds+str(i+1) + gpid = self._game.nextPieceSlotId() + path = PlaceTrait.SKEL_PATH.format('OddsMarkers',om) + subs.append(on) + + place.append( + PlaceTrait(command = '', + key = kn, + markerSpec = path, + markerText = 'null', + xOffset = -6, + yOffset = -8, + matchRotation = False, + afterKey = self._getBattle+'Details', + gpid = gpid, + placement = PlaceTrait.ABOVE, + description = f'Add odds marker {on}')) + trig.append( + TriggerTrait(name = '', + command = on, + key = kn+'real', + actionKeys = [ + self._setBattle, + kn])) + rept.append( + ReportTrait(kn+'real', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Set odds ' + f'{on} ({kn})"):""}}'))) + repp.append( + ReportTrait(kn, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Place odds ' + f'{on} ({kn})"):""}}'))) + ukeys.append(kn+'real') + + auto = [] + auton = [] + if len(self._oddsMarks) > 0: + auton = ['Auto'] + for i, odds in enumerate(self._oddsMarks): + trig.append( + TriggerTrait(name = '', + command = '', + key = self._markOdds+'Auto', + property = f'{{{self._battleIdx}=={i+1}}}', + actionKeys = [self._markOdds+str(i+1)])) + + auto = [GlobalHotkeyTrait(name = '', + key = self._calcBattleOdds, + globalHotkey = self._calcBattleOdds, + description = 'Calculate fraction'), + DeselectTrait(command = '', + key = self._calcBattleOdds+'Deselect', + deselect = DeselectTrait.ONLY), + ReportTrait(self._calcBattleOdds+'Deselect', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Select only this "' + f'+" Attacker="+IsAttacker' + f'):""}}')), + TriggerTrait(name = '', + command = '', + key = self._markOdds+'Trampoline', + actionKeys = [ + self._calcBattleOdds, + self._markOdds+'Auto', + self._calcBattleOdds+'Deselect'], + property = f'{{!IsAttacker}}' + ), + TriggerTrait(name = '', + command = 'Auto', + key = self._calcBattleOdds+'Start', + actionKeys = [ + self._setBattle, + self._markOdds+'Trampoline', + ]), + ReportTrait(self._calcBattleOdds+'Start', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle odds "+' + f'{self._battleOdds}):""}}')), + ReportTrait(self._markOdds+'Auto', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": ' + f'Auto battle odds "+' + f'{self._battleOdds}):""}}')) + ] + + traits.extend([ + RestrictCommandsTrait( + name='Hide when auto-odds are enabled', + hideOrDisable = RestrictCommandsTrait.HIDE, + expression = f'{{{self._autoOdds}==true}}', + keys = ukeys)]+ + place + +trig + +auto + +rept + +repp) + if len(subs) > 0: + traits.extend([ + SubMenuTrait(subMenu = 'Odds', + keys = auton+subs), + ]) + + return traits + + # ---------------------------------------------------------------- + def oddsMarkerTraits(self,c=None): + '''Derives from the CurrentBattle prototype and adds a submenu + to replace odds counter with result marker''' + traits = [PrototypeTrait(name=self._currentBattle), + NonRectangleTrait(filename = c['filename'], + image = c['img']), + DynamicPropertyTrait( + ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT, + (f'{{{self._battleAF}+" vs "+{self._battleDF}+' + f'" (odds "+{self._battleOdds}+" shift "+' + f'{self._battleShift}+")"}}')], + name = 'BattleDetails', + value = '', + numeric = False, + description = 'Stored battle details'), + TriggerTrait(command = '', + key = self._getBattle+'Details', + actionKeys = [self._getBattle, + self._getBattle+'More'])] + + subs = [] + place = [] + trig = [] + rept = [] + ukeys = [] + first = '' + for i, result in enumerate(self._resultMarks): + r = result.replace('result marker','').strip() + kn = self._markResult+str(i+1) + gpid = self._game.nextPieceSlotId() + ukeys.append(kn+'real') + subs.append(r) + if first == '': first = r + + path = PlaceTrait.SKEL_PATH.format('ResultMarkers',result) + + place.append( + ReplaceTrait(command = '', + key = kn, + markerSpec = path, + markerText = 'null', + xOffset = -6, + yOffset = -8, + matchRotation = False, + afterKey = self._getBattle, + gpid = gpid, + description = f'Add result marker {r}')) + trig.append( + TriggerTrait(name = '', + command = r, + key = kn+'real', + actionKeys = [ + self._setBattle, + kn])) + rept.append( + ReportTrait(kn+'real', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+" setting result ' + f'{r}"):""}}'))) + + auto = [] + auton = [] + if len(self._resultMarks) > 0: + auton = ['Auto'] + for i, res in enumerate(self._resultMarks): + r = res.replace('result marker','').strip() + trig.append( + TriggerTrait( + name = '', + command = '', + key = self._markResult+'Auto', + property = f'{{{self._battleResult}=="{r}"}}', + actionKeys = [self._markResult+str(i+1)])) + + auto = [ # Override in the module + CalculatedTrait( + name = 'Die', + expression = '{GetProperty("1d6_result")}', + description = 'Die roll'), + GlobalHotkeyTrait( + name = '', + key = self._rollDice, + globalHotkey = self._diceKey, + description = 'Roll dice'), + CalculatedTrait( + name = 'BattleResult', + expression = f'{{"{first}"}}', + ), + GlobalPropertyTrait( + ['',self._calcBattleRes+'real',GlobalPropertyTrait.DIRECT, + '{BattleResult}'], + name = self._battleResult, + numeric = False, + description = 'Set combat result'), + TriggerTrait(name = '', + command = 'Resolve', + key = self._resolveKey, + property = f'{{{self._autoResults}==true}}', + actionKeys = [ + self._setBattle, + self._rollDice, + self._calcBattleRes+'real', + self._markResult+'Auto', + ]), + ReportTrait(self._calcBattleRes, + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Battle result "+' + f'{self._battleOdds}):""}}')), + ReportTrait(self._markResult+'Auto', + report=(f'{{{self._debug}?' + f'("~ "+BasicName+": Auto battle result "+' + f'{self._battleResult}):""}}')), + ReportTrait(self._markResult+'Auto', + report=(f'{{"` Battle # "+{self._battleNo}+": "+' + f'BattleDetails+' + f'" with die roll "+Die+": "+' + f'{self._battleResult}' + # f'+ ""' + f'}}')), + MarkTrait(name=self._battleOddsM,value='true') + ] + + traits.extend( + [RestrictCommandsTrait( + name='Hide when auto-results are enabled', + hideOrDisable = RestrictCommandsTrait.HIDE, + expression = f'{{{self._autoResults}==true}}', + keys = ukeys)]+ + place + +trig + +auto) + + if len(subs) > 0: + traits.append(SubMenuTrait(subMenu = 'Result', + keys = subs)) + + return traits + + # ---------------------------------------------------------------- + def resultMarkerTraits(self,c=None): + traits = [PrototypeTrait(name=self._currentBattle), + NonRectangleTrait(filename = c['filename'], + image = c['img'])] + + return traits + + # ---------------------------------------------------------------- + def factionTraits(self,faction): + traits = [ReportTrait(self._eliminateKey, + self._restoreKey, + self._trailKey), + TrailTrait(), + RotateTrait(), + MovedTrait(xoff = int( 36 * self._counterScale), + yoff = int(-38 * self._counterScale)), + DeleteTrait(), + SendtoTrait(mapName = 'DeadMap', + boardName = f'{faction} pool', + name = 'Eliminate', + key = self._eliminateKey, + restoreName = 'Restore', + restoreKey = self._restoreKey, + description = 'Eliminate unit'), + PrototypeTrait(name=self._battleUnit), + MarkTrait(name='Faction',value=faction)] + + return traits + + # ---------------------------------------------------------------- + def getFactors(self,val): + cf = None + mf = None + df = None + ra = None + try: + if 'chit 1 factor' in val: + vv = val.replace('chit 1 factor=','') + cf = int(vv) + elif 'chit 2 factors artillery' in val: + vv = val.replace('chit 2 factors artillery=','') + cf,mf,ra = [int(v) for v in vv.strip('=').split()] + elif 'chit 2 factors' in val: + vv = val.replace('chit 2 factors=','') + cf,mf = [int(v) for v in vv.split()] + elif 'chit 3 factors' in val: + vv = val.replace('chit 3 factors=','') + cf,df,mf = [int(v) for v in vv.split()] + + # Set defensive factor combat factor if not defined. + if df is None and cf is not None: + df = cf + + + except Exception as e: + print(f'\nWarning when extracting factors: {e} ' + f'in "{val}" -> "{vv}"') + return None,None,None,None + pass - Returns - ------- - ''' - from zipfile import ZipFile, ZIP_DEFLATED - from pprint import pprint - - categories = {} - unittypes = [] - echelons = [] - commands = [] - with ZipFile(vmodname,'w',compression=ZIP_DEFLATED) as vmod: - # Create images in ZIP file - for i in info: - if ignore_entry(i): continue - - # Store the name of the image file in catalogue - i['filename'] = i['name'].replace(' ','_') + '.png' - imgfn = 'images/'+i['filename'] - if imgfn not in vmod.namelist(): - vmod.writestr(imgfn,i['img']) - - # Categorize - assume counter - typ = i.get('category','counter') - sub = i.get('subcategory','all') - if sub == '': - i['subcategory'] = 'all' - sub = 'all' - - # Add to catalogue - if typ not in categories: - categories[typ] = {} - cat = categories[typ] - - if sub not in cat: - cat[sub] = {} - tgt = cat[sub] - - tgt[i['name']] = i - - natoapp6c = i.get('natoapp6c',None) - if natoapp6c is not None: - from re import sub - mains = natoapp6c.get('main',None) - echelon = natoapp6c.get('echelon',None) - command = natoapp6c.get('command',None) - - if mains is not None: - mains = sub(r'\[[^]]+\]','',mains)\ - .replace('{','').replace('}','').split(',') - unittypes.extend(mains) - i['mains'] = mains + return cf,df,mf,ra + + # ---------------------------------------------------------------- + def pieceTraits(self,subn,subc,cn,c): + from re import sub + + bb = self.getBB(c['img']) + height = bb[3]-bb[1] if bb is not None else 1 + width = bb[2]-bb[0] if bb is not None else 1 + cf = subc.get(cn + ' flipped', None) + traits = [PrototypeTrait(name=f'{subn} prototype')] + + def clean(s): + return s.strip().replace(',',' ').replace('/',' ').strip() + + if not self._nonato: + mains = c.get('mains','') + m = set([clean(mains)] + + [clean(m) for m in mains.split(',')]) + traits.extend([PrototypeTrait(name=f'{m.strip()} prototype') + for m in set(m)]) + for p in ['echelon','command']: + val = c.get(p,None) + if val is not None: + pv = f'{val.strip()} prototype' + traits.append(PrototypeTrait(name=pv)) - if echelon is not None: - echelons.append(natoapp6c['echelon']) - i['echelon'] = echelon - - if command is not None: - commands.append(command) - i['command'] = command - - # Friendly message - # if verbose: - # print(f'{i["name"]} -> {i["type"]}/{i["sub type"]} {typ}/{sub}') - - unittypes = set(unittypes) - echelons = set(echelons) - commands = set(commands) - - # Now create the `buildFile.xml` file - build, notes = create_build(info, - categories, - unittypes, - echelons, - commands, - vmod, - title, - version, - desc, - rules, - tutorial, - pdfname, - infoname, - verbose, - visible) - # Now create the `moduledata` file - data = create_data(title, version, desc,verbose) - - - if patch: - from importlib.util import spec_from_file_location, \ - module_from_spec - from pathlib import Path - from sys import modules - - p = Path(patch) - if verbose: - pn = str(p.stem) - print(f'Will patch module with {pn}.patch function') - - spec = spec_from_file_location(p.stem, p.absolute()) - module = module_from_spec(spec) - spec.loader.exec_module(module) - modules[p.stem] = module + if cf is not None: + traits.extend([ + LayerTrait(images = [c['filename'], + cf['filename']], + newNames = ['','Reduced +'], + activateName = '', + decreaseName = '', + increaseName = 'Flip', + increaseKey = self._flipKey, + decreaseKey = '', + name = 'Step'), + ReportTrait(self._flipKey)]) + + if not self._nochit: + def clean(value): + return sub(r'\[[^=]+\]=','',value)\ + .replace('{','')\ + .replace('}','')\ + .replace('/',' ')\ + .replace(',',' ')\ + .replace('\\',' ') + + # Add extra marks. This may be useful later on. + for field in ['upper left', 'upper right', + 'lower left', 'lower right', + 'left', 'right', + 'factors']: + value = c.get('chit',{}).get(field,None) + if value is None: + continue + val = clean(value) + val = val\ + .replace('chit identifier=','')\ + .replace('chit small identifier=','') + + traits.append(MarkTrait(name = field, value = val)) + + if field != 'factors': continue + + af, df, mf, ra = self.getFactors(val) + saf, sdf, smf, sra = None,None,None,None + if cf is not None: + value = cf.get('chit',{}).get(field,None) + if value is not None: + val = clean(value) + val = val\ + .replace('chit identifier=','')\ + .replace('chit small identifier=','') + saf, sdf, smf, sra = self.getFactors(val) + + rf = [] + srf = [] + for f,sf,n in [[af,saf,'CF'], + [df,sdf,'DF'], + [mf,smf,'MF'], + [ra,sra,'Range']]: + if f is None: continue + + if sf is None: + rf.append(MarkTrait(name=n,value=f)) + else: + rf .append(MarkTrait(name='Full'+n, value=f)) + srf.append(MarkTrait(name='Reduced'+n,value=sf)) + traits.append(CalculatedTrait( + name = n, + expression = (f'{{(Step_Level==2)?' + f'Reduced{n}:Full{n}}}'))) + + traits.extend(rf+srf) + + - # Patch must accept xml.dom.document,xml.dom.document,ZipFile - module.patch(build,data,vmod,verbose) - - buildstr = build.toprettyxml(indent=' ', - encoding="UTF-8", - standalone=False) - datastr = data.toprettyxml(indent=' ', - encoding="UTF-8", - standalone=False) + + + return height, width, traits + + # ---------------------------------------------------------------- + def addCounters(self): + '''Add all counters (pieces) element to the module. + Prototypes are also created as part of this. + ''' + from re import sub + + with VerboseGuard('Adding counters') as v: + protos = self._game.addPrototypes() + + self.addNatoPrototypes(protos) + self.addBattlePrototypes(protos) + + pieces = self._game.addPieceWindow(name = 'Counters', + icon = self.getIcon('unit-icon', + '/images/counter.gif'), + hotkey = self._countersKey) + tabs = pieces.addTabs(entryName='Counters') + + for subn, subc in self._categories.get('counter',{}).items(): + + subn = subn.strip() + panel = tabs.addPanel(entryName = subn, fixed = False) + plist = panel.addList(entryName = f'{subn} counters') + + traits = [] + if subn in ['BattleMarkers']: + traits = self.battleMarkerTraits(list(subc.values())[0]) + elif subn in ['OddsMarkers']: + traits = self.oddsMarkerTraits(list(subc.values())[0]) + elif subn in ['ResultMarkers']: + traits = self.resultMarkerTraits(list(subc.values())[0]) + elif subn.lower() in ['marker', 'markers']: + traits = self.markerTraits() + else: + traits = self.factionTraits(subn) + + traits.append(BasicTrait()) + + p = protos.addPrototype(name = f'{subn} prototype', + description = f'Prototype for {subn}', + traits = traits) + v('') + + with VerboseGuard(f'Adding pieces for "{subn}"') as vv: + for i, (cn, c) in enumerate(subc.items()): + if cn.endswith('flipped'): continue + + if i == 0: v('',end='') + vv(f'[{cn}',end='',flush=True,noindent=True) + + height, width, traits = self.pieceTraits(subn,subc,cn,c) + if cn == self._hiddenName: + traits = [ + PrototypeTrait(name=self._battleCtrl), + PrototypeTrait(name=self._battleCalc)] + if self._diceInit is not None: + traits.extend(self._diceInit) + traits.append( + RestrictAccessTrait(sides=[], + description='Fixed')) + + + #if cn.startswith('odds marker'): + # cn = cn.replace(':','_') + + gpid = self._game.nextPieceSlotId() + traits.extend([BasicTrait(name = c['name'], + filename = c['filename'], + gpid = gpid)]) + + ps = plist.addPieceSlot(entryName = cn, + gpid = gpid, + height = height, + width = width, + traits = traits) + if cn == self._hiddenName: + self._hidden = ps + vv('] ',end='',flush=True,noindent=True) - vmod.writestr('buildFile.xml',buildstr) - vmod.writestr('moduledata',datastr) + vv('') + - return notes + # ---------------------------------------------------------------- + def addNotes(self,**kwargs): + '''Add a `Notes` element to the module -# ==================================================================== -def export(vmodname = 'Draft.vmod', - pdfname = 'export.pdf', - infoname = 'export.json', - title = 'Test', - version = 'Draft', - desc = '', - rules = None, - tutorial = None, - patch = None, - verbose = False, - visible = False): - '''Do the whole thing + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + ''' + self._game.addNotes(**kwargs) + + # ---------------------------------------------------------------- + def addInventory(self,**kwargs): + '''Add a `Inventory` element to module + + Parameters + ---------- + kwargs : dict + Dictionary of attribute key-value pairs + ''' + filt = '{' + '||'.join([f'Faction=="{s}"' for s in self._sides])+'}' + grp = 'Faction,Command,Echelon,Type' + self._game.addInventory(include = filt, + groupBy = grp, + sortFormat = '$PieceName$', + tooltip ='Show inventory of all pieces', + zoomOn = True, + **kwargs) + + # ---------------------------------------------------------------- + def addBoard(self,name,info,hasFlipped=False): + '''Add a `Board` element to module + + Parameters + ---------- + name : str + Name of board + info : dict + Information on board image + hasFlipped : bool + True if any piece can be flipped + ''' + with VerboseGuard(f'Adding board {name}') as v: + map = self._game.addMap(mapName=name, + markUnmovedHotkey=self._clearMoved) + map.addCounterDetailViewer( + propertyFilter=f'{{{self._battleMark}!=true}}') + map.addHidePiecesButton() + map.addGlobalMap() + # Basics + map.addStackMetrics() + map.addImageSaver() + map.addTextSaver() + map.addForwardToChatter() + map.addMenuDisplayer() + map.addMapCenterer() + map.addStackExpander() + map.addPieceMover() + map.addKeyBufferer() + map.addSelectionHighlighters() + map.addHighlightLastMoved() + map.addZoomer() + + map.addMassKey(name='Eliminate', + buttonHotkey = self._eliminateKey, + hotkey = self._eliminateKey, + icon = self.getIcon('eliminate-icon', + '/icons/16x16/edit-undo.png'), + tooltip = 'Eliminate selected units') + map.addMassKey(name='Delete', + buttonHotkey = self._deleteKey, + hotkey = self._deleteKey, + icon = self.getIcon('delete-icon', + '/icons/16x16/no.png'), + tooltip = 'Delete selected units') + map.addMassKey(name='Rotate CW', + buttonHotkey = self._rotateCWKey, + hotkey = self._rotateCWKey, + icon = '', #/icons/16x16/no.png', + tooltip = 'Rotate selected units') + map.addMassKey(name='Rotate CCW', + buttonHotkey = self._rotateCCWKey, + hotkey = self._rotateCCWKey, + icon = '', #/icons/16x16/no.png', + tooltip = 'Rotate selected units') + map.addMassKey(name='Phase clear moved markers', + buttonHotkey = self._clearMoved+'Phase', + hotkey = self._clearMoved+'Trampoline', + canDisable = True, + target = '', + filter = f'{{{self._battleCtrl}==true}}', + propertyGate = f'{self._noClearMoves}', + icon = '', #/icons/16x16/no.png', + tooltip = 'Phase clear moved markers', + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Phase Clear moved markers "+' + f'{self._noClearMoves})' + f':""}}')) + if hasFlipped: + map.addMassKey(name='Flip', + buttonHotkey = self._flipKey, + hotkey = self._flipKey, + icon = self.getIcon('flip-icon', + '/images/Undo16.gif'), + tooltip = 'Flip selected units') + + if len(self._battleMarks) > 0: + v(f'Adding battle mark interface') + ctrlSel = f'{{{self._battleCtrl}==true}}' + oddsSel = f'{{{self._battleMark}==true}}' + calcSel = f'{{{self._battleCalc}==true}}' + curSel = (f'{{{self._battleNo}=={self._currentBattle}}}') + curAtt = (f'{{{self._battleNo}=={self._currentBattle}&&' + f'{self._battleUnit}==true&&' + f'IsAttacker==true}}') + curDef = (f'{{{self._battleNo}=={self._currentBattle}&&' + f'{self._battleUnit}==true&&' + f'IsAttacker==false}}') + curUnt = (f'{{{self._battleNo}=={self._currentBattle}&&' + f'{self._battleUnit}==true}}') + markSel = (f'{{{self._battleNo}=={self._currentBattle}&&' + f'{self._battleMark}==true}}') + + # ctrlSel = '{BasicName=="wg hidden unit"}' + map.addMassKey(name = 'User mark battle', + buttonHotkey = self._markKey, + buttonText = '', + hotkey = self._markBattle, + icon = 'battle-marker-icon.png', + tooltip = 'Mark battle', + target = '', + singleMap = False, + filter = ctrlSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'User marks battle # "+' + f'{self._currentBattle})' + f':""}}')) + map.addMassKey(name = 'Selected mark battle', + buttonHotkey = self._markBattle, + hotkey = self._markBattle, + icon = '', + tooltip = '', + singleMap = False, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Mark battle # "+' + f'{self._currentBattle})' + f':""}}')) + map.addMassKey(name = 'Clear current battle', + buttonText = '', + buttonHotkey = self._clearBattle, + hotkey = self._clearBattle, + icon = '', + tooltip = '', + target = '', + singleMap = False, + filter = curSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Clear battle # "+' + f'{self._currentBattle})' + f':""}}')) + map.addMassKey(name = 'Clear selected battle', + buttonText = '', + buttonHotkey = self._clearKey, + hotkey = self._clearKey, + icon = '', + tooltip = '', + singleMap = False, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Clear battle # "+' + f'{self._currentBattle})' + f':""}}')) + map.addMassKey(name = 'Clear all battles', + buttonText = '', + buttonHotkey = self._clearAllBattle, + hotkey = self._clearBattle, + icon = '', + tooltip = '', + target = '', + singleMap = False, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Clear all battle markers")' + f':""}}')) + map.addMassKey(name = 'User clear all battles', + buttonText = '', + buttonHotkey = self._clearAllKey, + hotkey = self._clearAllBattle, + icon = 'clear-battles-icon.png', + tooltip = 'Clear all battles', + target = '', + singleMap = False, + filter = ctrlSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'User clears battle markers")' + f':""}}')) + map.addMassKey(name = 'Phase clear all battles', + buttonText = '', + buttonHotkey = self._clearBattlePhs, + hotkey = self._clearAllBattle, + icon = '', + tooltip = 'Clear all battles', + canDisable = True, + propertyGate = f'{self._noClearBattles}', + target = '', + singleMap = False, + filter = ctrlSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Phase clears battle markers "+' + f'{self._noClearBattles})' + f':""}}')) + map.addMassKey(name = 'Selected resolve battle', + buttonHotkey = self._resolveKey, + hotkey = self._resolveKey, + icon = 'resolve-battles-icon.png', + tooltip = 'Resolve battle', + singleMap = False, + filter = oddsSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Resolve battle # "+' + f'{self._currentBattle})' + f':""}}')) + map.addMassKey(name = 'Sum AFs', + buttonText = '', + buttonHotkey = self._calcBattleAF, + hotkey = self._calcBattleAF, + icon = '', + tooltip = '', + target = '', + singleMap = False, + filter = curAtt, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Calculate total AF"):""}}')) + map.addMassKey(name = 'Sum DFs', + buttonText = '', + buttonHotkey = self._calcBattleDF, + hotkey = self._calcBattleDF, + icon = '', + tooltip = '', + target = '', + singleMap = False, + filter = curDef, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Calculate total DF"):""}}')) + map.addMassKey(name = 'Sum odds shifts', + buttonText = '', + buttonHotkey = self._calcBattleShft, + hotkey = self._calcBattleShft, + icon = '', + tooltip = '', + target = '', + singleMap = False, + filter = curUnt, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Calculate odds shift"):""}}')) + map.addMassKey(name = 'Calc battle odds', + buttonText = '', + buttonHotkey = self._calcBattleOdds, + hotkey = self._calcBattleOdds, + icon = '', + tooltip = '', + target = '', + singleMap = False, + filter = calcSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Calculate odds"):""}}')) + map.addMassKey(name = 'Auto calc battle odds', + buttonText = '', + buttonHotkey = self._calcBattleOdds+'Auto', + hotkey = self._calcBattleOdds+'Start', + icon = '', + tooltip = '', + # target = '', + singleMap = False, + filter = markSel, + reportFormat = (f'{{{self._debug}?' + f'("~ {name}: ' + f'Auto calculate odds"):""}}')) + + + ulx,uly,lrx,lry = self.getBB(info['img']) + width = abs(ulx - lrx) + height = abs(uly - lry) + width, height = self.getWH(info['img']) + height += 20 + width += 5 + + picker = map.addBoardPicker() + board = picker.addBoard(name = name, + image = info['filename'], + width = width, + height = height) + zoned = board.addZonedGrid() + zoned.addHighlighter() + + if not 'zones' in info: + full = zoned.addZone(name = full, + useParentGrid = False, + path=(f'{ulx},{uly};' + + f'{lrx},{uly};' + + f'{lrx},{lry};' + + f'{ulx},{lry}')) + grid = zone.addHexGrid(color = color, + dx = HEX_WIDTH, + dy = HEX_HEIGHT, + visible = self._visible) + grid.addNumbering(color = color, + hType = 'A', + hOff = -1, + vType = 'N', + vOff = -1, + visible = self._visible) + return + + w = abs(ulx-lrx) + h = abs(uly-lry) + self.addZones(zoned,name,info['zones'],w,h) + + if self._hidden is not None: + v(f'Adding hidden unit to map {name}') + at = map.addAtStart(name = self._hiddenName, + location = '', + useGridLocation = False, + owningBoard = name, + x = 0, + y = 0) + at.addPieces(self._hidden) + - - Read in information form the 'infoname' file (a CSV file - generated by LaTeX) + # ---------------------------------------------------------------- + def addDeadMap(self): + '''Add a "Dead Map" element to the module + ''' + name = 'DeadMap' + with VerboseGuard(f'Adding board {name}') as v: + map = self._game.addMap(mapName = name, + buttonName = '', + markMoved = 'Never', + launch = True, + icon = self.getIcon('pool-icon', + '/images/playerAway.gif'), + allowMultiple = True, + hotkey = self._deadKey) + # Basics + map.addStackMetrics() + map.addImageSaver() + map.addTextSaver() + map.addForwardToChatter() + map.addMenuDisplayer() + map.addMapCenterer() + map.addStackExpander() + map.addPieceMover() + map.addKeyBufferer() + map.addSelectionHighlighters() + map.addHighlightLastMoved() + map.addZoomer() + + map.addMassKey(name='Restore', + buttonHotkey = self._restoreKey, + hotkey = self._restoreKey, + icon = self.getIcon('restore-icon', + '/images/Undo16.gif'), + tooltip = 'Restore selected units') + + picker = map.addBoardPicker() + picker.addSetup(maxColumns=len(self._sides),mapName=name, + boardNames=[s+' pool' for s in self._sides]) + + for i, s in enumerate(self._sides): + v(f'Adding {s} pool') + color = [0,0,0,64] + color[i % 3] = 255 + w = 400 + h = 400 + c = rgba(*color) + img = '' + dimg = self._categories.get('pool',{}).get('all',{})\ + .get(s,None) + + if dimg: + bb = self.getBB(dimg['img']) + w = bb[2] - bb[0] + h = bb[3] - bb[1] + c = '' + img = dimg['filename'] + v(f'Using image provided by user {img}') + + board = picker.addBoard(name = f'{s} pool', + image = img, + width = w, + height = h, + color = c) + + if dimg is None or not 'zones' in dimg: + continue + + zoned = board.addZonedGrid() + zoned.addHighlighter() + w = abs(w) + h = abs(h) + self.addZones(zoned,board['name'],dimg['zones'],w,h) + + + # -------------------------------------------------------------------- + def getPictureInfo(self,picture,name,width,height): + ''' + Returns + ------- + hex_width, hex_height : float, float + Scale hex width + scx, scy : float, float, float, float + Scale to image and picture (x,y) + rot90 : bool + True if rotated +/-90 degrees + tran : callable + Translation function + ''' + if picture is None: + print(f'WARNING: No Tikz picture information.' + f"Are you sure you used the `[zoned]' option for the " + f"tikzpicture environment of {name}?") + f = lambda x,y: (x,y) + return HEX_WIDTH,HEX_HEIGHT,1,1,False,f + + # Get picture bounding box + tll = picture['lower left'] + tur = picture['upper right'] + # Get picture transformation + pa = picture['xx'] + pb = picture['xy'] + pc = picture['yx'] + pd = picture['yy'] + # Get picture offset (always 0,0?) + pdx = picture['dx'] + pdy = picture['dy'] + # Define picture global transformation + pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y) + # Globally transform (rotate) picture bounding box + pll = pr(*tll) + pur = pr(*tur) + # Calculate widht, height, and scaling factors + pw = pur[0] - pll[0] + ph = pur[1] - pll[1] + scw = width / pw + sch = height / ph + # Extract picture scales and rotation + # Courtesy of + # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix + from math import sqrt, atan2, degrees, isclose + psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1) + psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1) + prt = degrees(atan2(pc,pd)) + if not any([isclose(abs(prt),a) for a in [0,90,180,270]]): + raise RuntimeException('Rotations of Tikz pictures other than ' + '0 or +/-90,+/- 180, or +/-270 not supported. ' + 'found {prt}') + rot90 = int(prt // 90) + if rot90 == 2: rot90 = -2 + # Now supported + # if any([isclose(prt,a) for a in [90,270,180,-180]]): + # print(f'WARNING: rotations by {prt} not fully supported') + + from math import sqrt + hex_width = psx * scw * 2 # HEX_WIDTH + hex_height = psy * sch * sqrt(3) # HEX_HEIGHT + with VerboseGuard('Picture') as v: + v(f'Transformations: {pa},{pb},{pc},{pd}') + v(f'Scale (x,y): {psx},{psy}') + v(f'Rotation (degrees): {prt} ({rot90})') + v(f'Scale to pixels (x,y): {scw},{sch}') + + # When translating the Tikz coordinates, it is important to note + # that the Tikz y-axis point upwards, while the picture y-axis + # point downwards. This means that the upper right corner is at + # (width,0) and the lower left corner is at (0,height). + def tranx(x,off=-pll[0]): + # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}') + return int(scw * (x + off)+.5) + def trany(y,off=-pur[1]): + # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}') + return -int(sch * (y + off)+.5) + tran = lambda x,y : (tranx(x), trany(y)) + + return hex_width, hex_height, scw * psx, sch * psy, rot90, tran + + # -------------------------------------------------------------------- + def getHexParams(self, + llx, + lly, + urx, + ury, + mx, + my, + hex_width, + hex_height, + rot90, + labels, + coords, + targs, + nargs): + '''rot90 = 0 No rotation + = 1 Rotated -90 (clock-wise) + = -1 Rotated 90 (counter clock-wise) + = -2 Rotated 180 + ''' + with VerboseGuard('Hex parameters') as v: + from math import sqrt + isodd = lambda x : (x % 2 == 1) + iseven = lambda x : (x % 2 == 0) + isfalse = lambda x : False + shorts = {'isodd': isodd, 'iseven': iseven, 'isfalse': isfalse } + + # Funny scaling needed by VASSAL. Seems like they only + # really about the absolute value of 'dy' and then the + # aspect ratio between dx and dy. + pxfac = sqrt(3)/2 + hex_pw = hex_height * pxfac + hex_ph = hex_width * pxfac + stagger = False + # + # Get parameters from coordinates. These should always be set + # + rows = coords .get('row', {}) + columns = coords .get('column',{}) + top_short = columns .get('top short', 'isfalse') + bot_short = columns .get('bottom short','isfalse') + inv_col = columns .get('factor',1) + inv_row = rows .get('factor',1) + voff = -rows .get('offset',0) # 0: from 0 -> -1 + hoff = -columns.get('offset',0) # -1: from 1 -> -2 + vdesc = inv_row == 1 + hdesc = inv_col == -1 + # + # Calculate total dimensions, and number of columns and rows + # + w = abs((urx-llx) - 2 * mx) + h = abs((ury-lly) - 2 * my) + if abs(rot90) == 1: h, w = w, h + nc = int(w // (hex_width * 3 / 4)) + nr = int(h // (hex_height)) + namrot = {0: 'none - 0', + -1: '-90 - CCW', + 1: '90 CW', + -2: '180 - half-turn'} + + v(f'Width: {w}') + v(f'Height: {h}') + v(f'Margins: x={mx} y={my}') + v(f'Rotation: {rot90} ({namrot[rot90]})') + v(f'Labels: {labels}') + v(f'Columns:') + v(f' size: {nc}') + v(f' start: {hoff}') + v(f' direction: {inv_col}') + v(f' top short: {top_short}') + v(f' bottom short: {bot_short}') + v(f'Rows:') + v(f' size: {nr}') + v(f' start: {voff}') + v(f' direction: {inv_row}') + v(f'Image:') + v(f' BB: ({llx},{lly}) x ({urx},{ury})') + # + # X0 and Y0 are in the local (rotated) frame of the hex grid. + # Thus X is always along hex breadth, and Y along the + # height. Thus the base offset (rotated into the hex frame) differs. + x0 = ury if abs(rot90) == 1 else llx + y0 = llx if abs(rot90) == 1 else ury + # Calculate column,row of corners + llc = hoff + ulc = hoff + lrc = hoff+nc-1 + urc = hoff+nc-1 + # + # Swap in directions + if hdesc: llc, lrc, ulc, urc = lrc, llc, urc, ulc + # + is_short_top = shorts[columns.get('top short', 'isfalse')] + is_short_bot = shorts[columns.get('bottom short','isfalse')] + if is_short_top is isfalse: + # Assume fully populated columns + is_short_top = isodd if iseven(hoff) else iseven + if is_short_bot is isfalse: + is_short_bot = isodd if isodd(hoff) else iseven + + # + # Now we have the hex coordinates of the corners. We can + # now check how things are offset. Before rotation, we + # will have that the first column is offset by hex_pw / 2. + x0 += hex_width / 2 + # + # If the first column is _not_ short on top, then off set + # is simply hex_ph / 2. Otherwise, the offset is hex_ph + y0 += hex_ph / 2 + voff -= 1 + voff -= inv_row + v(f' Initial offset of image {x0},{y0}') + + # Treat each kind of rotation separately. Note that -90 and + # 180 uses the `is_short_bot' while 0 and 90 uses + # `is_short_top'. There might be a way to unify these, if + # offsets and so on may warrent it, but it may be complete + # overkill. + is_off = False + col_map = {0 : (ulc, is_short_top, is_short_bot), + -1 : (urc, is_short_top, is_short_bot), + 1 : (ulc, is_short_bot, is_short_top), + -2 : (urc, is_short_bot, is_short_top) } + col_chk, is_s1, is_s2 = col_map[rot90] + + is_off = is_s1(col_chk) + if is_off: + y0 += hex_ph /2 + + v(f'Is first column off: {is_off}') + + # For full columns, noting more is needed + # + # Below is if some columns are short both top and bottom. + # VASSAL seems to start numbering from a given place, and + # then use that for the rest numbering, and forgets to + # take into account various offsets and the like. hence, + # we need to hack it hard. + if iseven(nc): + v(f'Even number of columns, perhaps hacks') + if rot90 == 0: + # Hacks + # + # If the last column is short in both top and bottom, + # and we have inverse columns, but not inverse rows, + # then add to offset + if inv_col == -1 and inv_row == 1 and \ + is_s1(urc) and is_s2(urc): + voff += 1 + # If the column we check for short is short both top + # and bottom, and we have inverse rows, but not + # inverse columns, then add offset + if inv_row == -1 and inv_col == 1 and \ + is_s2(col_chk) and is_off: + voff += 1 + + if rot90 == -1: + # If the last column is short in both top and bottom, + # and we have inverse columns, then add to offset + if is_s1(urc) and inv_col == -1 and is_s2(urc): + voff -= inv_row + + if rot90 == 1: + voff += inv_row + (inv_row == 1) + # If the first column is short in both top and bottom, + # and we have inverse columns, then add to offset + if is_s1(ulc) and is_s2(ulc) and inv_col == -1: + voff += inv_row + + if rot90 == -2: + voff += inv_row * 2 + # Hacks If the column we check for short is short both + # top and bottom, and we have either inverse rows and + # inverse columns, or rows and columns are normal, + # then add offset + if inv_col == inv_row and is_s1(col_chk) and is_s2(col_chk): + voff += 1 + # If the first column is short in both top and bottom, + # and we have inverse columns and rows, then add to + # offset + if inv_col == inv_row and inv_col == -1 and \ + is_s1(ulc) and is_s2(ulc): + voff += 1 + else: + v(f'Odd number of columns') + voff -= inv_row + if rot90 == 1: + # If we offset in the column direction, add the + # inverse row direction, and if we have inverse rows, + # substract one, otherwise add 2. + voff += (inv_row * hoff + (-1 if inv_row == -1 else 2)) + # If we have a short column, and that column is even, + # then add, otherwise subtract, the inverse row + # direction, if the checked column is even. + voff += ((1 if is_off else -1) * + inv_row if is_short_bot(2) else 0) + if rot90 == 2: + voff += inv_row * (2 + is_off) # OK for odd + + + if rot90 == 0: + if inv_col == -1 and iseven(nc): # OK + stagger = not stagger + hoff -= (inv_col == -1) # OK + + if rot90 == -1: # CCW + if inv_col == 1 and iseven(nc): # OK + stagger = not stagger + vdesc, hdesc = hdesc, vdesc + vdesc = not vdesc + voff += (inv_row == 1) + hoff -= (inv_col == 1) # OK + + if rot90 == 1: # CW + if (inv_col == 1 and iseven(nc)) or isodd(nc): # OK + stagger = not stagger + vdesc, hdesc = hdesc, vdesc + hdesc = not hdesc + hoff -= (inv_col == -1) # OK + + if rot90 == -2: + if (inv_col == -1 and iseven(nc)) or isodd(nc): # OK + stagger = not stagger + vdesc, hdesc = not vdesc, not hdesc + hoff -= (inv_col == 1) # OK + + # Labels + if labels is not None: + labmap = { + 'auto': { + 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }, + 'auto=numbers' : { + 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }, + 'auto=alpha column': { + 'hLeading': 0,'vLeading': 0,'hType': 'A','vType': 'N' }, + 'auto=alpha 2 column': {# Not supported + 'hLeading': 1,'vLeading': 1,'hType': 'A','vType': 'N' }, + 'auto=inv y x plus 1': { + 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }, + 'auto=x and y plus 1': { + 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' } + } + for l in labels.split(','): + nargs.update(labmap.get(l,{})) + if 'alpha column' in l or 'alpha 2 column' in l: + hoff -= 1 # VASSAL 0->A, wargame 1->A + if l == 'auto=inv y x plus 1': + hoff += 1 + #inv_row = not inv_row + if l == 'auto=x and y plus 1': + hoff -= 1 + voff -= 1 + + # Add margins + x0 += int(mx) + y0 += int(my) + + targs['dx'] = hex_pw + targs['dy'] = hex_ph + nargs['vOff'] = voff + nargs['hOff'] = hoff + nargs['vDescend'] = vdesc + nargs['hDescend'] = hdesc + targs['edgesLegal'] = True + targs['sideways'] = abs(rot90) == 1 + nargs['stagger'] = stagger + targs['x0'] = int(x0+.5) + targs['y0'] = int(y0+.5) + + # -------------------------------------------------------------------- + def getRectParams(self,i,llx,ury,width,height,targs,nargs): + targs['dx'] = width + targs['dy'] = height + targs['x0'] = int(llx - width/2) + targs['y0'] = int(ury + height/2) + targs['color'] = rgb(0,255,0) + nargs['color'] = rgb(0,255,0) + nargs['vDescend'] = True + nargs['vOff'] = -3 + nargs.update({'sep':',','vLeading':0,'hLeading':0}) + + # ---------------------------------------------------------------- + def addZones(self, + zoned, + name, + info, + width, + height, + labels=None, + coords=None, + picinfo=None): + '''Add zones to the Zoned element. + + Parameters + ---------- + zoned : Zoned + Parent element + name : str + Name of Zoned + info : dict + Dictionary of zones informatio + width : int + Width of parent + height : int + Height of parent + labels : list + On recursive call, list of labels + coords : list + On recursive call, coordinates + picinfo : dict + On recursive call, picture information + ''' + grids = [] + picture = None + + with VerboseGuard(f'Adding zones to {name}') as v: + for k, val in info.items(): + if k == 'labels': labels = val; + if k == 'coords': coords = val + if k == 'zoned': picture = val + if 'zone' not in k or k == 'zoned': + continue + + grids = [[k,val]] + grids # Reverse order! + # grids.append([k,v]) + + if len(grids) < 1: + return + + if picinfo is None: + picinfo = self.getPictureInfo(picture,name,width,height) + + hex_width, hex_height, scx, scy, rot90, tran = picinfo + + for g in grids: + n, i = g + v(f'Adding zone {n}') + + if 'scope' in n: + llx,lly = tran(*i['global lower left']) + urx,ury = tran(*i['global upper right']) + path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]] + nm = n.replace('zone scope ','') + elif 'path' in n: + path = [tran(*p) for p in i['path']] + llx = min([px for px,py in path]) + ury = max([py for px,py in path]) + nm = n.replace('zone path ','') + + # Checkf if we have "point" type elements in this object and + # add them to dict. + points = [ val for k,val in i.items() + if (k.startswith('point') and + isinstance(val,dict) and \ + val.get('type','') == 'point')] + + pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path]) + v(f'Zone path ({llx},{ury}): {pathstr} ({len(points)})') + + ispool = 'pool' in n.lower() and len(points) <= 0 + zone = zoned.addZone(name = nm, + locationFormat = ("$name$" + if ispool else + "$gridLocation$"), + useParentGrid = False, + path = pathstr) + + # Do not add grids to pools + if ispool: + v('Board {n} is pool with no points') + continue + + targs = {'color':rgb(255,0,0),'visible':self._visible} + nargs = {'color':rgb(255,0,0),'visible':self._visible} + # print(targs,nargs) + if 'turn' in n.lower(): nargs['sep'] = 'T' + if 'oob' in n.lower(): nargs['sep'] = 'O' + + if len(points) > 0: + with VerboseGuard('Using region grid') as vv: + grid = zone.addRegionGrid(snapto = True, + visible = self._visible) + for j,p in enumerate(points): + pn = p["name"].strip() + pp = p.get('parent','').strip() + pc = p["coords"] + if j == 0: vv(f'',end='') + vv(f'[{pn}] ',end='',flush=True,noindent=True) + + x, y = tran(*pc) + r = grid.addRegion(name = pn, + originx = x, + originy = y, + alsoPiece = True, + prefix = pp) + v('') + + elif 'hex' in n.lower(): + margin = i.get('board frame',{}).get('margin',0) + mx = scx * margin + my = scy * margin + # self.message(f'{margin} -> {scx},{scy} -> {mx},{my}') + w = abs(urx - llx)-2*mx + h = abs(ury - lly)-2*my + self.getHexParams(llx = llx, + lly = lly, + urx = urx, + ury = ury, + mx = mx, + my = my, + hex_width = hex_width, + hex_height = hex_height, + rot90 = rot90, + labels = labels, + coords = coords, + targs = targs, + nargs = nargs) + + v(f'Adding hex grid') + + grid = zone.addHexGrid(**targs) + grid.addNumbering(**nargs) + + else: + width = hex_width / HEX_WIDTH * RECT_WIDTH + height = hex_height / HEX_HEIGHT * RECT_HEIGHT + self.getRectParams(i,llx,ury,width,height,targs,nargs) + + v(f'Adding rectangular grid') + + grid = zone.addSquareGrid(**targs) + grid.addNumbering(**nargs) + + + # Once we've dealt with this grid, we should see if we have + # any embedded zones we should deal with. + self.addZones(zoned,name,i,width,height, + labels=labels, + coords=coords, + picinfo=picinfo) + + + # ---------------------------------------------------------------- + def addBoards(self): + '''Add Boards to the module + ''' + with VerboseGuard('Adding boards') as v: + hasFlipped = False + for cn,cd in self._categories.get('counter',{}).items(): + for sn in cd: + if ' flipped' in sn: + hasFlipped = True + break + + v(f'Has flipped? {hasFlipped}') + for bn, b in self._categories.get('board',{}).get('all',{}).items(): + self.addBoard(bn, b,hasFlipped=hasFlipped) + + + # ---------------------------------------------------------------- + def getIcon(self,name,otherwise): + with VerboseGuard(f'Get Icon {name}') as v: + icon = self._categories\ + .get('icon',{})\ + .get('all',{})\ + .get(name,{ + 'filename':otherwise})['filename'] + v(f'Using "{icon}"') + return icon + + # ---------------------------------------------------------------- + def addOOBs(self): + '''Add OOBs to the game''' + oobc = self._categories.get('oob',{}).get('all',{}).items() + if len(oobc) < 1: + return + + with VerboseGuard(f'Adding OOBs') as v: + icon = self.getIcon('oob-icon','/images/inventory.gif') + v(f'Using icon "{icon}" for OOB') + charts = \ + self._game.addChartWindow(name='OOBs', + hotkey = self._oobKey, + description = 'OOBs', + text = '', + icon = icon, + tooltip = 'Show/hide OOBs') + tabs = charts.addTabs(entryName='OOBs') + + for on, o in oobc: + widget = tabs.addMapWidget(entryName=on) + self.addOOB(widget, on, o) + + + # ---------------------------------------------------------------- + def addOOB(self,widget,name,info): + '''Add a OOB elements to the game + + Parameters + ---------- + widget : Widget + Widget to add to + name : str + Name + info : dict + Information on the OOB image + ''' + map = widget.addWidgetMap(mapName = name, + markMoved = 'Never', + hotkey = '') + map.addCounterDetailViewer() + map.addStackMetrics() + map.addImageSaver() + map.addTextSaver() + map.addForwardToChatter() + map.addMenuDisplayer() + map.addMapCenterer() + map.addStackExpander() + map.addPieceMover() + map.addKeyBufferer() + map.addSelectionHighlighters() + map.addHighlightLastMoved() + map.addZoomer() + + picker = map.addPicker() + ulx,uly,lrx,lry = self.getBB(info['img']) + board = picker.addBoard(name = name, + image = info['filename']) + zoned = board.addZonedGrid() + zoned.addHighlighter() + + if not 'zones' in info: + zone = zoned.addZone(name = 'full', + useParentGrid = False, + path=(f'{ulx},{uly};' + + f'{lrx},{uly};' + + f'{lrx},{lry};' + + f'{ulx},{lry}')) + grid = zone.addSquareGrid() + grid.addNumbering() + + return + + # If we get here, we have board info! + w = abs(ulx-lrx) + h = abs(uly-lry) + self.addZones(zoned,name,info['zones'],w,h) + + # ---------------------------------------------------------------- + def addCharts(self): + '''Add Charts elements to game + ''' + chartc = self._categories.get('chart',{}).get('all',{}).items() + if len(chartc) < 1: + return + + with VerboseGuard('Adding charts') as v: + charts = self._game.addChartWindow(name = 'Charts', + hotkey = self._chartsKey, + description = '', + text = '', + tooltip = 'Show/hide charts', + icon = self.getIcon('chart-icon', + '/images/chart.gif')) + tabs = charts.addTabs(entryName='Charts') + for i, (cn, c) in enumerate(chartc): + if i == 0: v('',end='') + v(f'[{cn}] ',end='',flush=True,noindent=True) + + tabs.addChart(chartName = cn, + description = cn, + fileName = c['filename']) + + v('') - - Convert each page in the 'pdfname' file (a PDF generated by - LaTex) into images + # ---------------------------------------------------------------- + def addDie(self): + '''Add a `Die` element to the module + ''' + if self._dice is not None: + return + self._game.addDiceButton(name = '1d6', + hotkey = self._diceKey) - - Create the VASSAL module +# +# EOF +# +# ==================================================================== +# From main.py - ''' - if verbose: - print(f'Module name : {vmodname}\n' - f'PDF name : {pdfname}\n' - f'Info name : {infoname}\n' - f'Title : {title}\n' - f'Version : {version}\n' - f'Description : {desc}\n' - f'Rules : {rules}\n' - f'Tutorial : {tutorial}') - info = convert_pages(pdfname=pdfname,infoname=infoname) - return create_vmod(vmodname = vmodname, - info = info, - title = title, - version = version, - desc = desc, - rules = rules, - tutorial = tutorial, - pdfname = pdfname, - infoname = infoname, - patch = patch, - verbose = verbose, - visible = visible) - # ==================================================================== if __name__ == '__main__': from argparse import ArgumentParser, FileType @@ -3695,15 +11549,25 @@ if __name__ == '__main__': help='Short description of module', type=str, default='draft of module') + ap.add_argument('-W','--vassal-version', + help='Vassal version number', + type=str, + default='3.6.7') ap.add_argument('-V','--verbose', help='Be verbose', action='store_true') ap.add_argument('-G','--visible-grids', action='store_true', help='Make grids visible in the module') - ap.add_argument('-n','--notes', - help='Show notes', - action='store_true') + ap.add_argument('-N','--no-nato-prototypes', + action='store_true', + help='Do not make prototypes for types,echelons,commands') + ap.add_argument('-C','--no-chit-information', + action='store_true', + help='Do not make properties from chit information') + ap.add_argument('-S','--counter-scale', + type=float, default=1, + help='Scale counters by factor') args = ap.parse_args() @@ -3720,27 +11584,27 @@ if __name__ == '__main__': if args.version.lower() == 'draft': args.visible_grids = True + Verbose().setVerbose(args.verbose) + try: - notes = export(vmodname = vmodname, - pdfname = args.pdffile.name, - infoname = args.infofile.name, - title = args.title, - version = args.version, - desc = args.description, - rules = rulesname, - tutorial = tutname, - patch = patchname, - verbose = args.verbose, - visible = args.visible_grids) - - if args.notes: - print(f'Module {vmodname} created. ' - 'Here are the notes from the module') - print(notes) - + exporter = LaTeXExporter(vmodname = vmodname, + pdfname = args.pdffile.name, + infoname = args.infofile.name, + title = args.title, + version = args.version, + description = args.description, + rules = rulesname, + tutorial = tutname, + patch = patchname, + visible = args.visible_grids, + vassalVersion = args.vassal_version, + nonato = args.no_nato_prototypes, + nochit = args.no_chit_information, + counterScale = args.counter_scale) + exporter.run() except Exception as e: from sys import stderr - print(f'Failed to build {vmodname}: e',file=stderr) + print(f'Failed to build {vmodname}: {e}',file=stderr) from os import unlink try: unlink(vmodname) @@ -3748,3 +11612,10 @@ if __name__ == '__main__': pass raise e + +# +# EOF +# +## +# End of generated script +## diff --git a/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py b/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py index a9f3c60203..0800e18130 100755 --- a/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py +++ b/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py @@ -204,7 +204,7 @@ def path_to_tikz(path,f='8.5f'): ''' from svg.path import Line, Move, Close, CubicBezier, Path - code = [] + code = [f'% '] for e in path: if isinstance(e, Move): code.append(tikz_move(e, f)) elif isinstance(e, Line): code.append(tikz_line(e, f)) diff --git a/macros/latex/contrib/wargame/source/wargame.dtx b/macros/latex/contrib/wargame/source/wargame.dtx index d721befdb6..e8a3b9b1e1 100644 --- a/macros/latex/contrib/wargame/source/wargame.dtx +++ b/macros/latex/contrib/wargame/source/wargame.dtx @@ -44,6 +44,11 @@ \date{\today} \begin{document} \maketitle +\begin{center} + \begin{tikzpicture} + \wargamelogo + \end{tikzpicture} +\end{center} \begin{abstract} This package provides tools to typesetting manuals, board, and counters for wargames using \LaTeX{}. @@ -92,6 +97,10 @@ % typeset using a similar interface. Unit types are identified using % the NATO Joint Military Symbology \cite{app6c} standard. % +% This document is meant as a reference manual (although far from +% complete). A separate tutorial is available, and may be the best +% starting point. +% % \iffalse % -------------------------------------------------------------------- % @@ -127,17 +136,26 @@ % \item[\keyval{town}{\meta{town-keys}}] specifies that a town (or % similar) is present in the hex. The various keys are described in % \sectionname~\ref{sec:hex:town}. +% \item[\keyval{bevel}{\meta{bevel-keys}}] specifies that a bevel +% should be added to the hex. The various keys are described in +% \sectionname~\ref{sec:hex:bevel}. % \item[\keyval{extra}{\meta{extra-keyx}} \textrm{and} \keyval{extra % clipped}{\meta{extra-keyx}}] allows the user to put custom % graphics in the hexes. See also \sectionname~\ref{sec:hex:extra} % for more. +% \item[\keyval{row}{\meta{row}} \textrm{and} +% \keyval{column}{\meta{column}}] Keys to set hex coordinates. +% Mainly used when using \cs{node} rather than \cs{hex}. These +% coordinates should be specified in the \spec{hex cs} coordinate +% system (\sectionname~\ref{sec:hex:cs}). % \item any style key defined for \TikZ{} pictures. % \end{description} % % The \meta{location} argument specifies the coordinates, in the hex % coordinate system where to put the hex. More about the coordinate % system is given in \sectionname~\ref{sec:hex:cs}. Note, the numbers -% starts from the lower--left corner. +% by default starts from the lower--left corner, but can be changed +% via options. % % The elements are rendered in the following order % \begin{enumerate} @@ -146,6 +164,7 @@ % \item The ridges, if any % \item The label, if any % \item Extra graphics clipped to the hex +% \item Bevel if selected % \item Town, if any % \item Extra graphics which may extend beyond the confines of the % hex. @@ -180,7 +199,46 @@ % indicate two units of length.} % \label{fig:hex:parts} % \end{figure} +% +% Note that the macro \cs{hex} is really a short hand for \TikZ{}'s +% \cs{node} macro, but with preset options. An alternative to using +% the \cs{hex} macro is to do % +% \begin{MacroSyntax}{\hex} +% \cs{node}[hex=\marg{key-value-pairs}] \parg{name} at \parg{location}; +% \end{MacroSyntax} +% +% This can be useful when placing explanatory graphics or the like. +% The main difference between using \cs{hex} and the raw +% \cs{node}\texttt{[hex=\textellipsis]} is that the former can +% automatically generate labels and set shape coordinates in the +% picture. If you want that for your board, it is recommended to use +% \cs{hex}. For example, if one does +% +% \begin{verbatim} +% \begin{tikzpicture}[ +% every hex={label={auto=alpha column}}, +% hex/labels is name=true] +% \hex(c=1,r=1); +% \end{tikzpicture} +% \end{verbatim} +% +% then one can refer to the location of the hex by its label i.e., +% \texttt{(A1)}. Since the hex is really a \TikZ{} \texttt{node}, we +% can also use anchors defined for \texttt{hex} node shape, such as +% \texttt{(A1.west)}, \texttt{(A1.north edge)}, and so on. This is +% not possible if one uses the \cs{node} macro. +% +% \subsection{Hex bevels} +% \label{sec:hex:bevel} +% +% A bevel (or ``shadow-effect'') can be added to hexes using the key +% \spec{bevel}, with a value that specifies where the light comes from +% (e.g., north west or NW). The percentage of the half width of a +% chit of the bevel can be specified by the key \spec{bevel fraction} +% (default 10\%). +% +% % \subsection{Styling hexes} % % Typical \TikZ{} options can be passed to the \cs{hex} macro. For @@ -200,6 +258,18 @@ % \end{scope} % \end{tikzpicture} % \end{verbatim} +% +% For example, to render only the corners of the hexes, as popular +% among some designers, one can do +% +% \begin{verbatim} +% every hex/.style={ +% dash pattern=on .2cm off .6cm on .2cm off 0cm +% }, +% \end{verbatim} +% Note that the dash pattern should be 1cm long and the last element +% should be \spec{off 0cm} so the dash pattern is started afresh on +% each hex edge. % % \subsection{Hex coordinate system} % \label{sec:hex:cs} @@ -216,7 +286,7 @@ % where \meta{vertex} and \meta{edge} are optional. The hex row and % column defaults both to 0 and can be decimal numbers. The % \spec{row}, \spec{column}, \spec{vertex}, and \spec{edge} keywords -% may be shorted to \spec{r}, \spec{c}, \spec{v}, and \spec{e}, +% may be shortened to \spec{r}, \spec{c}, \spec{v}, and \spec{e}, % respectively. Possible vertexes and edges are listed in % \tablename~\ref{tab:hex:ve}. % @@ -320,9 +390,15 @@ % coordinates are shifted upward or downward for smaller or larger % numbers. Figure~\ref{fig:hex:coord:nb} illustrates. this. This can % make it a little hard to specify coordinates relative to a hex -% centre. Alternatively one may use vertex or edge specifciations -% together with a relative offset in those directions. -% \ +% centre. Alternatively one may use vertex or edge specifications +% together with a relative offset in those directions. If one +% require even more flexibility, one can use the \TikZ{} library +% \spec{calc} to add arbitrary offsets, e.g., +% +% \begin{verbatim} +% \coordinate at ($(hex cs:c=1,r=10)+(.2,.2)$); +% \end{verbatim} +% % \begin{figure} % \centering % \begin{tikzpicture} @@ -1014,6 +1090,15 @@ % \caption{Adding rivers, boarders, and roads} % \end{figure} % +% \subsubsection{Styling paths} +% +% Rivers, roads, railroads, and borders are styled by +% \spec{hex/river}, \spec{hex/road}, \spec{hex/railroad}, and +% \spec{border}, respectively, and the keys \spec{every hex river}, +% \spec{every hex road}, \spec{every hex railroad}, and \spec{every +% hex border} will also be applied. The latter can be defined by the +% user. +% % \subsection{Board clipping and frame} % % In the river, border, and road example above, the roads extend beyond @@ -1117,6 +1202,134 @@ % The \cs{boardframe} macro prints the position of the rectangle to % the log output, if one needs to do some more stuff around the board. % +% \subsection{Constructing the physical board} +% +% If the board is not too large, so that it may fit on a paper format +% that can easily be printed (say A4, A3, Letter, or Tabloid), one can +% simply print the board and glue it onto a sturdy surface (say +% 1\textonehalf\,mm poster carton). However, if the board is large, +% meaning it does not fit on a piece of printable paper, then one has +% two options. +% \begin{description} +% \item[\textit{Either}] scale the board down so that it fits. Use +% the \TikZ{} key \spec{scale=}\meta{factor} as an argument to the +% \spec{tikzpicture} environment in which you create the board. In +% this case, you should make sure you also scale the chits by the +% same \meta{factor}, again via the \spec{scale} key. +% \item[\textit{Or}] you can split the board over several pages. The +% package provides a number of tools to help with this. +% \end{description} +% +% \subsubsection{Split the board over multiple sheets} +% +% First, make sure you produce a standalone PDF of the board only. +% +% \begin{verbatim} +% \documentclass{standalone} +% \usepackage{wargame} +% \begin{tikzpicture}[scale=SCALE] +% % Define the board here. +% \end{tikzpicture} +% \end{verbatim} +% +% and that you have created this PDF --- say \texttt{board.pdf}. +% +% Next, prepare another document in which we will do the calculations. +% For example +% +% \begin{verbatim} +% \documentclass[11pt]{standalone} +% \usepackage{wargame} +% \begin{document} +% \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1} +% \end{document} +% \end{verbatim} +% +% to calculate the split of \spec{board.pdf} over $2\times2$ letter +% paper sheets, with a non-printable margin of 7mm, and an overlap +% between the segments of 1cm. +% +% The possible keys for the \cs{splitboard} macro are +% +% \begin{itemize} +% \item \spec{paper}=\meta{format}: Specifies the paper format. One +% of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default +% is \spec{a4}. +% \item \spec{landscape}: Sets the paper format to be in landscape +% mode (default is portrait). +% \item \spec{margin}=\meta{size in centimetres}: Size of margins on +% each sheet in centimetres \emph{without} unit. That is put +% \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}. +% This should be \emph{slightly} larger (by roughly 5\%) than the +% \emph{least} margin required by the printer used. \emph{Must} be +% given \emph{before} \spec{paper} to have any effect. +% \item \spec{ncol}=\meta{number of columns}: Sets the number of +% columns of sheets. +% \item \spec{nrow}=\meta{number of rows}: Set the number of rows of +% sheets. +% \item \spec{overlap}=\meta{size in centimetres}: Sets the size of +% the overlap between sheets in centimetres \emph{without} unit. +% That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is +% \spec{2}. +% \item \spec{image}=\meta{image file name}: File name of the board +% image (a PDF). Default is \spec{board} +% \item \spec{output}=\meta{output file name}: File name (without +% \spec{.tex} ending) to write calculated split to. +% \item \spec{standalone}: Boolean flag. If true, then output file +% will be a standalone document (i.e., has a \cs{documentclass}). +% \item \spec{scale}=\meta{scale}: Set scale of board. +% \end{itemize} +% +% The macro will produce a file named \meta{output file +% name}\spec{.tex} which can be included in another document to +% generate the split board PDF. Crop marks will be added to the +% board segments to make it easier to align the parts. +% +% \subsubsection{Foldable board} +% +% To make a fold-able board use for example the below template to +% create grooves and cuts. +% +% \begin{tikzpicture}[ +% cut/.style={red!50!black,thick}, +% front groove/.style={green!50!black,dashed,thick}, +% back groove/.style={blue!50!black,dash dot,thick}, +% ] +% \node[minimum width=8.4cm, +% minimum height=5.9cm, +% inner sep=0pt, +% draw, +% thick] (a) {}; +% \coordinate (middle) at (a); +% \coordinate (top) at (a.north); +% \coordinate (top left) at ($(a.north west)!.5!(a.north)$); +% \coordinate (middle left) at ($(a.west)!.5!(a)$); +% \coordinate (bottom left) at ($(a.south west)!.5!(a.south)$); +% \coordinate (top right) at ($(a.north)!.5!(a.north east)$); +% \coordinate (middle right) at ($(a)!.5!(a.east)$); +% \coordinate (bottom right) at ($(a.south)!.5!(a.south east)$); +% \coordinate (bottom) at (a.south); +% \draw[cut] (middle)--(bottom); +% \draw[cut] (top left)--(middle left); +% \draw[cut] (top right)--(middle right); +% \draw[back groove] (middle)--(top); +% \draw[back groove] (bottom left)--(middle left); +% \draw[back groove] (bottom right)--(middle right); +% \draw[front groove] (a.west)--(a.east); +% +% \draw[cut] ($(a.south west)+(0,-1)$)--++(2,0) node[anchor=west]{% +% Cut through carton}; +% \draw[back groove] ($(a.south west)+(0,-1.5)$)--++(2,0) node[anchor=west]{% +% Cut groove (\textonehalf{} through) in carton on \emph{back} side}; +% \draw[front groove] ($(a.south west)+(0,-2)$)--++(2,0) node[anchor=west]{% +% Cut groove (\textonehalf{} through) in carton on \emph{front} side}; +% \end{tikzpicture} +% +% This will fold the board down to a fourth of the size of the full +% map. For example, if the board is A1 +% ($84\,\mathrm{cm}\times59.4\,\mathrm{cm}$) it will fold down to A4 +% ($21\,\mathrm{cm}\times29.7\,\mathrm{cm}$) for easier storage. +% % \iffalse % -------------------------------------------------------------------- % @@ -1228,7 +1441,22 @@ % \caption{Example of chits fit within hexes. } % \label{fig:chit:hex} % \end{figure} +% +% Just as \cs{hex} is really a wrapper around \TikZ{}'s \cs{node} +% macro, so it is with \cs{chit}. This means that an alternative way +% of making a chit is to do +% +% \begin{MacroSyntax}{\chit} +% \cs{node}[chit=\marg{key-value-pairs}] \parg{name} at \parg{location}; +% \end{MacroSyntax} +% +% Since chits are really \TikZ{} \texttt{node}s we can use anchors +% on the chit. Unlike for \cs{hex} where there are additional +% features available when using the dedicated macro, there really +% isn't much difference between \cs{chit} and +% \cs{node}\texttt{[chit=\textellipsis]}. % +% % \subsection{Styling chits} % % Typical \TikZ{} options can be passed to the \cs{chit} macro. For @@ -1269,9 +1497,21 @@ % similar keys. For example, the picture \spec{chit/identifier} is % styled by \spec{tikz/chit/identifier}. % +% +% A bevel (or ``shadow-effect'') can be added to chits using the key +% \spec{bevel}, with a value that specifies where the light comes from +% (e.g., north west or NW). The percentage of the half width of a +% chit of the bevel can be specified by the key \spec{bevel fraction} +% (default 10\%). This can be used for both symbol or full chits. +% % In addition, one can define the key \spec{tikz/every chit} to be the % default options for all chits. % +% By default, the outer ``frame'' of a chit is drawn with the same +% graphics options as the chit it self (i.e., same fill and stroke +% colour). To change that, one can pass \spec{frame}=\marg{options} +% as part of the chit options. +% % \subsection{Defining preset chit types} % % One can conveniently pre-define some chit styles. For example, @@ -1314,7 +1554,7 @@ % The NATO markers are designed to fit within the template shown in % \figurename~\ref{fig:natoapp:template}. The template is serves as a % placement guide of the the various parts of the NATO marker as -% illustrated in \figurename{fig:natoapp:usage}. +% illustrated in \figurename~\ref{fig:natoapp:usage}. % % \begin{figure}[htbp] % \centering @@ -1688,6 +1928,9 @@ % % \subsection{Unit type identification} % +% +% See \tablename~\ref{tab:natoapp6c:abbr}. +% % \begin{table} % \centering% % \def\typemark#1{% @@ -1737,7 +1980,7 @@ % & FA\\ % \typemark{main=infantry} % & Infantry -% & IN\\ +% & IN\footnote{Sometimes just `I'}\\ % \rowcolor{altcol} % \typemark{main={infantry,armoured}} % & Mechanised infantry @@ -1775,7 +2018,7 @@ % \rowcolor{altcol} % \sizemark{division} % & Division -% & D\\ +% & D\footnote{Sometimes `DIV'}\\ % \sizemark{brigade} % & Brigade % & BD\\ @@ -1828,7 +2071,9 @@ % % \begin{thebibliography}{99} % \bibitem{fb} Hanover,C., Hendrix,C.E., \& Llewelyn,S., -% \textit{First Blood}, 1997, \url{https://grognard.com/fb/} +% \textit{First Blood}, 1997, \url{https://grognard.com/fb/}. See +% also implementation using this package at +% \url{https://gitlab.com/wargames_tex/firstblood_tex}. % \bibitem{app6c} \textit{NATO Joint Military Symbology}, APP-6(C), % May 2011, % \url{https://en.wikipedia.org/wiki/NATO_Joint_Military_Symbology}. @@ -2453,3 +2698,5 @@ % TeX-command-extra-options: "-shell-escape" % End: % \fi + + diff --git a/macros/latex/contrib/wargame/source/wargame.ins b/macros/latex/contrib/wargame/source/wargame.ins index 23ec10e251..44acad580e 100644 --- a/macros/latex/contrib/wargame/source/wargame.ins +++ b/macros/latex/contrib/wargame/source/wargame.ins @@ -81,10 +81,17 @@ End: \file{symbols.tex} {\from{wargame.dtx}{driver,symbols}} % \file{tikzlibrarywargame.util.code.tex}{% - \from{util/core.dtx}{utils}} + \from{util/core.dtx} {utils} + \from{util/misc.dtx} {utils} + \from{util/compound.dtx}{utils} + \from{util/bb.dtx} {utils} + \from{util/tikz.dtx} {utils} + \from{util/randomid.dtx}{utils} + \from{util/icons.dtx} {utils} + } % \file{wgexport.cls}{% - \from{util/core.dtx}{exportcls}} + \from{util/export.dtx}{exportcls}} % \file{tikzlibrarywargame.hex.code.tex}{ \from{hex/core.dtx} {hex} @@ -107,7 +114,8 @@ End: \from{hex/labels.dtx} {hex} \from{hex/extra.dtx} {hex} \from{hex/paths.dtx} {hex} - \from{hex/board.dtx} {hex}} + \from{hex/board.dtx} {hex} + \from{hex/split.dtx} {hex}} % \file{tikzlibrarywargame.natoapp6c.code.tex}{ \from{natoapp6c/core.dtx} {natoapp6c} @@ -125,10 +133,16 @@ End: \from{natoapp6c/list.dtx} {natoapp6c}} % \file{tikzlibrarywargame.chit.code.tex}{ - \from{chit/core.dtx} {chit} - \from{chit/shape.dtx} {chit} - \from{chit/elements.dtx}{chit} - \from{chit/misc.dtx} {chit}} + \from{chit/core.dtx} {chit} + \from{chit/shape.dtx} {chit} + \from{chit/elements.dtx} {chit} + \from{chit/modifiers.dtx}{chit} + \from{chit/stack.dtx} {chit} + \from{chit/oob.dtx} {chit} + \from{chit/table.dtx} {chit} + \from{chit/battle.dtx} {chit} + \from{chit/dice.dtx} {chit} + \from{chit/misc.dtx} {chit}} % \file{wargame.beach.tex} {\from{hex/tile.dtx}{tile,beach}} \file{wargame.city.tex} {\from{hex/tile.dtx}{tile,city}} diff --git a/macros/luatex/generic/luavlna/README.md b/macros/luatex/generic/luavlna/README.md index df27afa648..870552cd0f 100644 --- a/macros/luatex/generic/luavlna/README.md +++ b/macros/luatex/generic/luavlna/README.md @@ -1,4 +1,4 @@ -# Luavlna, version 0.1j, 2022-08-15 +# Luavlna, version 0.1k, 2023-03-28 # Introduction diff --git a/macros/luatex/generic/luavlna/luavlna-doc.pdf b/macros/luatex/generic/luavlna/luavlna-doc.pdf index 175e8032d0..900d6c3714 100644 Binary files a/macros/luatex/generic/luavlna/luavlna-doc.pdf and b/macros/luatex/generic/luavlna/luavlna-doc.pdf differ diff --git a/macros/luatex/generic/luavlna/luavlna.lua b/macros/luatex/generic/luavlna/luavlna.lua index f13fe6bd59..5bbe2c060b 100644 --- a/macros/luatex/generic/luavlna/luavlna.lua +++ b/macros/luatex/generic/luavlna/luavlna.lua @@ -5,10 +5,12 @@ -- 1. It is possible to turn this functionality only for some letters -- 2. Code now works even for single letters after brackets etc. -- +-- + local M = {} -local utf_match = unicode.utf8.match -local utf_char = unicode.utf8.char -local utf_len = unicode.utf8.len +-- local utf_match = unicode.utf8.match +local utf_char = utf8.char -- unicode.utf8.char +local utf_len = utf8.len -- unicode.utf8.len local glue_id = node.id "glue" local glyph_id = node.id "glyph" @@ -18,10 +20,36 @@ local penalty_id = node.id "penalty" local math_id = node.id "math" local period_char = string.byte(".") +-- we must process only glues with the spaceskip or xspaceskip subtypes +local allowed_space_names = {spaceskip = true, xspaceskip = true} +local allowed_space_types = {} +local glue_subtypes = node.subtypes("glue") +-- convert allowed subtypes names to numbers used in nodes +for k,v in pairs(glue_subtypes) do + if allowed_space_names[v] then + allowed_space_types[k] = true + end +end + +local unicode_alphas = "0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC0030-0039" + + +-- construct table with unicode alphas for later testing +local alpha_tbl = {} +for start, finish in unicode_alphas:gmatch("(....)-(....)") do + for i = tonumber(start, 16), tonumber(finish, 16) do + alpha_tbl[utf_char(i)] = true + end +end + + + + local alpha = string.char(37).."a" -- alpha class, entering -- percent char directly caused error local alphas = {} -local match_char = function(x) return utf_match(x,alpha) end +-- the following function is not used anymore +-- local match_char = function(x) return utf_match(x,alpha) end local match_table = function(x, chars) local chars=chars or {} return chars[x] @@ -141,7 +169,7 @@ end local is_alpha = function(c) local status = alphas[c] if not status then - status = utf_match(c, alpha) + status = alpha_tbl[c] alphas[c] = status end return status @@ -267,7 +295,7 @@ local function prevent_single_letter (head) end end if skip ~= 1 and not in_math then - if id == glue_id then + if id == glue_id and allowed_space_types[head.subtype] then if wasnumber then if word ~= "" then wasnumber = false diff --git a/macros/luatex/generic/luavlna/luavlna.sty b/macros/luatex/generic/luavlna/luavlna.sty index 31c5e6f284..f8af19876c 100644 --- a/macros/luatex/generic/luavlna/luavlna.sty +++ b/macros/luatex/generic/luavlna/luavlna.sty @@ -1,4 +1,9 @@ -\ProvidesPackage{luavlna}[2022-08-15 luavlna] +\ProvidesPackage{luavlna}[2023-03-28 luavlna] + +\ifdefined\directlua\else +\PackageWarning{luavlna}{Luavlna needs LuaLaTeX, aborting} +\endinput +\fi \RequirePackage{kvoptions} diff --git a/macros/luatex/generic/luavlna/luavlna.tex b/macros/luatex/generic/luavlna/luavlna.tex index dd7415534d..e86df0f325 100644 --- a/macros/luatex/generic/luavlna/luavlna.tex +++ b/macros/luatex/generic/luavlna/luavlna.tex @@ -22,7 +22,6 @@ end % By default, spaces for all single letters are inserted % This can be modified with \singlechars macro set_singlechars = function(lang,chars) - local utf_gmatch = unicode.utf8.gmatch % local langnumbers = languages.names local langid = get_lang_id(lang) if not langid then return nil, "Cannot find language number for: "..lang end @@ -30,8 +29,8 @@ set_singlechars = function(lang,chars) local singlechars = {} local percent = string.char(37) local alpha = percent .. "a" - for char in utf_gmatch(chars,"("..alpha..")") do - singlechars[char] = true + for pos, char in utf8.codes(chars) do + singlechars[utf8.char(char)] = true end luavlna.singlechars(langid,singlechars) end -- cgit v1.2.3